From 21ed4569326b5ab56a9d7def99cf77db835705ce Mon Sep 17 00:00:00 2001 From: Ben Karl Date: Mon, 10 Nov 2025 16:11:33 +1300 Subject: [PATCH 1/3] write failing test --- .../geopins/drivers/gdf/filetypes/test_gpkg.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/geopins/drivers/gdf/filetypes/test_gpkg.py b/tests/geopins/drivers/gdf/filetypes/test_gpkg.py index bdf1d31..2a5b13d 100644 --- a/tests/geopins/drivers/gdf/filetypes/test_gpkg.py +++ b/tests/geopins/drivers/gdf/filetypes/test_gpkg.py @@ -1,5 +1,6 @@ from __future__ import annotations +from time import sleep from typing import TYPE_CHECKING import geopandas as gpd @@ -27,3 +28,20 @@ def test_round_trip(tmp_geoboard: GeoBaseBoard): # Assert assert gdf.equals(retrieved) assert gdf.crs == retrieved.crs + + +def test_hash_is_not_dependent_on_file_write_time(tmp_geoboard: GeoBaseBoard): + # Arrange + gdf = gpd.GeoDataFrame( + {"id": [1, 2, 3]}, + geometry=gpd.points_from_xy([0, 1, 2], [0, 1, 2]), + crs="EPSG:2193", # NZGD2000 / New Zealand Transverse Mercator 2000 + ) + + # Act + meta1 = tmp_geoboard.pin_write(gdf, name="test-gdf-hash", type="gpkg") + sleep(1) + meta2 = tmp_geoboard.pin_write(gdf, name="test-gdf-hash", type="gpkg") + + # Assert + assert meta1.pin_hash == meta2.pin_hash From df6470ea2cc1847673405b7d30565fbdbbbf9c5e Mon Sep 17 00:00:00 2001 From: Ben Karl Date: Mon, 10 Nov 2025 16:28:44 +1300 Subject: [PATCH 2/3] freeze last_modified --- src/geopins/drivers/gdf/filetypes/gpkg.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/geopins/drivers/gdf/filetypes/gpkg.py b/src/geopins/drivers/gdf/filetypes/gpkg.py index c35416d..4b16f59 100644 --- a/src/geopins/drivers/gdf/filetypes/gpkg.py +++ b/src/geopins/drivers/gdf/filetypes/gpkg.py @@ -3,6 +3,7 @@ import tempfile import warnings from pathlib import Path +from sqlite3 import connect from typing import TYPE_CHECKING import geopandas as gpd @@ -112,6 +113,9 @@ def pin_write_gdf_gpkg( # noqa: PLR0913 path = Path(tmpdir_path) / f"{name}.gpkg" x.to_file(path, driver="GPKG") + # Overwrite the modification time to keep hashing stable and release locks. + _snapshot_last_change(path=path) + with warnings.catch_warnings(): # Upstream issue relating to opening files without context managers warnings.simplefilter("ignore", category=ResourceWarning) @@ -123,3 +127,19 @@ def pin_write_gdf_gpkg( # noqa: PLR0913 description=description, metadata=metadata, ) + + +def _snapshot_last_change(path: Path) -> None: + """Set the last_change timestamp to Unix epoch to keep GeoPackage hashing stable.""" + + conn = connect(path.as_posix()) + try: + conn.execute( + """ + UPDATE gpkg_contents + SET last_change = '1970-01-01T00:00:00Z'; + """ + ) + conn.commit() + finally: + conn.close() From be6de49bf65c9f99f682b98d66fa68eee36d66cf Mon Sep 17 00:00:00 2001 From: Ben Karl Date: Mon, 10 Nov 2025 16:38:05 +1300 Subject: [PATCH 3/3] add comment explaining not using a context manager --- src/geopins/drivers/gdf/filetypes/gpkg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/geopins/drivers/gdf/filetypes/gpkg.py b/src/geopins/drivers/gdf/filetypes/gpkg.py index 4b16f59..5ef9d55 100644 --- a/src/geopins/drivers/gdf/filetypes/gpkg.py +++ b/src/geopins/drivers/gdf/filetypes/gpkg.py @@ -132,6 +132,8 @@ def pin_write_gdf_gpkg( # noqa: PLR0913 def _snapshot_last_change(path: Path) -> None: """Set the last_change timestamp to Unix epoch to keep GeoPackage hashing stable.""" + # Avoid `with connect(...)` because the context manager delays handle release on + # Windows, which keeps the temporary GeoPackage locked during cleanup. conn = connect(path.as_posix()) try: conn.execute(