From e4ad1f8f7a0652069e12cc09d32620c84118f16d Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 30 May 2026 22:58:52 +0300 Subject: [PATCH] perf(estimator): executemany batch insert in _save_yandex_history_items (Refs #826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace N+1 loop `for row in rows: db.execute(sql, row)` with single `db.execute(sql, rows)` — SQLAlchemy Session passes list-of-dicts as executemany, reducing round-trips to one per batch. Guard with `if rows:` so empty batches skip the execute. Update unit tests to reflect one call with a list instead of N calls with individual dicts. --- tradein-mvp/backend/app/services/estimator.py | 4 +-- .../tests/test_yandex_valuation_save.py | 26 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tradein-mvp/backend/app/services/estimator.py b/tradein-mvp/backend/app/services/estimator.py index 76e58cc0..0e552469 100644 --- a/tradein-mvp/backend/app/services/estimator.py +++ b/tradein-mvp/backend/app/services/estimator.py @@ -659,8 +659,8 @@ def _save_yandex_history_items( ) try: - for row in rows: - db.execute(sql, row) + if rows: + db.execute(sql, rows) db.commit() return len(rows) except Exception as e: diff --git a/tradein-mvp/backend/tests/test_yandex_valuation_save.py b/tradein-mvp/backend/tests/test_yandex_valuation_save.py index 2d79589f..ff138d91 100644 --- a/tradein-mvp/backend/tests/test_yandex_valuation_save.py +++ b/tradein-mvp/backend/tests/test_yandex_valuation_save.py @@ -84,10 +84,11 @@ def test_save_row_contains_house_id_and_confidence(): ): _save_yandex_history_items(db, result) - # db.execute called twice (one per item) - assert db.execute.call_count == 2 - for c in db.execute.call_args_list: - row = c.args[1] + # db.execute called once with list-of-dicts (executemany, one round-trip) + assert db.execute.call_count == 1 + rows = db.execute.call_args_list[0].args[1] + assert isinstance(rows, list) and len(rows) == 2 + for row in rows: assert row["house_id"] == 54321 assert row["confidence"] == pytest.approx(0.9) assert row["notes"] == "match_method=fingerprint" @@ -117,8 +118,9 @@ def test_save_row_contains_removed_date(): ): _save_yandex_history_items(db, result) - row = db.execute.call_args_list[0].args[1] - assert row["removed_date"] == date(2024, 5, 20) + # args[1] is now the list-of-dicts passed to executemany + rows = db.execute.call_args_list[0].args[1] + assert rows[0]["removed_date"] == date(2024, 5, 20) def test_save_row_removed_date_none_when_active(): @@ -144,8 +146,8 @@ def test_save_row_removed_date_none_when_active(): ): _save_yandex_history_items(db, result) - row = db.execute.call_args_list[0].args[1] - assert row["removed_date"] is None + rows = db.execute.call_args_list[0].args[1] + assert rows[0]["removed_date"] is None def test_save_handles_match_failure_gracefully(): @@ -172,10 +174,10 @@ def test_save_handles_match_failure_gracefully(): saved = _save_yandex_history_items(db, result) assert saved == 1 - row = db.execute.call_args_list[0].args[1] - assert row["house_id"] is None - assert row["confidence"] == pytest.approx(0.0) - assert row["notes"] is None + rows = db.execute.call_args_list[0].args[1] + assert rows[0]["house_id"] is None + assert rows[0]["confidence"] == pytest.approx(0.0) + assert rows[0]["notes"] is None def test_save_match_called_with_address_and_year():