From aa8765a83e3db9e2f7751b05ed3581eb55064e97 Mon Sep 17 00:00:00 2001 From: jasper blues Date: Fri, 6 Nov 2026 02:32:27 +1000 Subject: [PATCH] =?UTF-8?q?Mark=20the=20Postgres=20CALL=E2=80=A6RETURN=20t?= =?UTF-8?q?iming=20case=20as=20an=20xfail=20for=20#420,=20as=20the=20MERGE?= =?UTF-8?q?=20case=20is?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drivine4j#609 found a second cause of #420: a CALL joins its one-row list to the node table for each row, and Postgres, taking the list for 100 rows, may scan every node for it. drivine-python CI now fails the sync case on Python 3.13 (36.8 s and 36.5 s over 20,000 rows). The mark is not strict, as the MERGE one isn't, and comes off with #420's fix. --- tests/integration/sql/test_compiled_write_loop_time.py | 6 ++++++ tests/sync_integration/sql/test_compiled_write_loop_time.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/integration/sql/test_compiled_write_loop_time.py b/tests/integration/sql/test_compiled_write_loop_time.py index 6d84f73..0e3f43a 100644 --- a/tests/integration/sql/test_compiled_write_loop_time.py +++ b/tests/integration/sql/test_compiled_write_loop_time.py @@ -10,6 +10,9 @@ On Postgres the MERGE is timed with the edge table vacuumed while empty, as autovacuum leaves it late in a full run: its per-row read is then a Seq Scan, so it is quadratic (#420). A vacuum doesn't always leave statistics that say empty (one run took 2.4 s over 20,000 rows, not 27 s), so the case is an xfail allowed to pass until #420 is fixed. +The CALL…RETURN joins its one-row list to the node table for each row, and Postgres, which takes the list for 100 +rows, may scan every node for it instead (liberation-data/drivine4j#609): 36.8 s over 20,000 rows in one CI run. It +too is an xfail allowed to pass, until #420 is fixed. """ import time @@ -104,6 +107,9 @@ async def test_a_write_over_ten_times_the_rows_takes_about_ten_times_as_long_not if vacuumed_empty(pm, write): reason = "#420: with the edge table vacuumed empty, a compiled MERGE's per-row read is a Seq Scan on Postgres." request.applymarker(pytest.mark.xfail(strict=False, reason=reason)) + if pm.type == DatabaseType.POSTGRES and write.name == "CALL…RETURN": + reason = "#420: a CALL's one-row list joined to the node table may be planned as a Seq Scan on Postgres." + request.applymarker(pytest.mark.xfail(strict=False, reason=reason)) if store not in TIMES: try: TIMES[store] = (await timed(pm, SMALL), await timed(pm, LARGE)) diff --git a/tests/sync_integration/sql/test_compiled_write_loop_time.py b/tests/sync_integration/sql/test_compiled_write_loop_time.py index 0d0a68f..c6504b6 100644 --- a/tests/sync_integration/sql/test_compiled_write_loop_time.py +++ b/tests/sync_integration/sql/test_compiled_write_loop_time.py @@ -12,6 +12,9 @@ On Postgres the MERGE is timed with the edge table vacuumed while empty, as autovacuum leaves it late in a full run: its per-row read is then a Seq Scan, so it is quadratic (#420). A vacuum doesn't always leave statistics that say empty (one run took 2.4 s over 20,000 rows, not 27 s), so the case is an xfail allowed to pass until #420 is fixed. +The CALL…RETURN joins its one-row list to the node table for each row, and Postgres, which takes the list for 100 +rows, may scan every node for it instead (liberation-data/drivine4j#609): 36.8 s over 20,000 rows in one CI run. It +too is an xfail allowed to pass, until #420 is fixed. """ import time @@ -106,6 +109,9 @@ def test_a_write_over_ten_times_the_rows_takes_about_ten_times_as_long_not_a_hun if vacuumed_empty(pm, write): reason = "#420: with the edge table vacuumed empty, a compiled MERGE's per-row read is a Seq Scan on Postgres." request.applymarker(pytest.mark.xfail(strict=False, reason=reason)) + if pm.type == DatabaseType.POSTGRES and write.name == "CALL…RETURN": + reason = "#420: a CALL's one-row list joined to the node table may be planned as a Seq Scan on Postgres." + request.applymarker(pytest.mark.xfail(strict=False, reason=reason)) if store not in TIMES: try: TIMES[store] = (timed(pm, SMALL), timed(pm, LARGE))