diff --git a/backend/app/services/site_finder/noise_loader.py b/backend/app/services/site_finder/noise_loader.py index cc2f4f6e..30802325 100644 --- a/backend/app/services/site_finder/noise_loader.py +++ b/backend/app/services/site_finder/noise_loader.py @@ -126,6 +126,11 @@ def _way_to_polygon_wkt(nodes: list[dict]) -> str | None: Используется для natural=water (озёра, пруды). Если кольцо не замкнуто автоматически — добавляем первую точку в конец. + + PostGIS отвергает linear ring < 4 точек. Замкнутый way из 3 точек + [A, B, A] (points[0]==points[-1]) даёт POLYGON((A,B,A)) — "geometry + requires more points". Возвращаем None, чтобы caller сделал fallback + на LineString или пропустил элемент. """ points = [f"{n['lon']} {n['lat']}" for n in nodes if "lon" in n and "lat" in n] if len(points) < 3: @@ -133,6 +138,9 @@ def _way_to_polygon_wkt(nodes: list[dict]) -> str | None: # Замкнуть кольцо если нужно if points[0] != points[-1]: points.append(points[0]) + # Итоговое кольцо должно содержать ≥4 точек (PostGIS требование). + if len(points) < 4: + return None return f"POLYGON(({', '.join(points)}))" @@ -203,35 +211,50 @@ def sync_noise_sources_to_db() -> dict[str, int]: **geom_params, } - result = db.execute( - text(f""" - INSERT INTO osm_noise_sources_ekb - (osm_type, osm_id, source_type, road_class, name, geom, tags, fetched_at) - VALUES ( - :osm_type, :osm_id, :source_type, :road_class, :name, - {geom_sql}, - CAST(:tags AS jsonb), NOW() - ) - ON CONFLICT (osm_type, osm_id) DO UPDATE - SET source_type = EXCLUDED.source_type, - road_class = EXCLUDED.road_class, - name = EXCLUDED.name, - geom = EXCLUDED.geom, - tags = EXCLUDED.tags, - fetched_at = NOW() - RETURNING (xmax = 0) AS is_insert - """), - params, - ).scalar() - - if result: - inserted += 1 - else: - updated += 1 + try: + with db.begin_nested(): # SAVEPOINT — откат только этой записи + result = db.execute( + text(f""" + INSERT INTO osm_noise_sources_ekb + (osm_type, osm_id, source_type, road_class, name, geom, tags, + fetched_at) + VALUES ( + :osm_type, :osm_id, :source_type, :road_class, :name, + {geom_sql}, + CAST(:tags AS jsonb), NOW() + ) + ON CONFLICT (osm_type, osm_id) DO UPDATE + SET source_type = EXCLUDED.source_type, + road_class = EXCLUDED.road_class, + name = EXCLUDED.name, + geom = EXCLUDED.geom, + tags = EXCLUDED.tags, + fetched_at = NOW() + RETURNING (xmax = 0) AS is_insert + """), + params, + ).scalar() + if result: + inserted += 1 + else: + updated += 1 + except Exception as e: + # Дефектный way (например, 3-точечное замкнутое кольцо POLYGON((A,B,A)) + # отвергается PostGIS) не должен валить весь weekly-sync. SAVEPOINT + # откатывает только эту строку, продолжаем с остальными. + logger.warning( + "noise_sync insert failed for %s/%s (source_type=%s): %s", + osm_type, + osm_id, + source_type, + e, + ) + skipped += 1 db.commit() - except Exception: + except Exception as e: db.rollback() + logger.exception("noise_sync: unexpected error, outer tx rolled back: %s", e) raise finally: db.close()