Skip to content

Commit db8c85b

Browse files
fix(changeset): long standing changeset fix
1 parent 07c34f9 commit db8c85b

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

osmsg/export/psql.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ def to_psql(conn: duckdb.DuckDBPyConnection, dsn: str, *, bulk_load: bool = Fals
223223
not_history = "changeset_id NOT IN (SELECT changeset_id FROM changeset_stats WHERE seq_id = 0)"
224224
conn.execute(
225225
"INSERT INTO pg_target.users SELECT * FROM users "
226-
"WHERE uid IN (SELECT uid FROM changeset_stats WHERE seq_id <> 0) ON CONFLICT DO NOTHING"
226+
"WHERE uid IN (SELECT uid FROM changeset_stats WHERE seq_id <> 0) "
227+
"OR uid IN (SELECT uid FROM changesets) ON CONFLICT DO NOTHING"
227228
)
228229
# Chunked like the bulk path: one INSERT of the whole live tail exceeds the worker's memory
229230
# once the tail grows to a month; per-range commits keep peak memory to a single chunk.

tests/test_psql_export.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,64 @@ def test_push_chunked_scales_with_row_count_and_caps():
598598

599599
def test_push_chunked_empty_source_pushes_nothing():
600600
assert _count_pushes(0, None, None) == []
601+
602+
603+
@pytest.mark.network
604+
@pytest.mark.skipif(not os.environ.get("OSMSG_PG_DSN"), reason="OSMSG_PG_DSN not set; live PG push not exercised")
605+
def test_metadata_only_changeset_fills_stub_without_fk_abort(fresh_db):
606+
"""A long-open changeset's metadata arrives in a later, no-edit delta (its uid absent from this
607+
delta's changeset_stats). With history present the push takes the not_history branch, which must
608+
still carry that user, or the changesets FK aborts the whole push and the stub is stranded forever."""
609+
dsn = os.environ["OSMSG_PG_DSN"]
610+
safe_dsn = dsn.replace("'", "''")
611+
612+
fresh_db.execute("INSTALL postgres")
613+
fresh_db.execute("LOAD postgres")
614+
fresh_db.execute(f"ATTACH '{safe_dsn}' AS pg_w (TYPE postgres)")
615+
try:
616+
for stmt in (s.strip() for s in PG_SCHEMA.strip().split(";")):
617+
if stmt:
618+
fresh_db.execute(f"CALL postgres_execute('pg_w', $${stmt}$$)")
619+
for table in ("changeset_hashtag", "changeset_stats", "changesets", "users", "state"):
620+
fresh_db.execute(f"CALL postgres_execute('pg_w', $$DELETE FROM {table}$$)")
621+
# A seq_id=0 history row makes _pg_has_history() true -> the not_history branch runs.
622+
fresh_db.execute("CALL postgres_execute('pg_w', $$INSERT INTO users VALUES (1, 'hist')$$)")
623+
fresh_db.execute("CALL postgres_execute('pg_w', $$INSERT INTO changesets (changeset_id, uid) VALUES (1, 1)$$)")
624+
fresh_db.execute(
625+
"CALL postgres_execute('pg_w', $$INSERT INTO changeset_stats "
626+
"(changeset_id, seq_id, uid, nodes_created, nodes_modified, nodes_deleted, ways_created, "
627+
"ways_modified, ways_deleted, rels_created, rels_modified, rels_deleted, poi_created, poi_modified) "
628+
"VALUES (1, 0, 1, 0,0,0,0,0,0,0,0,0,0,0)$$)"
629+
)
630+
finally:
631+
fresh_db.execute("DETACH pg_w")
632+
633+
# Delta buffer: changeset 500 is metadata-only (uid 100, full metadata, NO stats this tick);
634+
# changeset 600 has live edits (uid 200). uid 100 is absent from this delta's changeset_stats.
635+
fresh_db.execute("INSERT INTO users VALUES (100, 'stubuser'), (200, 'liveuser')")
636+
fresh_db.execute(
637+
"INSERT INTO changesets VALUES "
638+
"(500, 100, '2026-08-04 02:01:51+00', ['#msf'], 'iD', NULL), "
639+
"(600, 200, '2026-08-04 03:00:00+00', ['#msf'], 'iD', NULL)"
640+
)
641+
fresh_db.execute("INSERT INTO changeset_stats VALUES (600, 7228931, 200, 10,0,0,2,0,0,0,0,0,0,0, NULL)")
642+
643+
to_psql(fresh_db, dsn) # must not raise the changesets_uid_fkey violation
644+
645+
verifier = duckdb.connect(":memory:")
646+
verifier.execute("INSTALL postgres")
647+
verifier.execute("LOAD postgres")
648+
verifier.execute(f"ATTACH '{safe_dsn}' AS pg_r (TYPE postgres, READ_ONLY)")
649+
try:
650+
landed = verifier.execute("SELECT created_at FROM pg_r.changesets WHERE changeset_id = 500").fetchone()
651+
user_ok = verifier.execute("SELECT count(*) FROM pg_r.users WHERE uid = 100").fetchone()[0]
652+
hashtag_ok = verifier.execute(
653+
"SELECT count(*) FROM pg_r.changeset_hashtag WHERE changeset_id = 500 AND hashtag = '#msf'"
654+
).fetchone()[0]
655+
finally:
656+
verifier.execute("DETACH pg_r")
657+
verifier.close()
658+
659+
assert landed is not None and landed[0] is not None # metadata-only changeset reached PG
660+
assert user_ok == 1 # its user was carried even without an edit row this tick
661+
assert hashtag_ok == 1 # and its #msf hashtag reached the index (the stub is no longer stranded)

0 commit comments

Comments
 (0)