From 6677497cf64d17c9a8317790a0c1e092eb4309ff Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 5 May 2026 17:00:59 -0500 Subject: [PATCH 1/8] feat: add pgstac-migrate compatibility layer --- .../instructions/migrations.instructions.md | 4 +- .github/instructions/pypgstac.instructions.md | 3 + .gitignore | 2 + docker/pypgstac/Dockerfile | 32 +-- scripts/container-scripts/test | 35 ++-- src/pgstac-migrate/README.md | 22 ++ src/pgstac-migrate/pyproject.toml | 20 ++ src/pgstac-migrate/scripts/build_artifact.py | 29 +++ .../src/pgstac_migrate/__init__.py | 3 + src/pgstac-migrate/src/pgstac_migrate/api.py | 51 +++++ .../src/pgstac_migrate/build.py | 45 ++++ src/pgstac-migrate/src/pgstac_migrate/cli.py | 160 ++++++++++++++ .../src/pgstac_migrate/compat.py | 100 +++++++++ .../src/pgstac_migrate/version_source.py | 77 +++++++ src/pgstac-migrate/tests/test_api.py | 44 ++++ src/pgstac-migrate/tests/test_cli.py | 57 +++++ .../tests/test_version_source.py | 101 +++++++++ src/pgstac-migrate/uv.lock | 198 ++++++++++++++++++ ...1.9-0.2.3.sql => pgstac--0.1.9--0.2.3.sql} | 0 .../{pgstac.0.1.9.sql => pgstac--0.1.9.sql} | 0 ...2.3-0.2.4.sql => pgstac--0.2.3--0.2.4.sql} | 0 .../{pgstac.0.2.3.sql => pgstac--0.2.3.sql} | 0 ...2.4-0.2.5.sql => pgstac--0.2.4--0.2.5.sql} | 0 ...2.4-0.2.7.sql => pgstac--0.2.4--0.2.7.sql} | 0 .../{pgstac.0.2.4.sql => pgstac--0.2.4.sql} | 0 ...2.5-0.2.7.sql => pgstac--0.2.5--0.2.7.sql} | 0 .../{pgstac.0.2.5.sql => pgstac--0.2.5.sql} | 0 ...2.7-0.2.8.sql => pgstac--0.2.7--0.2.8.sql} | 0 .../{pgstac.0.2.7.sql => pgstac--0.2.7.sql} | 0 ...2.8-0.2.9.sql => pgstac--0.2.8--0.2.9.sql} | 0 .../{pgstac.0.2.8.sql => pgstac--0.2.8.sql} | 0 ...2.9-0.3.0.sql => pgstac--0.2.9--0.3.0.sql} | 0 .../{pgstac.0.2.9.sql => pgstac--0.2.9.sql} | 0 ...3.0-0.3.1.sql => pgstac--0.3.0--0.3.1.sql} | 0 .../{pgstac.0.3.0.sql => pgstac--0.3.0.sql} | 0 ...3.1-0.3.2.sql => pgstac--0.3.1--0.3.2.sql} | 0 .../{pgstac.0.3.1.sql => pgstac--0.3.1.sql} | 0 ...3.2-0.3.3.sql => pgstac--0.3.2--0.3.3.sql} | 0 .../{pgstac.0.3.2.sql => pgstac--0.3.2.sql} | 0 ...3.3-0.3.4.sql => pgstac--0.3.3--0.3.4.sql} | 0 .../{pgstac.0.3.3.sql => pgstac--0.3.3.sql} | 0 ...3.4-0.3.5.sql => pgstac--0.3.4--0.3.5.sql} | 0 .../{pgstac.0.3.4.sql => pgstac--0.3.4.sql} | 0 ...3.5-0.3.6.sql => pgstac--0.3.5--0.3.6.sql} | 0 .../{pgstac.0.3.5.sql => pgstac--0.3.5.sql} | 0 ...3.6-0.4.0.sql => pgstac--0.3.6--0.4.0.sql} | 0 .../{pgstac.0.3.6.sql => pgstac--0.3.6.sql} | 0 ...4.0-0.4.1.sql => pgstac--0.4.0--0.4.1.sql} | 0 .../{pgstac.0.4.0.sql => pgstac--0.4.0.sql} | 0 ...4.1-0.4.2.sql => pgstac--0.4.1--0.4.2.sql} | 0 .../{pgstac.0.4.1.sql => pgstac--0.4.1.sql} | 0 ...4.2-0.4.3.sql => pgstac--0.4.2--0.4.3.sql} | 0 .../{pgstac.0.4.2.sql => pgstac--0.4.2.sql} | 0 ...4.3-0.4.4.sql => pgstac--0.4.3--0.4.4.sql} | 0 .../{pgstac.0.4.3.sql => pgstac--0.4.3.sql} | 0 ...4.4-0.4.5.sql => pgstac--0.4.4--0.4.5.sql} | 0 .../{pgstac.0.4.4.sql => pgstac--0.4.4.sql} | 0 ...4.5-0.5.0.sql => pgstac--0.4.5--0.5.0.sql} | 0 .../{pgstac.0.4.5.sql => pgstac--0.4.5.sql} | 0 ...5.0-0.5.1.sql => pgstac--0.5.0--0.5.1.sql} | 0 .../{pgstac.0.5.0.sql => pgstac--0.5.0.sql} | 0 ...5.1-0.6.0.sql => pgstac--0.5.1--0.6.0.sql} | 0 .../{pgstac.0.5.1.sql => pgstac--0.5.1.sql} | 0 ...6.0-0.6.1.sql => pgstac--0.6.0--0.6.1.sql} | 0 .../{pgstac.0.6.0.sql => pgstac--0.6.0.sql} | 0 ...6.1-0.6.2.sql => pgstac--0.6.1--0.6.2.sql} | 0 .../{pgstac.0.6.1.sql => pgstac--0.6.1.sql} | 0 ...-0.6.11.sql => pgstac--0.6.10--0.6.11.sql} | 0 .../{pgstac.0.6.10.sql => pgstac--0.6.10.sql} | 0 ...-0.6.12.sql => pgstac--0.6.11--0.6.12.sql} | 0 .../{pgstac.0.6.11.sql => pgstac--0.6.11.sql} | 0 ...-0.6.13.sql => pgstac--0.6.12--0.6.13.sql} | 0 .../{pgstac.0.6.12.sql => pgstac--0.6.12.sql} | 0 ...13-0.7.0.sql => pgstac--0.6.13--0.7.0.sql} | 0 ...13-0.7.3.sql => pgstac--0.6.13--0.7.3.sql} | 0 .../{pgstac.0.6.13.sql => pgstac--0.6.13.sql} | 0 ...6.2-0.6.3.sql => pgstac--0.6.2--0.6.3.sql} | 0 .../{pgstac.0.6.2.sql => pgstac--0.6.2.sql} | 0 ...6.3-0.6.4.sql => pgstac--0.6.3--0.6.4.sql} | 0 .../{pgstac.0.6.3.sql => pgstac--0.6.3.sql} | 0 ...6.4-0.6.5.sql => pgstac--0.6.4--0.6.5.sql} | 0 .../{pgstac.0.6.4.sql => pgstac--0.6.4.sql} | 0 ...6.5-0.6.6.sql => pgstac--0.6.5--0.6.6.sql} | 0 .../{pgstac.0.6.5.sql => pgstac--0.6.5.sql} | 0 ...6.6-0.6.7.sql => pgstac--0.6.6--0.6.7.sql} | 0 .../{pgstac.0.6.6.sql => pgstac--0.6.6.sql} | 0 ...6.7-0.6.8.sql => pgstac--0.6.7--0.6.8.sql} | 0 .../{pgstac.0.6.7.sql => pgstac--0.6.7.sql} | 0 ...6.8-0.6.9.sql => pgstac--0.6.8--0.6.9.sql} | 0 .../{pgstac.0.6.8.sql => pgstac--0.6.8.sql} | 0 ...9-0.6.10.sql => pgstac--0.6.9--0.6.10.sql} | 0 .../{pgstac.0.6.9.sql => pgstac--0.6.9.sql} | 0 ...7.0-0.7.1.sql => pgstac--0.7.0--0.7.1.sql} | 0 .../{pgstac.0.7.0.sql => pgstac--0.7.0.sql} | 0 ...7.1-0.7.2.sql => pgstac--0.7.1--0.7.2.sql} | 0 .../{pgstac.0.7.1.sql => pgstac--0.7.1.sql} | 0 ...10-0.8.0.sql => pgstac--0.7.10--0.8.0.sql} | 0 .../{pgstac.0.7.10.sql => pgstac--0.7.10.sql} | 0 ...7.2-0.7.3.sql => pgstac--0.7.2--0.7.3.sql} | 0 .../{pgstac.0.7.2.sql => pgstac--0.7.2.sql} | 0 ...7.3-0.7.4.sql => pgstac--0.7.3--0.7.4.sql} | 0 .../{pgstac.0.7.3.sql => pgstac--0.7.3.sql} | 0 ...7.4-0.7.5.sql => pgstac--0.7.4--0.7.5.sql} | 0 .../{pgstac.0.7.4.sql => pgstac--0.7.4.sql} | 0 ...7.5-0.7.6.sql => pgstac--0.7.5--0.7.6.sql} | 0 .../{pgstac.0.7.5.sql => pgstac--0.7.5.sql} | 0 ...7.6-0.7.7.sql => pgstac--0.7.6--0.7.7.sql} | 0 .../{pgstac.0.7.6.sql => pgstac--0.7.6.sql} | 0 ...7.7-0.7.8.sql => pgstac--0.7.7--0.7.8.sql} | 0 .../{pgstac.0.7.7.sql => pgstac--0.7.7.sql} | 0 ...7.8-0.7.9.sql => pgstac--0.7.8--0.7.9.sql} | 0 .../{pgstac.0.7.8.sql => pgstac--0.7.8.sql} | 0 ...9-0.7.10.sql => pgstac--0.7.9--0.7.10.sql} | 0 .../{pgstac.0.7.9.sql => pgstac--0.7.9.sql} | 0 ...8.0-0.8.1.sql => pgstac--0.8.0--0.8.1.sql} | 0 .../{pgstac.0.8.0.sql => pgstac--0.8.0.sql} | 0 ...8.1-0.8.2.sql => pgstac--0.8.1--0.8.2.sql} | 0 .../{pgstac.0.8.1.sql => pgstac--0.8.1.sql} | 0 ...8.2-0.8.3.sql => pgstac--0.8.2--0.8.3.sql} | 0 .../{pgstac.0.8.2.sql => pgstac--0.8.2.sql} | 0 ...8.3-0.8.4.sql => pgstac--0.8.3--0.8.4.sql} | 0 .../{pgstac.0.8.3.sql => pgstac--0.8.3.sql} | 0 ...8.4-0.8.5.sql => pgstac--0.8.4--0.8.5.sql} | 0 .../{pgstac.0.8.4.sql => pgstac--0.8.4.sql} | 0 ...8.5-0.9.0.sql => pgstac--0.8.5--0.9.0.sql} | 0 .../{pgstac.0.8.5.sql => pgstac--0.8.5.sql} | 0 ...8.6-0.9.0.sql => pgstac--0.8.6--0.9.0.sql} | 0 ...6-0.9.10.sql => pgstac--0.8.6--0.9.10.sql} | 0 .../{pgstac.0.8.6.sql => pgstac--0.8.6.sql} | 0 ...9.0-0.9.1.sql => pgstac--0.9.0--0.9.1.sql} | 0 .../{pgstac.0.9.0.sql => pgstac--0.9.0.sql} | 0 ...9.1-0.9.2.sql => pgstac--0.9.1--0.9.2.sql} | 0 .../{pgstac.0.9.1.sql => pgstac--0.9.1.sql} | 0 ...-0.9.11.sql => pgstac--0.9.10--0.9.11.sql} | 0 .../{pgstac.0.9.10.sql => pgstac--0.9.10.sql} | 0 ...sed.sql => pgstac--0.9.11--unreleased.sql} | 0 .../{pgstac.0.9.11.sql => pgstac--0.9.11.sql} | 0 ...9.2-0.9.3.sql => pgstac--0.9.2--0.9.3.sql} | 0 .../{pgstac.0.9.2.sql => pgstac--0.9.2.sql} | 0 ...9.3-0.9.4.sql => pgstac--0.9.3--0.9.4.sql} | 0 .../{pgstac.0.9.3.sql => pgstac--0.9.3.sql} | 0 ...9.4-0.9.5.sql => pgstac--0.9.4--0.9.5.sql} | 0 .../{pgstac.0.9.4.sql => pgstac--0.9.4.sql} | 0 ...9.5-0.9.6.sql => pgstac--0.9.5--0.9.6.sql} | 0 .../{pgstac.0.9.5.sql => pgstac--0.9.5.sql} | 0 ...9.6-0.9.7.sql => pgstac--0.9.6--0.9.7.sql} | 0 .../{pgstac.0.9.6.sql => pgstac--0.9.6.sql} | 0 ...9.7-0.9.8.sql => pgstac--0.9.7--0.9.8.sql} | 0 .../{pgstac.0.9.7.sql => pgstac--0.9.7.sql} | 0 ...9.8-0.9.9.sql => pgstac--0.9.8--0.9.9.sql} | 0 .../{pgstac.0.9.8.sql => pgstac--0.9.8.sql} | 0 ...9-0.9.10.sql => pgstac--0.9.9--0.9.10.sql} | 0 .../{pgstac.0.9.9.sql => pgstac--0.9.9.sql} | 0 ....unreleased.sql => pgstac--unreleased.sql} | 0 src/pgstac/pyproject.toml | 6 + src/pypgstac/pyproject.toml | 6 +- src/pypgstac/src/pypgstac/migrate.py | 126 ++++------- src/pypgstac/tests/test_migrate.py | 39 ++++ src/pypgstac/tests/test_migrate_wrapper.py | 60 ++++++ 159 files changed, 1086 insertions(+), 134 deletions(-) create mode 100644 src/pgstac-migrate/README.md create mode 100644 src/pgstac-migrate/pyproject.toml create mode 100644 src/pgstac-migrate/scripts/build_artifact.py create mode 100644 src/pgstac-migrate/src/pgstac_migrate/__init__.py create mode 100644 src/pgstac-migrate/src/pgstac_migrate/api.py create mode 100644 src/pgstac-migrate/src/pgstac_migrate/build.py create mode 100644 src/pgstac-migrate/src/pgstac_migrate/cli.py create mode 100644 src/pgstac-migrate/src/pgstac_migrate/compat.py create mode 100644 src/pgstac-migrate/src/pgstac_migrate/version_source.py create mode 100644 src/pgstac-migrate/tests/test_api.py create mode 100644 src/pgstac-migrate/tests/test_cli.py create mode 100644 src/pgstac-migrate/tests/test_version_source.py create mode 100644 src/pgstac-migrate/uv.lock rename src/pgstac/migrations/{pgstac.0.1.9-0.2.3.sql => pgstac--0.1.9--0.2.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.1.9.sql => pgstac--0.1.9.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.3-0.2.4.sql => pgstac--0.2.3--0.2.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.3.sql => pgstac--0.2.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.4-0.2.5.sql => pgstac--0.2.4--0.2.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.4-0.2.7.sql => pgstac--0.2.4--0.2.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.4.sql => pgstac--0.2.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.5-0.2.7.sql => pgstac--0.2.5--0.2.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.5.sql => pgstac--0.2.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.7-0.2.8.sql => pgstac--0.2.7--0.2.8.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.7.sql => pgstac--0.2.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.8-0.2.9.sql => pgstac--0.2.8--0.2.9.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.8.sql => pgstac--0.2.8.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.9-0.3.0.sql => pgstac--0.2.9--0.3.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.2.9.sql => pgstac--0.2.9.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.0-0.3.1.sql => pgstac--0.3.0--0.3.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.0.sql => pgstac--0.3.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.1-0.3.2.sql => pgstac--0.3.1--0.3.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.1.sql => pgstac--0.3.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.2-0.3.3.sql => pgstac--0.3.2--0.3.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.2.sql => pgstac--0.3.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.3-0.3.4.sql => pgstac--0.3.3--0.3.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.3.sql => pgstac--0.3.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.4-0.3.5.sql => pgstac--0.3.4--0.3.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.4.sql => pgstac--0.3.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.5-0.3.6.sql => pgstac--0.3.5--0.3.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.5.sql => pgstac--0.3.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.6-0.4.0.sql => pgstac--0.3.6--0.4.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.3.6.sql => pgstac--0.3.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.0-0.4.1.sql => pgstac--0.4.0--0.4.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.0.sql => pgstac--0.4.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.1-0.4.2.sql => pgstac--0.4.1--0.4.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.1.sql => pgstac--0.4.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.2-0.4.3.sql => pgstac--0.4.2--0.4.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.2.sql => pgstac--0.4.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.3-0.4.4.sql => pgstac--0.4.3--0.4.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.3.sql => pgstac--0.4.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.4-0.4.5.sql => pgstac--0.4.4--0.4.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.4.sql => pgstac--0.4.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.5-0.5.0.sql => pgstac--0.4.5--0.5.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.4.5.sql => pgstac--0.4.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.5.0-0.5.1.sql => pgstac--0.5.0--0.5.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.5.0.sql => pgstac--0.5.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.5.1-0.6.0.sql => pgstac--0.5.1--0.6.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.5.1.sql => pgstac--0.5.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.0-0.6.1.sql => pgstac--0.6.0--0.6.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.0.sql => pgstac--0.6.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.1-0.6.2.sql => pgstac--0.6.1--0.6.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.1.sql => pgstac--0.6.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.10-0.6.11.sql => pgstac--0.6.10--0.6.11.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.10.sql => pgstac--0.6.10.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.11-0.6.12.sql => pgstac--0.6.11--0.6.12.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.11.sql => pgstac--0.6.11.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.12-0.6.13.sql => pgstac--0.6.12--0.6.13.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.12.sql => pgstac--0.6.12.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.13-0.7.0.sql => pgstac--0.6.13--0.7.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.13-0.7.3.sql => pgstac--0.6.13--0.7.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.13.sql => pgstac--0.6.13.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.2-0.6.3.sql => pgstac--0.6.2--0.6.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.2.sql => pgstac--0.6.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.3-0.6.4.sql => pgstac--0.6.3--0.6.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.3.sql => pgstac--0.6.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.4-0.6.5.sql => pgstac--0.6.4--0.6.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.4.sql => pgstac--0.6.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.5-0.6.6.sql => pgstac--0.6.5--0.6.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.5.sql => pgstac--0.6.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.6-0.6.7.sql => pgstac--0.6.6--0.6.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.6.sql => pgstac--0.6.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.7-0.6.8.sql => pgstac--0.6.7--0.6.8.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.7.sql => pgstac--0.6.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.8-0.6.9.sql => pgstac--0.6.8--0.6.9.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.8.sql => pgstac--0.6.8.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.9-0.6.10.sql => pgstac--0.6.9--0.6.10.sql} (100%) rename src/pgstac/migrations/{pgstac.0.6.9.sql => pgstac--0.6.9.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.0-0.7.1.sql => pgstac--0.7.0--0.7.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.0.sql => pgstac--0.7.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.1-0.7.2.sql => pgstac--0.7.1--0.7.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.1.sql => pgstac--0.7.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.10-0.8.0.sql => pgstac--0.7.10--0.8.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.10.sql => pgstac--0.7.10.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.2-0.7.3.sql => pgstac--0.7.2--0.7.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.2.sql => pgstac--0.7.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.3-0.7.4.sql => pgstac--0.7.3--0.7.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.3.sql => pgstac--0.7.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.4-0.7.5.sql => pgstac--0.7.4--0.7.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.4.sql => pgstac--0.7.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.5-0.7.6.sql => pgstac--0.7.5--0.7.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.5.sql => pgstac--0.7.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.6-0.7.7.sql => pgstac--0.7.6--0.7.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.6.sql => pgstac--0.7.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.7-0.7.8.sql => pgstac--0.7.7--0.7.8.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.7.sql => pgstac--0.7.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.8-0.7.9.sql => pgstac--0.7.8--0.7.9.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.8.sql => pgstac--0.7.8.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.9-0.7.10.sql => pgstac--0.7.9--0.7.10.sql} (100%) rename src/pgstac/migrations/{pgstac.0.7.9.sql => pgstac--0.7.9.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.0-0.8.1.sql => pgstac--0.8.0--0.8.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.0.sql => pgstac--0.8.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.1-0.8.2.sql => pgstac--0.8.1--0.8.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.1.sql => pgstac--0.8.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.2-0.8.3.sql => pgstac--0.8.2--0.8.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.2.sql => pgstac--0.8.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.3-0.8.4.sql => pgstac--0.8.3--0.8.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.3.sql => pgstac--0.8.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.4-0.8.5.sql => pgstac--0.8.4--0.8.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.4.sql => pgstac--0.8.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.5-0.9.0.sql => pgstac--0.8.5--0.9.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.5.sql => pgstac--0.8.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.6-0.9.0.sql => pgstac--0.8.6--0.9.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.6-0.9.10.sql => pgstac--0.8.6--0.9.10.sql} (100%) rename src/pgstac/migrations/{pgstac.0.8.6.sql => pgstac--0.8.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.0-0.9.1.sql => pgstac--0.9.0--0.9.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.0.sql => pgstac--0.9.0.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.1-0.9.2.sql => pgstac--0.9.1--0.9.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.1.sql => pgstac--0.9.1.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.10-0.9.11.sql => pgstac--0.9.10--0.9.11.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.10.sql => pgstac--0.9.10.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.11-unreleased.sql => pgstac--0.9.11--unreleased.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.11.sql => pgstac--0.9.11.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.2-0.9.3.sql => pgstac--0.9.2--0.9.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.2.sql => pgstac--0.9.2.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.3-0.9.4.sql => pgstac--0.9.3--0.9.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.3.sql => pgstac--0.9.3.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.4-0.9.5.sql => pgstac--0.9.4--0.9.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.4.sql => pgstac--0.9.4.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.5-0.9.6.sql => pgstac--0.9.5--0.9.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.5.sql => pgstac--0.9.5.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.6-0.9.7.sql => pgstac--0.9.6--0.9.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.6.sql => pgstac--0.9.6.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.7-0.9.8.sql => pgstac--0.9.7--0.9.8.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.7.sql => pgstac--0.9.7.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.8-0.9.9.sql => pgstac--0.9.8--0.9.9.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.8.sql => pgstac--0.9.8.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.9-0.9.10.sql => pgstac--0.9.9--0.9.10.sql} (100%) rename src/pgstac/migrations/{pgstac.0.9.9.sql => pgstac--0.9.9.sql} (100%) rename src/pgstac/migrations/{pgstac.unreleased.sql => pgstac--unreleased.sql} (100%) create mode 100644 src/pgstac/pyproject.toml create mode 100644 src/pypgstac/tests/test_migrate.py create mode 100644 src/pypgstac/tests/test_migrate_wrapper.py diff --git a/.github/instructions/migrations.instructions.md b/.github/instructions/migrations.instructions.md index 008b0684..1052ece1 100644 --- a/.github/instructions/migrations.instructions.md +++ b/.github/instructions/migrations.instructions.md @@ -7,7 +7,7 @@ applyTo: "src/pgstac/migrations/**" These files are **generated** — see CLAUDE.md "Migration Process" for the full workflow. - **DO NOT** create, edit, or hand-modify migration files -- Base (`pgstac.X.Y.Z.sql`) = full schema at that version -- Incremental (`pgstac.X.Y.Z-A.B.C.sql`) = upgrade diff +- Base (`pgstac--X.Y.Z.sql`) = full schema at that version +- Incremental (`pgstac--X.Y.Z--A.B.C.sql`) = upgrade diff - Staged (`*.sql.staged`) = needs review before removing `.staged` suffix - Test: `scripts/test --migrations` diff --git a/.github/instructions/pypgstac.instructions.md b/.github/instructions/pypgstac.instructions.md index 725e2695..0e2741eb 100644 --- a/.github/instructions/pypgstac.instructions.md +++ b/.github/instructions/pypgstac.instructions.md @@ -7,6 +7,9 @@ applyTo: "src/pypgstac/**" See CLAUDE.md "pypgstac Loader Internals" for patterns. See AGENTS.md "loader-developer" for critical rules. - Uses psycopg v3 (not psycopg2), orjson (not json), tenacity, plpygis, fire +- `pypgstac migrate` is a thin wrapper over `pgstac-migrate`; put new migration runtime behavior in `src/pgstac-migrate/`, not `src/pypgstac/src/pypgstac/migrate.py` +- `src/pypgstac/pyproject.toml` keeps a local `[tool.uv.sources]` override for `pgstac-migrate`, while `pgpkg` resolves from PyPI +- In Docker-backed dev runs, `scripts/runinpypgstac` can mount a local `pgpkg` checkout at `/pgpkg` and export `PGPKG_REPO_DIR` so container scripts can force that checkout when needed - Materialize generators before retry boundaries - Query `partition_sys_meta` (live VIEW), never `partitions` (stale MATERIALIZED VIEW) - Test: `scripts/runinpypgstac --build test --pypgstac` diff --git a/.gitignore b/.gitignore index c57bfb98..a60912e1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ src/pypgstac/python/pypgstac/*.so .plans/ .env src/pgstacrust/target/ +src/pgstac-migrate/dist/ +src/pgstac-migrate/src/pgstac_migrate/migrations.tar.zst diff --git a/docker/pypgstac/Dockerfile b/docker/pypgstac/Dockerfile index aee9873b..5c0ff71c 100644 --- a/docker/pypgstac/Dockerfile +++ b/docker/pypgstac/Dockerfile @@ -4,7 +4,7 @@ ENV PYTHONWRITEBYTECODE=1 ENV PYTHONBUFFERED=1 ENV PIP_ROOT_USER_ACTION=ignore ENV UV_BREAK_SYSTEM_PACKAGES=1 -ENV PYTHONPATH="/opt/src/pypgstac:$PYTHONPATH" +ENV PYTHONPATH="/opt/src/pypgstac:/opt/src/pgstac-migrate:$PYTHONPATH" ENV PATH="/opt/pgstac/container-scripts:$PATH" ENV UV_CACHE_DIR=/root/.cache/uv ARG PG_MAJOR=17 @@ -24,27 +24,18 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ && rm -rf /var/lib/apt/lists/* FROM pyrustbase AS pypgstac -COPY ./src/pypgstac/pyproject.toml /tmp/pyproject.toml -WORKDIR /tmp -RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ - uv pip compile /tmp/pyproject.toml \ - --extra dev \ - --extra test \ - --extra psycopg \ - --extra migrations \ - >/tmp/requirements.txt \ - && uv pip install --system -r /tmp/requirements.txt +ENV UV_CACHE_DIR=/home/user/.cache/uv COPY scripts/container-scripts /opt/pgstac/container-scripts COPY src/pypgstac /opt/src/pypgstac COPY src/pgstac /opt/src/pgstac +COPY src/pgstac-migrate /opt/src/pgstac-migrate WORKDIR /opt/src/pypgstac -RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ - uv pip install --system -e . \ - && rm -rf /usr/local/cargo/registry +RUN rm -rf /usr/local/cargo/registry RUN addgroup --gid 1000 user && \ adduser --uid 1000 --gid 1000 --disabled-password --gecos "" --home /home/user user && \ - chown -R user:user /opt/src/pypgstac /opt/src/pgstac + mkdir -p /home/user/.cache/uv && \ + chown -R user:user /home/user /opt/src/pypgstac /opt/src/pgstac /opt/src/pgstac-migrate USER user # Optional runtime-optimized image: no build toolchain, only pypgstac package + runtime deps. @@ -52,6 +43,7 @@ FROM python:3.13-slim-trixie AS pypgstac-runtime ENV PYTHONWRITEBYTECODE=1 ENV PYTHONBUFFERED=1 ENV UV_BREAK_SYSTEM_PACKAGES=1 +ENV PYTHONPATH="/opt/src/pypgstac:/opt/src/pgstac-migrate:$PYTHONPATH" ENV PATH="/opt/pgstac/container-scripts:$PATH" ENV UV_CACHE_DIR=/root/.cache/uv ARG PG_MAJOR=17 @@ -65,16 +57,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ postgresql-client-${PG_MAJOR} \ && curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh \ && apt-get clean && rm -rf /var/lib/apt/lists/* -COPY ./src/pypgstac/pyproject.toml /tmp/pyproject.toml -RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ - uv pip compile /tmp/pyproject.toml \ - --extra psycopg \ - --extra migrations \ - >/tmp/requirements.txt \ - && uv pip install --system -r /tmp/requirements.txt COPY scripts/container-scripts /opt/pgstac/container-scripts COPY src/pypgstac /opt/src/pypgstac COPY src/pgstac /opt/src/pgstac +COPY src/pgstac-migrate /opt/src/pgstac-migrate WORKDIR /opt/src/pypgstac -RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ - uv pip install --system . diff --git a/scripts/container-scripts/test b/scripts/container-scripts/test index 8bb5a416..6d2e03c6 100755 --- a/scripts/container-scripts/test +++ b/scripts/container-scripts/test @@ -73,24 +73,28 @@ function test_formatting(){ cd $SRCDIR/pypgstac echo "Running ruff" - ruff check src/pypgstac tests - ruff format --check src/pypgstac tests + uv run --extra dev ruff check src/pypgstac tests + uv run --extra dev ruff format --check src/pypgstac tests echo "Running ty" - ty check + uv run --extra dev --extra test --extra psycopg ty check echo "Checking if there are any staged migrations." - find $SRCDIR/pgstac/migrations | grep 'staged' && { echo "There are staged migrations in pypgstac/migrations. Please check migrations and remove staged suffix."; exit 1; } + find $SRCDIR/pgstac/migrations | grep 'staged' && { echo "There are staged migrations in pgstac/migrations. Please check migrations and remove the staged suffix."; exit 1; } + echo "Checking for legacy dotted migration filenames." + find $SRCDIR/pgstac/migrations -maxdepth 1 -type f -name 'pgstac.*.sql' | grep . && { echo "Legacy dotted migration filenames remain in src/pgstac/migrations."; exit 1; } + find $SRCDIR/pypgstac/src/pypgstac/migrations -maxdepth 1 -type f -name 'pgstac.*.sql' | grep . && { echo "Legacy dotted migration filenames remain in src/pypgstac/src/pypgstac/migrations."; exit 1; } - VERSION=$(python -c "from pypgstac.version import __version__; print(__version__)") + + VERSION=$(cd $SRCDIR/pypgstac && uv run python -c "from pypgstac.version import __version__; print(__version__)") echo $VERSION if echo $VERSION | grep "dev"; then VERSION="unreleased" fi echo "Checking whether base sql migration exists for pypgstac version." - [ -f $SRCDIR/pgstac/migrations/pgstac."${VERSION}".sql ] || { echo "****FAIL No Migration exists pypgstac/migrations/pgstac.${VERSION}.sql"; exit 1; } + [ -f $SRCDIR/pgstac/migrations/pgstac--"${VERSION}".sql ] || { echo "****FAIL No migration exists at pgstac/migrations/pgstac--${VERSION}.sql"; exit 1; } echo "Congratulations! All formatting tests pass." } @@ -162,16 +166,12 @@ function test_pypgstac(){ [[ $MESSAGELOG == 1 ]] && VERBOSE="-vvv" TEMPLATEDB=${1:-pgstac_test_db_template} cd $SRCDIR/pypgstac - python -m venv venv - source venv/bin/activate - pip install --cache /tmp/.pipcache --upgrade pip - pip install --cache /tmp/.pipcache -e . --no-deps psql -X -q -v ON_ERROR_STOP=1 <=0.1,<0.2` from PyPI by default. + +Examples: + +```bash +uv run --directory src/pgstac-migrate pgstac-migrate build-artifact +uv run --directory src/pgstac-migrate pgstac-migrate info +uv run --directory src/pgstac-migrate pgstac-migrate versions +uv run --directory src/pgstac-migrate pgstac-migrate migrate --help +``` + +Standalone post-release bootstrap helper: + +```bash +uv run --script src/pgstac-migrate/scripts/build_artifact.py +``` + +That helper does not use `uv.lock`; it resolves its own inline dependency on `pgpkg>=0.1,<0.2` directly from PyPI. diff --git a/src/pgstac-migrate/pyproject.toml b/src/pgstac-migrate/pyproject.toml new file mode 100644 index 00000000..f507e8a9 --- /dev/null +++ b/src/pgstac-migrate/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "pgstac-migrate" +version = "0.9.11-dev" +description = "Apply PgSTAC database migrations." +readme = "README.md" +requires-python = ">=3.11" +license = "MIT" +dependencies = [ + "pgpkg>=0.1,<0.2", +] + +[project.scripts] +pgstac-migrate = "pgstac_migrate.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/pgstac_migrate"] diff --git a/src/pgstac-migrate/scripts/build_artifact.py b/src/pgstac-migrate/scripts/build_artifact.py new file mode 100644 index 00000000..880c5f1e --- /dev/null +++ b/src/pgstac-migrate/scripts/build_artifact.py @@ -0,0 +1,29 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "pgpkg>=0.1,<0.2", +# ] +# /// +"""Build the local pgstac-migrate baked artifact with the published pgpkg API.""" + +from __future__ import annotations + +from pathlib import Path + +from pgpkg.api import bundle_project + + +def main() -> int: + package_root = Path(__file__).resolve().parents[1] + repo_root = package_root.parents[1] + project_root = repo_root / "src" / "pgstac" + artifact_path = package_root / "src" / "pgstac_migrate" / "migrations.tar.zst" + + artifact_path = bundle_project(project_root, artifact_path) + print(f"wrote {artifact_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/pgstac-migrate/src/pgstac_migrate/__init__.py b/src/pgstac-migrate/src/pgstac_migrate/__init__.py new file mode 100644 index 00000000..192da94a --- /dev/null +++ b/src/pgstac-migrate/src/pgstac_migrate/__init__.py @@ -0,0 +1,3 @@ +"""PgSTAC migration wrapper.""" + +__version__ = "0.9.11-dev" diff --git a/src/pgstac-migrate/src/pgstac_migrate/api.py b/src/pgstac-migrate/src/pgstac_migrate/api.py new file mode 100644 index 00000000..83e27d3c --- /dev/null +++ b/src/pgstac-migrate/src/pgstac_migrate/api.py @@ -0,0 +1,51 @@ +"""Public Python API for PgSTAC migration artifacts.""" + +from __future__ import annotations + +from pathlib import Path + +from pgpkg.api import migrate_from_artifact +from pgpkg.executor import ApplyResult + +from pgstac_migrate.build import ensure_artifact_path +from pgstac_migrate.version_source import PgstacVersionSource + + +def normalize_target_version(target: str | None) -> str | None: + """Map source-tree dev versions to the staged unreleased migration label.""" + if target is None: + return None + if target.endswith("-dev"): + return "unreleased" + return target + + +def artifact_path() -> Path: + """Return the baked artifact path, building it when running from source.""" + return ensure_artifact_path() + + +def migrate( + *, + target: str | None = None, + dry_run: bool = False, + conninfo: str | None = None, + host: str | None = None, + port: int | str | None = None, + dbname: str | None = None, + user: str | None = None, + password: str | None = None, +) -> ApplyResult: + """Apply baked PgSTAC migrations to a live database.""" + return migrate_from_artifact( + str(artifact_path()), + target=normalize_target_version(target), + dry_run=dry_run, + conninfo=conninfo, + host=host, + port=port, + dbname=dbname, + user=user, + password=password, + version_source=PgstacVersionSource(), + ) diff --git a/src/pgstac-migrate/src/pgstac_migrate/build.py b/src/pgstac-migrate/src/pgstac_migrate/build.py new file mode 100644 index 00000000..1d9f861d --- /dev/null +++ b/src/pgstac-migrate/src/pgstac_migrate/build.py @@ -0,0 +1,45 @@ +"""Source-tree helpers for building and locating the baked PgSTAC artifact.""" + +from __future__ import annotations + +from pathlib import Path + +from pgpkg.api import bundle_project +from pgpkg.errors import PgpkgError + + +def artifact_path() -> Path: + return Path(__file__).with_name("migrations.tar.zst") + + +def package_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def source_project_root() -> Path: + project_root = package_root().parents[1] / "src" / "pgstac" + if not (project_root / "pyproject.toml").is_file(): + raise PgpkgError( + "Could not find the PgSTAC source tree. `build-artifact` only works from a pgstac checkout.", + code="E_ARTIFACT", + ) + return project_root + + +def build_local_artifact(output_path: Path | None = None) -> Path: + return bundle_project(source_project_root(), output_path or artifact_path()) + + +def ensure_artifact_path() -> Path: + """Return the baked artifact path, building it from source when possible.""" + path = artifact_path() + if path.is_file(): + return path + + try: + return build_local_artifact(path) + except PgpkgError as exc: + raise PgpkgError( + "Missing baked artifact. Run `uv run --directory src/pgstac-migrate pgstac-migrate build-artifact` first.", + code="E_ARTIFACT", + ) from exc diff --git a/src/pgstac-migrate/src/pgstac_migrate/cli.py b/src/pgstac-migrate/src/pgstac_migrate/cli.py new file mode 100644 index 00000000..0a57fb4d --- /dev/null +++ b/src/pgstac-migrate/src/pgstac_migrate/cli.py @@ -0,0 +1,160 @@ +"""CLI for PgSTAC migration artifacts.""" + +from __future__ import annotations + +import argparse +import atexit +import shutil +import sys +import tempfile +from pathlib import Path + +from pgpkg.artifact import LoadedArtifact, load_artifact +from pgpkg.catalog import Catalog, build_catalog +from pgpkg.cli import _add_db_args, _resolve_password +from pgpkg.config import ProjectConfig +from pgpkg.errors import PgpkgError +from pgpkg.planner import MigrationPlan, plan +from pgpkg.versioning import default_target + +from pgstac_migrate.api import artifact_path as resolved_artifact_path +from pgstac_migrate.api import migrate as migrate_database +from pgstac_migrate.build import build_local_artifact + + +def _artifact_path() -> Path: + return resolved_artifact_path() + + +def _catalog_from_artifact(artifact: LoadedArtifact) -> Catalog: + tmp_root = Path(tempfile.mkdtemp(prefix="pgstac_migrate_")) + atexit.register(shutil.rmtree, tmp_root, True) + + migrations_dir = tmp_root / "migrations" + migrations_dir.mkdir() + for name, data in artifact.migrations_files().items(): + (migrations_dir / Path(name).name).write_bytes(data) + + sql_dir = tmp_root / "sql" + pre_dir = sql_dir / "pre" + post_dir = sql_dir / "post" + pre_dir.mkdir(parents=True) + post_dir.mkdir(parents=True) + + config = ProjectConfig( + project_name=artifact.manifest.project_name, + prefix=artifact.manifest.prefix, + sql_dir=sql_dir, + migrations_dir=migrations_dir, + pre_dir=pre_dir, + post_dir=post_dir, + project_root=tmp_root, + version_source=artifact.manifest.version_source, + tracking_schema=artifact.manifest.tracking_schema, + tracking_table=artifact.manifest.tracking_table, + ) + return build_catalog(config) + + +def _load_artifact_and_catalog() -> tuple[LoadedArtifact, Catalog]: + artifact = load_artifact(_artifact_path()) + return artifact, _catalog_from_artifact(artifact) + + +def _render_plan(migration_plan: MigrationPlan) -> None: + print(f"target: {migration_plan.target}") + print(f"source: {migration_plan.source}") + bootstrap = migration_plan.bootstrap_base + print(f"bootstrap: {bootstrap.name if bootstrap else '(none)'}") + print("steps:") + if not migration_plan.steps: + print(" (none)") + for step in migration_plan.steps: + print(f" {step.from_version} -> {step.to_version} [{step.file.name}]") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="pgstac-migrate", add_help=False) + parser.add_argument("--help", action="help", help="Show help and exit") + sub = parser.add_subparsers(dest="cmd", required=True) + + p_migrate = sub.add_parser( + "migrate", + help="Apply baked PgSTAC migrations to a live DB", + add_help=False, + ) + p_migrate.add_argument("--help", action="help", help="Show help and exit") + _add_db_args(p_migrate) + p_migrate.add_argument( + "--to", dest="target", help="Target version (default: highest)" + ) + p_migrate.add_argument("--dry-run", action="store_true") + + sub.add_parser("info", help="Print baked artifact info") + sub.add_parser("versions", help="List baked migration versions") + sub.add_parser("build-artifact", help="Bake the local PgSTAC migration artifact") + + p_plan = sub.add_parser("plan", help="Show baked migration plan") + p_plan.add_argument("--source", help="Source version (omit for fresh install)") + p_plan.add_argument("--to", dest="target", help="Target version (default: highest)") + + args = parser.parse_args(argv) + + try: + if args.cmd == "build-artifact": + path = build_local_artifact() + print(f"wrote {path}") + return 0 + + if args.cmd == "migrate": + password = _resolve_password(args) + result = migrate_database( + target=args.target, + dry_run=args.dry_run, + conninfo=args.dsn, + host=args.host, + port=args.port, + dbname=args.dbname, + user=args.user, + password=password, + ) + if result.bootstrapped_from is not None: + print(f"bootstrapped to {result.bootstrapped_from}") + for from_version, to_version in result.applied_steps: + print(f"applied {from_version} -> {to_version}") + print(f"final version: {result.final_version}") + if args.dry_run: + print("(dry-run: rolled back)") + return 0 + + artifact, catalog = _load_artifact_and_catalog() + if args.cmd == "info": + print(f"project: {artifact.manifest.project_name}") + print(f"prefix: {artifact.manifest.prefix}") + for entry in artifact.manifest.entries: + print(f" {entry.name} {entry.sha256[:12]} {entry.size}B") + return 0 + + if args.cmd == "versions": + for version in catalog.versions: + print(version) + return 0 + + if args.cmd == "plan": + target = args.target or default_target(catalog.versions) + if target is None: + raise PgpkgError( + "Artifact catalog is empty; nothing to plan.", code="E_PLAN" + ) + migration_plan = plan(catalog, source=args.source, target=target) + _render_plan(migration_plan) + return 0 + + return 2 + except PgpkgError as exc: + print(f"error [{exc.code}]: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/pgstac-migrate/src/pgstac_migrate/compat.py b/src/pgstac-migrate/src/pgstac_migrate/compat.py new file mode 100644 index 00000000..3acf2f93 --- /dev/null +++ b/src/pgstac-migrate/src/pgstac_migrate/compat.py @@ -0,0 +1,100 @@ +"""Compatibility helpers for callers that still import legacy migration types.""" + +from __future__ import annotations + +import glob +import os +from collections import defaultdict +from collections.abc import Iterator + +MIGRATION_PREFIX = "pgstac--" + + +def base_migration_filename(version: str) -> str: + """Return the canonical base migration filename for a version.""" + return f"{MIGRATION_PREFIX}{version}.sql" + + +def incremental_migration_filename(from_version: str, to_version: str) -> str: + """Return the canonical incremental migration filename for a version hop.""" + return f"{MIGRATION_PREFIX}{from_version}--{to_version}.sql" + + +class MigrationPath: + """Calculate path from migration files to get from one version to the next.""" + + def __init__(self, path: str, f: str, t: str) -> None: + """Initialize MigrationPath.""" + self.path = path + if f is None: + f = "init" + if t is None: + raise Exception('Must set "to" version') + if f == t: + raise Exception("No Migration Necessary") + + self.f = f + self.t = t + + def parse_filename(self, filename: str) -> list[str]: + """Get version numbers from filename.""" + filename = os.path.splitext(os.path.basename(filename))[0].replace( + MIGRATION_PREFIX, + "", + 1, + ) + return filename.split("--") + + def get_files(self) -> Iterator[str]: + """Find all migration files available.""" + path = self.path.rstrip("/") + return glob.iglob(f"{path}/*.sql") + + def build_graph(self) -> dict[str, list[str]]: + """Build a graph to get from one version to another.""" + graph = defaultdict(list) + for file in self.get_files(): + parts = self.parse_filename(file) + if len(parts) == 2: + graph[parts[0]].append(parts[1]) + else: + graph["init"].append(parts[0]) + return graph + + def build_path(self) -> list[str] | None: + """Create the path of ordered files needed to migrate.""" + graph = self.build_graph() + explored: list[str] = [] + q = [[self.f]] + + while q: + path = q.pop(0) + node = path[-1] + if node not in explored: + neighbours = graph[node] + for neighbour in neighbours: + new_path = list(path) + new_path.append(neighbour) + q.append(new_path) + if neighbour == self.t: + return new_path + explored.append(node) + return None + + def migrations(self) -> list[str]: + """Return the list of migrations needed in order.""" + path = self.build_path() + if path is None: + raise Exception( + f"Could not determine path to get from {self.f} to {self.t}.", + ) + if len(path) == 1: + return [] + files = [] + start_idx = 0 + if path[0] == "init": + files.append(base_migration_filename(path[1])) + start_idx = 1 + for idx in range(start_idx, len(path) - 1): + files.append(incremental_migration_filename(path[idx], path[idx + 1])) + return files diff --git a/src/pgstac-migrate/src/pgstac_migrate/version_source.py b/src/pgstac-migrate/src/pgstac_migrate/version_source.py new file mode 100644 index 00000000..067f3e32 --- /dev/null +++ b/src/pgstac-migrate/src/pgstac_migrate/version_source.py @@ -0,0 +1,77 @@ +"""PgSTAC-specific version tracking integration for pgpkg.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pgpkg.tracking import current_tracking_version + +if TYPE_CHECKING: + import psycopg + from pgpkg.config import ProjectConfig + + +class PgstacVersionSource: + """Use pgstac.migrations as the authoritative installed version.""" + + def _has_set_version(self, conn: psycopg.Connection) -> bool: + with conn.cursor() as cur: + cur.execute("SELECT to_regprocedure('pgstac.set_version(text)')") + row = cur.fetchone() + return row is not None and row[0] is not None + + def read_live_version( + self, + conn: psycopg.Connection, + config: ProjectConfig, + ) -> str | None: + del config + with conn.cursor() as cur: + cur.execute("SELECT to_regclass('pgstac.migrations')") + row = cur.fetchone() + if row is None or row[0] is None: + return None + cur.execute( + """ + SELECT version + FROM pgstac.migrations + ORDER BY datetime DESC, version DESC + LIMIT 1 + """, + ) + version_row = cur.fetchone() + return version_row[0] if version_row else None + + def record_applied( + self, + conn: psycopg.Connection, + config: ProjectConfig, + *, + version: str, + sha256: str, + filename: str, + ) -> None: + del sha256, filename + with conn.cursor() as cur: + if self._has_set_version(conn): + cur.execute("SELECT pgstac.set_version(%s)", (version,)) + else: + cur.execute( + "INSERT INTO pgstac.migrations (version) VALUES (%s)", (version,) + ) + + tracking_version = current_tracking_version( + conn, + schema=config.tracking_schema, + table=config.tracking_table, + ) + if tracking_version != version: + raise RuntimeError( + f"pgpkg tracking version mismatch: expected {version!r}, got {tracking_version!r}", + ) + + live_version = self.read_live_version(conn, config) + if live_version != version: + raise RuntimeError( + f"pgstac live version mismatch: expected {version!r}, got {live_version!r}", + ) diff --git a/src/pgstac-migrate/tests/test_api.py b/src/pgstac-migrate/tests/test_api.py new file mode 100644 index 00000000..f5a2c3fa --- /dev/null +++ b/src/pgstac-migrate/tests/test_api.py @@ -0,0 +1,44 @@ +from importlib import import_module +from pathlib import Path +from types import SimpleNamespace + + +def test_normalize_target_version_maps_dev_to_unreleased() -> None: + api = import_module("pgstac_migrate.api") + + assert api.normalize_target_version("0.9.11-dev") == "unreleased" + assert api.normalize_target_version("0.9.11") == "0.9.11" + assert api.normalize_target_version(None) is None + + +def test_artifact_path_builds_from_source_when_missing( + monkeypatch, tmp_path: Path +) -> None: + api = import_module("pgstac_migrate.api") + artifact = tmp_path / "migrations.tar.zst" + + monkeypatch.setattr(api, "ensure_artifact_path", lambda: artifact) + + assert api.artifact_path() == artifact + + +def test_migrate_uses_artifact_api(monkeypatch, tmp_path: Path) -> None: + api = import_module("pgstac_migrate.api") + artifact = tmp_path / "migrations.tar.zst" + captured: dict[str, object] = {} + + def fake_migrate_from_artifact(path: str, **kwargs): + captured["path"] = path + captured.update(kwargs) + return SimpleNamespace(final_version="0.9.11") + + monkeypatch.setattr(api, "artifact_path", lambda: artifact) + monkeypatch.setattr(api, "migrate_from_artifact", fake_migrate_from_artifact) + + result = api.migrate(target="0.9.11-dev", conninfo="postgresql:///example") + + assert result.final_version == "0.9.11" + assert captured["path"] == str(artifact) + assert captured["target"] == "unreleased" + assert captured["conninfo"] == "postgresql:///example" + assert captured["version_source"].__class__.__name__ == "PgstacVersionSource" diff --git a/src/pgstac-migrate/tests/test_cli.py b/src/pgstac-migrate/tests/test_cli.py new file mode 100644 index 00000000..a144659a --- /dev/null +++ b/src/pgstac-migrate/tests/test_cli.py @@ -0,0 +1,57 @@ +from importlib import import_module +from pathlib import Path + +import pytest + + +def run_cli(argv: list[str]) -> int: + return import_module("pgstac_migrate.cli").main(argv) + + +@pytest.fixture(scope="module", autouse=True) +def ensure_baked_artifact() -> None: + package_root = Path(__file__).resolve().parents[1] + artifact_path = package_root / "src" / "pgstac_migrate" / "migrations.tar.zst" + if artifact_path.is_file(): + return + + exit_code = run_cli(["build-artifact"]) + if exit_code != 0: + raise RuntimeError("pgstac-migrate build-artifact failed during test bootstrap") + + +def test_build_artifact_command_reports_output( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + built = tmp_path / "migrations.tar.zst" + monkeypatch.setattr( + import_module("pgstac_migrate.cli"), + "build_local_artifact", + lambda: built, + ) + + exit_code = run_cli(["build-artifact"]) + + captured = capsys.readouterr() + assert exit_code == 0 + assert f"wrote {built}" in captured.out + + +def test_versions_lists_known_versions(capsys) -> None: + exit_code = run_cli(["versions"]) + + captured = capsys.readouterr() + assert exit_code == 0 + assert "0.1.9" in captured.out.splitlines() + assert "unreleased" in captured.out.splitlines() + + +def test_plan_renders_known_incremental_step(capsys) -> None: + exit_code = run_cli(["plan", "--source", "0.9.10", "--to", "0.9.11"]) + + captured = capsys.readouterr() + assert exit_code == 0 + assert "0.9.10 -> 0.9.11" in captured.out + assert "pgstac--0.9.10--0.9.11.sql" in captured.out diff --git a/src/pgstac-migrate/tests/test_version_source.py b/src/pgstac-migrate/tests/test_version_source.py new file mode 100644 index 00000000..0f13cb6d --- /dev/null +++ b/src/pgstac-migrate/tests/test_version_source.py @@ -0,0 +1,101 @@ +from importlib import import_module +from types import SimpleNamespace + +import pytest + + +class FakeCursor: + def __init__( + self, + *, + fetchone_results: list[tuple[object, ...] | None], + executed: list[tuple[str, object | None]], + ): + self._fetchone_results = fetchone_results + self._executed = executed + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def execute(self, query, params=None): + self._executed.append((str(query), params)) + + def fetchone(self): + if not self._fetchone_results: + raise AssertionError("No fetchone result queued") + return self._fetchone_results.pop(0) + + +class FakeConnection: + def __init__(self, *, fetchone_results: list[tuple[object, ...] | None]): + self.executed: list[tuple[str, object | None]] = [] + self._fetchone_results = fetchone_results + + def cursor(self): + return FakeCursor( + fetchone_results=self._fetchone_results, executed=self.executed + ) + + +@pytest.fixture +def version_source_module(): + return import_module("pgstac_migrate.version_source") + + +def test_record_applied_uses_set_version_when_available( + monkeypatch, version_source_module +) -> None: + source = version_source_module.PgstacVersionSource() + conn = FakeConnection(fetchone_results=[("pgstac.set_version(text)",)]) + config = SimpleNamespace(tracking_schema="pgpkg", tracking_table="migrations") + + monkeypatch.setattr( + version_source_module, + "current_tracking_version", + lambda *args, **kwargs: "0.3.0", + ) + monkeypatch.setattr(source, "read_live_version", lambda *args, **kwargs: "0.3.0") + + source.record_applied( + conn, + config, + version="0.3.0", + sha256="ignored", + filename="pgstac--0.3.0.sql", + ) + + assert conn.executed == [ + ("SELECT to_regprocedure('pgstac.set_version(text)')", None), + ("SELECT pgstac.set_version(%s)", ("0.3.0",)), + ] + + +def test_record_applied_falls_back_to_direct_insert_without_set_version( + monkeypatch, version_source_module +) -> None: + source = version_source_module.PgstacVersionSource() + conn = FakeConnection(fetchone_results=[(None,)]) + config = SimpleNamespace(tracking_schema="pgpkg", tracking_table="migrations") + + monkeypatch.setattr( + version_source_module, + "current_tracking_version", + lambda *args, **kwargs: "0.3.0", + ) + monkeypatch.setattr(source, "read_live_version", lambda *args, **kwargs: "0.3.0") + + source.record_applied( + conn, + config, + version="0.3.0", + sha256="ignored", + filename="pgstac--0.3.0.sql", + ) + + assert conn.executed == [ + ("SELECT to_regprocedure('pgstac.set_version(text)')", None), + ("INSERT INTO pgstac.migrations (version) VALUES (%s)", ("0.3.0",)), + ] diff --git a/src/pgstac-migrate/uv.lock b/src/pgstac-migrate/uv.lock new file mode 100644 index 00000000..c5ba4f2b --- /dev/null +++ b/src/pgstac-migrate/uv.lock @@ -0,0 +1,198 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pgpkg" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "psycopg", extra = ["binary"] }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/12/bd74a956815835a0a1d318f54deab5ebfc8d807178e99421f6232d806111/pgpkg-0.1.0.tar.gz", hash = "sha256:fecfea66c84c5976eb4058f3325e4d601a4a47378b1499f56ba413b7222b5838", size = 43573, upload-time = "2026-05-05T21:24:41.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/8f/7153e33850f68867b340c93cde3b17d3784dbf28880169383cc4b01cff95/pgpkg-0.1.0-py3-none-any.whl", hash = "sha256:1d68d2b2287bf68ee3c47012678eac4247bad79fcefbb9fc53cff1480d4f9d73", size = 30600, upload-time = "2026-05-05T21:24:39.768Z" }, +] + +[[package]] +name = "pgstac-migrate" +version = "0.9.11.dev0" +source = { editable = "." } +dependencies = [ + { name = "pgpkg" }, +] + +[package.metadata] +requires-dist = [{ name = "pgpkg", specifier = ">=0.1,<0.2" }] + +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/82/df3312c0ca083d5b43b352f27d4dd8b1e614bd334473074715d9e0000da4/psycopg_binary-3.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da", size = 4609813, upload-time = "2026-05-01T23:26:30.612Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/d74d542458d3e8ac0571d8a88f57ca369999b9a82f4fa528052d0d7d3e4c/psycopg_binary-3.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e", size = 4676799, upload-time = "2026-05-01T23:26:38.475Z" }, + { url = "https://files.pythonhosted.org/packages/09/67/06bab9c60671999f4c6ceff1b334f3ac1f9fc5789eb467c714623ea21de9/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992", size = 5497050, upload-time = "2026-05-01T23:26:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/72/9b/023433e2b20f970de1e22d29132a95281277646da0b2e2879dd4ee94b8c1/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4", size = 5172428, upload-time = "2026-05-01T23:26:56.708Z" }, + { url = "https://files.pythonhosted.org/packages/08/cd/ae16da8fde228a38b2fe9269bbc13cf89e0186173f2265600f02d6a71e64/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e", size = 6762746, upload-time = "2026-05-01T23:27:07.023Z" }, + { url = "https://files.pythonhosted.org/packages/4f/81/0ba09fa5f5f88779093a2541a8e02489825721f258ab88058b11d68b3eb5/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097", size = 5006033, upload-time = "2026-05-01T23:27:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/73/6a/629136040cc3497adb442a305710b5913f2a754d4630fc3d3717c4c0df65/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95", size = 4534175, upload-time = "2026-05-01T23:27:18.248Z" }, + { url = "https://files.pythonhosted.org/packages/7c/32/1027f843c6dc2d5d51960ee62cc0c2cf755a4c39455aff1371173edbef7d/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839", size = 4224203, upload-time = "2026-05-01T23:27:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e1/380a724d9093c74adb14d4fce920ea8327838abb61f760b1448586b14a8e/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007", size = 3954509, upload-time = "2026-05-01T23:27:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/db/cd/895893ae575a09c97ccfd5def070d88993d955ef34df45a881fd5ff506d6/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c", size = 4259551, upload-time = "2026-05-01T23:27:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/2330a20794e37a3ec609ef2fd8522919ec7a4395a1abf979a8e2d1775cd5/psycopg_binary-3.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d", size = 3572054, upload-time = "2026-05-01T23:27:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +] diff --git a/src/pgstac/migrations/pgstac.0.1.9-0.2.3.sql b/src/pgstac/migrations/pgstac--0.1.9--0.2.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.1.9-0.2.3.sql rename to src/pgstac/migrations/pgstac--0.1.9--0.2.3.sql diff --git a/src/pgstac/migrations/pgstac.0.1.9.sql b/src/pgstac/migrations/pgstac--0.1.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.1.9.sql rename to src/pgstac/migrations/pgstac--0.1.9.sql diff --git a/src/pgstac/migrations/pgstac.0.2.3-0.2.4.sql b/src/pgstac/migrations/pgstac--0.2.3--0.2.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.3-0.2.4.sql rename to src/pgstac/migrations/pgstac--0.2.3--0.2.4.sql diff --git a/src/pgstac/migrations/pgstac.0.2.3.sql b/src/pgstac/migrations/pgstac--0.2.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.3.sql rename to src/pgstac/migrations/pgstac--0.2.3.sql diff --git a/src/pgstac/migrations/pgstac.0.2.4-0.2.5.sql b/src/pgstac/migrations/pgstac--0.2.4--0.2.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.4-0.2.5.sql rename to src/pgstac/migrations/pgstac--0.2.4--0.2.5.sql diff --git a/src/pgstac/migrations/pgstac.0.2.4-0.2.7.sql b/src/pgstac/migrations/pgstac--0.2.4--0.2.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.4-0.2.7.sql rename to src/pgstac/migrations/pgstac--0.2.4--0.2.7.sql diff --git a/src/pgstac/migrations/pgstac.0.2.4.sql b/src/pgstac/migrations/pgstac--0.2.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.4.sql rename to src/pgstac/migrations/pgstac--0.2.4.sql diff --git a/src/pgstac/migrations/pgstac.0.2.5-0.2.7.sql b/src/pgstac/migrations/pgstac--0.2.5--0.2.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.5-0.2.7.sql rename to src/pgstac/migrations/pgstac--0.2.5--0.2.7.sql diff --git a/src/pgstac/migrations/pgstac.0.2.5.sql b/src/pgstac/migrations/pgstac--0.2.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.5.sql rename to src/pgstac/migrations/pgstac--0.2.5.sql diff --git a/src/pgstac/migrations/pgstac.0.2.7-0.2.8.sql b/src/pgstac/migrations/pgstac--0.2.7--0.2.8.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.7-0.2.8.sql rename to src/pgstac/migrations/pgstac--0.2.7--0.2.8.sql diff --git a/src/pgstac/migrations/pgstac.0.2.7.sql b/src/pgstac/migrations/pgstac--0.2.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.7.sql rename to src/pgstac/migrations/pgstac--0.2.7.sql diff --git a/src/pgstac/migrations/pgstac.0.2.8-0.2.9.sql b/src/pgstac/migrations/pgstac--0.2.8--0.2.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.8-0.2.9.sql rename to src/pgstac/migrations/pgstac--0.2.8--0.2.9.sql diff --git a/src/pgstac/migrations/pgstac.0.2.8.sql b/src/pgstac/migrations/pgstac--0.2.8.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.8.sql rename to src/pgstac/migrations/pgstac--0.2.8.sql diff --git a/src/pgstac/migrations/pgstac.0.2.9-0.3.0.sql b/src/pgstac/migrations/pgstac--0.2.9--0.3.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.9-0.3.0.sql rename to src/pgstac/migrations/pgstac--0.2.9--0.3.0.sql diff --git a/src/pgstac/migrations/pgstac.0.2.9.sql b/src/pgstac/migrations/pgstac--0.2.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.2.9.sql rename to src/pgstac/migrations/pgstac--0.2.9.sql diff --git a/src/pgstac/migrations/pgstac.0.3.0-0.3.1.sql b/src/pgstac/migrations/pgstac--0.3.0--0.3.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.0-0.3.1.sql rename to src/pgstac/migrations/pgstac--0.3.0--0.3.1.sql diff --git a/src/pgstac/migrations/pgstac.0.3.0.sql b/src/pgstac/migrations/pgstac--0.3.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.0.sql rename to src/pgstac/migrations/pgstac--0.3.0.sql diff --git a/src/pgstac/migrations/pgstac.0.3.1-0.3.2.sql b/src/pgstac/migrations/pgstac--0.3.1--0.3.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.1-0.3.2.sql rename to src/pgstac/migrations/pgstac--0.3.1--0.3.2.sql diff --git a/src/pgstac/migrations/pgstac.0.3.1.sql b/src/pgstac/migrations/pgstac--0.3.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.1.sql rename to src/pgstac/migrations/pgstac--0.3.1.sql diff --git a/src/pgstac/migrations/pgstac.0.3.2-0.3.3.sql b/src/pgstac/migrations/pgstac--0.3.2--0.3.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.2-0.3.3.sql rename to src/pgstac/migrations/pgstac--0.3.2--0.3.3.sql diff --git a/src/pgstac/migrations/pgstac.0.3.2.sql b/src/pgstac/migrations/pgstac--0.3.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.2.sql rename to src/pgstac/migrations/pgstac--0.3.2.sql diff --git a/src/pgstac/migrations/pgstac.0.3.3-0.3.4.sql b/src/pgstac/migrations/pgstac--0.3.3--0.3.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.3-0.3.4.sql rename to src/pgstac/migrations/pgstac--0.3.3--0.3.4.sql diff --git a/src/pgstac/migrations/pgstac.0.3.3.sql b/src/pgstac/migrations/pgstac--0.3.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.3.sql rename to src/pgstac/migrations/pgstac--0.3.3.sql diff --git a/src/pgstac/migrations/pgstac.0.3.4-0.3.5.sql b/src/pgstac/migrations/pgstac--0.3.4--0.3.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.4-0.3.5.sql rename to src/pgstac/migrations/pgstac--0.3.4--0.3.5.sql diff --git a/src/pgstac/migrations/pgstac.0.3.4.sql b/src/pgstac/migrations/pgstac--0.3.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.4.sql rename to src/pgstac/migrations/pgstac--0.3.4.sql diff --git a/src/pgstac/migrations/pgstac.0.3.5-0.3.6.sql b/src/pgstac/migrations/pgstac--0.3.5--0.3.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.5-0.3.6.sql rename to src/pgstac/migrations/pgstac--0.3.5--0.3.6.sql diff --git a/src/pgstac/migrations/pgstac.0.3.5.sql b/src/pgstac/migrations/pgstac--0.3.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.5.sql rename to src/pgstac/migrations/pgstac--0.3.5.sql diff --git a/src/pgstac/migrations/pgstac.0.3.6-0.4.0.sql b/src/pgstac/migrations/pgstac--0.3.6--0.4.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.6-0.4.0.sql rename to src/pgstac/migrations/pgstac--0.3.6--0.4.0.sql diff --git a/src/pgstac/migrations/pgstac.0.3.6.sql b/src/pgstac/migrations/pgstac--0.3.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.3.6.sql rename to src/pgstac/migrations/pgstac--0.3.6.sql diff --git a/src/pgstac/migrations/pgstac.0.4.0-0.4.1.sql b/src/pgstac/migrations/pgstac--0.4.0--0.4.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.0-0.4.1.sql rename to src/pgstac/migrations/pgstac--0.4.0--0.4.1.sql diff --git a/src/pgstac/migrations/pgstac.0.4.0.sql b/src/pgstac/migrations/pgstac--0.4.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.0.sql rename to src/pgstac/migrations/pgstac--0.4.0.sql diff --git a/src/pgstac/migrations/pgstac.0.4.1-0.4.2.sql b/src/pgstac/migrations/pgstac--0.4.1--0.4.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.1-0.4.2.sql rename to src/pgstac/migrations/pgstac--0.4.1--0.4.2.sql diff --git a/src/pgstac/migrations/pgstac.0.4.1.sql b/src/pgstac/migrations/pgstac--0.4.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.1.sql rename to src/pgstac/migrations/pgstac--0.4.1.sql diff --git a/src/pgstac/migrations/pgstac.0.4.2-0.4.3.sql b/src/pgstac/migrations/pgstac--0.4.2--0.4.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.2-0.4.3.sql rename to src/pgstac/migrations/pgstac--0.4.2--0.4.3.sql diff --git a/src/pgstac/migrations/pgstac.0.4.2.sql b/src/pgstac/migrations/pgstac--0.4.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.2.sql rename to src/pgstac/migrations/pgstac--0.4.2.sql diff --git a/src/pgstac/migrations/pgstac.0.4.3-0.4.4.sql b/src/pgstac/migrations/pgstac--0.4.3--0.4.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.3-0.4.4.sql rename to src/pgstac/migrations/pgstac--0.4.3--0.4.4.sql diff --git a/src/pgstac/migrations/pgstac.0.4.3.sql b/src/pgstac/migrations/pgstac--0.4.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.3.sql rename to src/pgstac/migrations/pgstac--0.4.3.sql diff --git a/src/pgstac/migrations/pgstac.0.4.4-0.4.5.sql b/src/pgstac/migrations/pgstac--0.4.4--0.4.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.4-0.4.5.sql rename to src/pgstac/migrations/pgstac--0.4.4--0.4.5.sql diff --git a/src/pgstac/migrations/pgstac.0.4.4.sql b/src/pgstac/migrations/pgstac--0.4.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.4.sql rename to src/pgstac/migrations/pgstac--0.4.4.sql diff --git a/src/pgstac/migrations/pgstac.0.4.5-0.5.0.sql b/src/pgstac/migrations/pgstac--0.4.5--0.5.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.5-0.5.0.sql rename to src/pgstac/migrations/pgstac--0.4.5--0.5.0.sql diff --git a/src/pgstac/migrations/pgstac.0.4.5.sql b/src/pgstac/migrations/pgstac--0.4.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.4.5.sql rename to src/pgstac/migrations/pgstac--0.4.5.sql diff --git a/src/pgstac/migrations/pgstac.0.5.0-0.5.1.sql b/src/pgstac/migrations/pgstac--0.5.0--0.5.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.5.0-0.5.1.sql rename to src/pgstac/migrations/pgstac--0.5.0--0.5.1.sql diff --git a/src/pgstac/migrations/pgstac.0.5.0.sql b/src/pgstac/migrations/pgstac--0.5.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.5.0.sql rename to src/pgstac/migrations/pgstac--0.5.0.sql diff --git a/src/pgstac/migrations/pgstac.0.5.1-0.6.0.sql b/src/pgstac/migrations/pgstac--0.5.1--0.6.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.5.1-0.6.0.sql rename to src/pgstac/migrations/pgstac--0.5.1--0.6.0.sql diff --git a/src/pgstac/migrations/pgstac.0.5.1.sql b/src/pgstac/migrations/pgstac--0.5.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.5.1.sql rename to src/pgstac/migrations/pgstac--0.5.1.sql diff --git a/src/pgstac/migrations/pgstac.0.6.0-0.6.1.sql b/src/pgstac/migrations/pgstac--0.6.0--0.6.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.0-0.6.1.sql rename to src/pgstac/migrations/pgstac--0.6.0--0.6.1.sql diff --git a/src/pgstac/migrations/pgstac.0.6.0.sql b/src/pgstac/migrations/pgstac--0.6.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.0.sql rename to src/pgstac/migrations/pgstac--0.6.0.sql diff --git a/src/pgstac/migrations/pgstac.0.6.1-0.6.2.sql b/src/pgstac/migrations/pgstac--0.6.1--0.6.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.1-0.6.2.sql rename to src/pgstac/migrations/pgstac--0.6.1--0.6.2.sql diff --git a/src/pgstac/migrations/pgstac.0.6.1.sql b/src/pgstac/migrations/pgstac--0.6.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.1.sql rename to src/pgstac/migrations/pgstac--0.6.1.sql diff --git a/src/pgstac/migrations/pgstac.0.6.10-0.6.11.sql b/src/pgstac/migrations/pgstac--0.6.10--0.6.11.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.10-0.6.11.sql rename to src/pgstac/migrations/pgstac--0.6.10--0.6.11.sql diff --git a/src/pgstac/migrations/pgstac.0.6.10.sql b/src/pgstac/migrations/pgstac--0.6.10.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.10.sql rename to src/pgstac/migrations/pgstac--0.6.10.sql diff --git a/src/pgstac/migrations/pgstac.0.6.11-0.6.12.sql b/src/pgstac/migrations/pgstac--0.6.11--0.6.12.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.11-0.6.12.sql rename to src/pgstac/migrations/pgstac--0.6.11--0.6.12.sql diff --git a/src/pgstac/migrations/pgstac.0.6.11.sql b/src/pgstac/migrations/pgstac--0.6.11.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.11.sql rename to src/pgstac/migrations/pgstac--0.6.11.sql diff --git a/src/pgstac/migrations/pgstac.0.6.12-0.6.13.sql b/src/pgstac/migrations/pgstac--0.6.12--0.6.13.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.12-0.6.13.sql rename to src/pgstac/migrations/pgstac--0.6.12--0.6.13.sql diff --git a/src/pgstac/migrations/pgstac.0.6.12.sql b/src/pgstac/migrations/pgstac--0.6.12.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.12.sql rename to src/pgstac/migrations/pgstac--0.6.12.sql diff --git a/src/pgstac/migrations/pgstac.0.6.13-0.7.0.sql b/src/pgstac/migrations/pgstac--0.6.13--0.7.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.13-0.7.0.sql rename to src/pgstac/migrations/pgstac--0.6.13--0.7.0.sql diff --git a/src/pgstac/migrations/pgstac.0.6.13-0.7.3.sql b/src/pgstac/migrations/pgstac--0.6.13--0.7.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.13-0.7.3.sql rename to src/pgstac/migrations/pgstac--0.6.13--0.7.3.sql diff --git a/src/pgstac/migrations/pgstac.0.6.13.sql b/src/pgstac/migrations/pgstac--0.6.13.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.13.sql rename to src/pgstac/migrations/pgstac--0.6.13.sql diff --git a/src/pgstac/migrations/pgstac.0.6.2-0.6.3.sql b/src/pgstac/migrations/pgstac--0.6.2--0.6.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.2-0.6.3.sql rename to src/pgstac/migrations/pgstac--0.6.2--0.6.3.sql diff --git a/src/pgstac/migrations/pgstac.0.6.2.sql b/src/pgstac/migrations/pgstac--0.6.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.2.sql rename to src/pgstac/migrations/pgstac--0.6.2.sql diff --git a/src/pgstac/migrations/pgstac.0.6.3-0.6.4.sql b/src/pgstac/migrations/pgstac--0.6.3--0.6.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.3-0.6.4.sql rename to src/pgstac/migrations/pgstac--0.6.3--0.6.4.sql diff --git a/src/pgstac/migrations/pgstac.0.6.3.sql b/src/pgstac/migrations/pgstac--0.6.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.3.sql rename to src/pgstac/migrations/pgstac--0.6.3.sql diff --git a/src/pgstac/migrations/pgstac.0.6.4-0.6.5.sql b/src/pgstac/migrations/pgstac--0.6.4--0.6.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.4-0.6.5.sql rename to src/pgstac/migrations/pgstac--0.6.4--0.6.5.sql diff --git a/src/pgstac/migrations/pgstac.0.6.4.sql b/src/pgstac/migrations/pgstac--0.6.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.4.sql rename to src/pgstac/migrations/pgstac--0.6.4.sql diff --git a/src/pgstac/migrations/pgstac.0.6.5-0.6.6.sql b/src/pgstac/migrations/pgstac--0.6.5--0.6.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.5-0.6.6.sql rename to src/pgstac/migrations/pgstac--0.6.5--0.6.6.sql diff --git a/src/pgstac/migrations/pgstac.0.6.5.sql b/src/pgstac/migrations/pgstac--0.6.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.5.sql rename to src/pgstac/migrations/pgstac--0.6.5.sql diff --git a/src/pgstac/migrations/pgstac.0.6.6-0.6.7.sql b/src/pgstac/migrations/pgstac--0.6.6--0.6.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.6-0.6.7.sql rename to src/pgstac/migrations/pgstac--0.6.6--0.6.7.sql diff --git a/src/pgstac/migrations/pgstac.0.6.6.sql b/src/pgstac/migrations/pgstac--0.6.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.6.sql rename to src/pgstac/migrations/pgstac--0.6.6.sql diff --git a/src/pgstac/migrations/pgstac.0.6.7-0.6.8.sql b/src/pgstac/migrations/pgstac--0.6.7--0.6.8.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.7-0.6.8.sql rename to src/pgstac/migrations/pgstac--0.6.7--0.6.8.sql diff --git a/src/pgstac/migrations/pgstac.0.6.7.sql b/src/pgstac/migrations/pgstac--0.6.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.7.sql rename to src/pgstac/migrations/pgstac--0.6.7.sql diff --git a/src/pgstac/migrations/pgstac.0.6.8-0.6.9.sql b/src/pgstac/migrations/pgstac--0.6.8--0.6.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.8-0.6.9.sql rename to src/pgstac/migrations/pgstac--0.6.8--0.6.9.sql diff --git a/src/pgstac/migrations/pgstac.0.6.8.sql b/src/pgstac/migrations/pgstac--0.6.8.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.8.sql rename to src/pgstac/migrations/pgstac--0.6.8.sql diff --git a/src/pgstac/migrations/pgstac.0.6.9-0.6.10.sql b/src/pgstac/migrations/pgstac--0.6.9--0.6.10.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.9-0.6.10.sql rename to src/pgstac/migrations/pgstac--0.6.9--0.6.10.sql diff --git a/src/pgstac/migrations/pgstac.0.6.9.sql b/src/pgstac/migrations/pgstac--0.6.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.6.9.sql rename to src/pgstac/migrations/pgstac--0.6.9.sql diff --git a/src/pgstac/migrations/pgstac.0.7.0-0.7.1.sql b/src/pgstac/migrations/pgstac--0.7.0--0.7.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.0-0.7.1.sql rename to src/pgstac/migrations/pgstac--0.7.0--0.7.1.sql diff --git a/src/pgstac/migrations/pgstac.0.7.0.sql b/src/pgstac/migrations/pgstac--0.7.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.0.sql rename to src/pgstac/migrations/pgstac--0.7.0.sql diff --git a/src/pgstac/migrations/pgstac.0.7.1-0.7.2.sql b/src/pgstac/migrations/pgstac--0.7.1--0.7.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.1-0.7.2.sql rename to src/pgstac/migrations/pgstac--0.7.1--0.7.2.sql diff --git a/src/pgstac/migrations/pgstac.0.7.1.sql b/src/pgstac/migrations/pgstac--0.7.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.1.sql rename to src/pgstac/migrations/pgstac--0.7.1.sql diff --git a/src/pgstac/migrations/pgstac.0.7.10-0.8.0.sql b/src/pgstac/migrations/pgstac--0.7.10--0.8.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.10-0.8.0.sql rename to src/pgstac/migrations/pgstac--0.7.10--0.8.0.sql diff --git a/src/pgstac/migrations/pgstac.0.7.10.sql b/src/pgstac/migrations/pgstac--0.7.10.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.10.sql rename to src/pgstac/migrations/pgstac--0.7.10.sql diff --git a/src/pgstac/migrations/pgstac.0.7.2-0.7.3.sql b/src/pgstac/migrations/pgstac--0.7.2--0.7.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.2-0.7.3.sql rename to src/pgstac/migrations/pgstac--0.7.2--0.7.3.sql diff --git a/src/pgstac/migrations/pgstac.0.7.2.sql b/src/pgstac/migrations/pgstac--0.7.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.2.sql rename to src/pgstac/migrations/pgstac--0.7.2.sql diff --git a/src/pgstac/migrations/pgstac.0.7.3-0.7.4.sql b/src/pgstac/migrations/pgstac--0.7.3--0.7.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.3-0.7.4.sql rename to src/pgstac/migrations/pgstac--0.7.3--0.7.4.sql diff --git a/src/pgstac/migrations/pgstac.0.7.3.sql b/src/pgstac/migrations/pgstac--0.7.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.3.sql rename to src/pgstac/migrations/pgstac--0.7.3.sql diff --git a/src/pgstac/migrations/pgstac.0.7.4-0.7.5.sql b/src/pgstac/migrations/pgstac--0.7.4--0.7.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.4-0.7.5.sql rename to src/pgstac/migrations/pgstac--0.7.4--0.7.5.sql diff --git a/src/pgstac/migrations/pgstac.0.7.4.sql b/src/pgstac/migrations/pgstac--0.7.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.4.sql rename to src/pgstac/migrations/pgstac--0.7.4.sql diff --git a/src/pgstac/migrations/pgstac.0.7.5-0.7.6.sql b/src/pgstac/migrations/pgstac--0.7.5--0.7.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.5-0.7.6.sql rename to src/pgstac/migrations/pgstac--0.7.5--0.7.6.sql diff --git a/src/pgstac/migrations/pgstac.0.7.5.sql b/src/pgstac/migrations/pgstac--0.7.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.5.sql rename to src/pgstac/migrations/pgstac--0.7.5.sql diff --git a/src/pgstac/migrations/pgstac.0.7.6-0.7.7.sql b/src/pgstac/migrations/pgstac--0.7.6--0.7.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.6-0.7.7.sql rename to src/pgstac/migrations/pgstac--0.7.6--0.7.7.sql diff --git a/src/pgstac/migrations/pgstac.0.7.6.sql b/src/pgstac/migrations/pgstac--0.7.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.6.sql rename to src/pgstac/migrations/pgstac--0.7.6.sql diff --git a/src/pgstac/migrations/pgstac.0.7.7-0.7.8.sql b/src/pgstac/migrations/pgstac--0.7.7--0.7.8.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.7-0.7.8.sql rename to src/pgstac/migrations/pgstac--0.7.7--0.7.8.sql diff --git a/src/pgstac/migrations/pgstac.0.7.7.sql b/src/pgstac/migrations/pgstac--0.7.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.7.sql rename to src/pgstac/migrations/pgstac--0.7.7.sql diff --git a/src/pgstac/migrations/pgstac.0.7.8-0.7.9.sql b/src/pgstac/migrations/pgstac--0.7.8--0.7.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.8-0.7.9.sql rename to src/pgstac/migrations/pgstac--0.7.8--0.7.9.sql diff --git a/src/pgstac/migrations/pgstac.0.7.8.sql b/src/pgstac/migrations/pgstac--0.7.8.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.8.sql rename to src/pgstac/migrations/pgstac--0.7.8.sql diff --git a/src/pgstac/migrations/pgstac.0.7.9-0.7.10.sql b/src/pgstac/migrations/pgstac--0.7.9--0.7.10.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.9-0.7.10.sql rename to src/pgstac/migrations/pgstac--0.7.9--0.7.10.sql diff --git a/src/pgstac/migrations/pgstac.0.7.9.sql b/src/pgstac/migrations/pgstac--0.7.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.7.9.sql rename to src/pgstac/migrations/pgstac--0.7.9.sql diff --git a/src/pgstac/migrations/pgstac.0.8.0-0.8.1.sql b/src/pgstac/migrations/pgstac--0.8.0--0.8.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.0-0.8.1.sql rename to src/pgstac/migrations/pgstac--0.8.0--0.8.1.sql diff --git a/src/pgstac/migrations/pgstac.0.8.0.sql b/src/pgstac/migrations/pgstac--0.8.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.0.sql rename to src/pgstac/migrations/pgstac--0.8.0.sql diff --git a/src/pgstac/migrations/pgstac.0.8.1-0.8.2.sql b/src/pgstac/migrations/pgstac--0.8.1--0.8.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.1-0.8.2.sql rename to src/pgstac/migrations/pgstac--0.8.1--0.8.2.sql diff --git a/src/pgstac/migrations/pgstac.0.8.1.sql b/src/pgstac/migrations/pgstac--0.8.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.1.sql rename to src/pgstac/migrations/pgstac--0.8.1.sql diff --git a/src/pgstac/migrations/pgstac.0.8.2-0.8.3.sql b/src/pgstac/migrations/pgstac--0.8.2--0.8.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.2-0.8.3.sql rename to src/pgstac/migrations/pgstac--0.8.2--0.8.3.sql diff --git a/src/pgstac/migrations/pgstac.0.8.2.sql b/src/pgstac/migrations/pgstac--0.8.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.2.sql rename to src/pgstac/migrations/pgstac--0.8.2.sql diff --git a/src/pgstac/migrations/pgstac.0.8.3-0.8.4.sql b/src/pgstac/migrations/pgstac--0.8.3--0.8.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.3-0.8.4.sql rename to src/pgstac/migrations/pgstac--0.8.3--0.8.4.sql diff --git a/src/pgstac/migrations/pgstac.0.8.3.sql b/src/pgstac/migrations/pgstac--0.8.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.3.sql rename to src/pgstac/migrations/pgstac--0.8.3.sql diff --git a/src/pgstac/migrations/pgstac.0.8.4-0.8.5.sql b/src/pgstac/migrations/pgstac--0.8.4--0.8.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.4-0.8.5.sql rename to src/pgstac/migrations/pgstac--0.8.4--0.8.5.sql diff --git a/src/pgstac/migrations/pgstac.0.8.4.sql b/src/pgstac/migrations/pgstac--0.8.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.4.sql rename to src/pgstac/migrations/pgstac--0.8.4.sql diff --git a/src/pgstac/migrations/pgstac.0.8.5-0.9.0.sql b/src/pgstac/migrations/pgstac--0.8.5--0.9.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.5-0.9.0.sql rename to src/pgstac/migrations/pgstac--0.8.5--0.9.0.sql diff --git a/src/pgstac/migrations/pgstac.0.8.5.sql b/src/pgstac/migrations/pgstac--0.8.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.5.sql rename to src/pgstac/migrations/pgstac--0.8.5.sql diff --git a/src/pgstac/migrations/pgstac.0.8.6-0.9.0.sql b/src/pgstac/migrations/pgstac--0.8.6--0.9.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.6-0.9.0.sql rename to src/pgstac/migrations/pgstac--0.8.6--0.9.0.sql diff --git a/src/pgstac/migrations/pgstac.0.8.6-0.9.10.sql b/src/pgstac/migrations/pgstac--0.8.6--0.9.10.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.6-0.9.10.sql rename to src/pgstac/migrations/pgstac--0.8.6--0.9.10.sql diff --git a/src/pgstac/migrations/pgstac.0.8.6.sql b/src/pgstac/migrations/pgstac--0.8.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.8.6.sql rename to src/pgstac/migrations/pgstac--0.8.6.sql diff --git a/src/pgstac/migrations/pgstac.0.9.0-0.9.1.sql b/src/pgstac/migrations/pgstac--0.9.0--0.9.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.0-0.9.1.sql rename to src/pgstac/migrations/pgstac--0.9.0--0.9.1.sql diff --git a/src/pgstac/migrations/pgstac.0.9.0.sql b/src/pgstac/migrations/pgstac--0.9.0.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.0.sql rename to src/pgstac/migrations/pgstac--0.9.0.sql diff --git a/src/pgstac/migrations/pgstac.0.9.1-0.9.2.sql b/src/pgstac/migrations/pgstac--0.9.1--0.9.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.1-0.9.2.sql rename to src/pgstac/migrations/pgstac--0.9.1--0.9.2.sql diff --git a/src/pgstac/migrations/pgstac.0.9.1.sql b/src/pgstac/migrations/pgstac--0.9.1.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.1.sql rename to src/pgstac/migrations/pgstac--0.9.1.sql diff --git a/src/pgstac/migrations/pgstac.0.9.10-0.9.11.sql b/src/pgstac/migrations/pgstac--0.9.10--0.9.11.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.10-0.9.11.sql rename to src/pgstac/migrations/pgstac--0.9.10--0.9.11.sql diff --git a/src/pgstac/migrations/pgstac.0.9.10.sql b/src/pgstac/migrations/pgstac--0.9.10.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.10.sql rename to src/pgstac/migrations/pgstac--0.9.10.sql diff --git a/src/pgstac/migrations/pgstac.0.9.11-unreleased.sql b/src/pgstac/migrations/pgstac--0.9.11--unreleased.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.11-unreleased.sql rename to src/pgstac/migrations/pgstac--0.9.11--unreleased.sql diff --git a/src/pgstac/migrations/pgstac.0.9.11.sql b/src/pgstac/migrations/pgstac--0.9.11.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.11.sql rename to src/pgstac/migrations/pgstac--0.9.11.sql diff --git a/src/pgstac/migrations/pgstac.0.9.2-0.9.3.sql b/src/pgstac/migrations/pgstac--0.9.2--0.9.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.2-0.9.3.sql rename to src/pgstac/migrations/pgstac--0.9.2--0.9.3.sql diff --git a/src/pgstac/migrations/pgstac.0.9.2.sql b/src/pgstac/migrations/pgstac--0.9.2.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.2.sql rename to src/pgstac/migrations/pgstac--0.9.2.sql diff --git a/src/pgstac/migrations/pgstac.0.9.3-0.9.4.sql b/src/pgstac/migrations/pgstac--0.9.3--0.9.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.3-0.9.4.sql rename to src/pgstac/migrations/pgstac--0.9.3--0.9.4.sql diff --git a/src/pgstac/migrations/pgstac.0.9.3.sql b/src/pgstac/migrations/pgstac--0.9.3.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.3.sql rename to src/pgstac/migrations/pgstac--0.9.3.sql diff --git a/src/pgstac/migrations/pgstac.0.9.4-0.9.5.sql b/src/pgstac/migrations/pgstac--0.9.4--0.9.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.4-0.9.5.sql rename to src/pgstac/migrations/pgstac--0.9.4--0.9.5.sql diff --git a/src/pgstac/migrations/pgstac.0.9.4.sql b/src/pgstac/migrations/pgstac--0.9.4.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.4.sql rename to src/pgstac/migrations/pgstac--0.9.4.sql diff --git a/src/pgstac/migrations/pgstac.0.9.5-0.9.6.sql b/src/pgstac/migrations/pgstac--0.9.5--0.9.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.5-0.9.6.sql rename to src/pgstac/migrations/pgstac--0.9.5--0.9.6.sql diff --git a/src/pgstac/migrations/pgstac.0.9.5.sql b/src/pgstac/migrations/pgstac--0.9.5.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.5.sql rename to src/pgstac/migrations/pgstac--0.9.5.sql diff --git a/src/pgstac/migrations/pgstac.0.9.6-0.9.7.sql b/src/pgstac/migrations/pgstac--0.9.6--0.9.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.6-0.9.7.sql rename to src/pgstac/migrations/pgstac--0.9.6--0.9.7.sql diff --git a/src/pgstac/migrations/pgstac.0.9.6.sql b/src/pgstac/migrations/pgstac--0.9.6.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.6.sql rename to src/pgstac/migrations/pgstac--0.9.6.sql diff --git a/src/pgstac/migrations/pgstac.0.9.7-0.9.8.sql b/src/pgstac/migrations/pgstac--0.9.7--0.9.8.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.7-0.9.8.sql rename to src/pgstac/migrations/pgstac--0.9.7--0.9.8.sql diff --git a/src/pgstac/migrations/pgstac.0.9.7.sql b/src/pgstac/migrations/pgstac--0.9.7.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.7.sql rename to src/pgstac/migrations/pgstac--0.9.7.sql diff --git a/src/pgstac/migrations/pgstac.0.9.8-0.9.9.sql b/src/pgstac/migrations/pgstac--0.9.8--0.9.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.8-0.9.9.sql rename to src/pgstac/migrations/pgstac--0.9.8--0.9.9.sql diff --git a/src/pgstac/migrations/pgstac.0.9.8.sql b/src/pgstac/migrations/pgstac--0.9.8.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.8.sql rename to src/pgstac/migrations/pgstac--0.9.8.sql diff --git a/src/pgstac/migrations/pgstac.0.9.9-0.9.10.sql b/src/pgstac/migrations/pgstac--0.9.9--0.9.10.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.9-0.9.10.sql rename to src/pgstac/migrations/pgstac--0.9.9--0.9.10.sql diff --git a/src/pgstac/migrations/pgstac.0.9.9.sql b/src/pgstac/migrations/pgstac--0.9.9.sql similarity index 100% rename from src/pgstac/migrations/pgstac.0.9.9.sql rename to src/pgstac/migrations/pgstac--0.9.9.sql diff --git a/src/pgstac/migrations/pgstac.unreleased.sql b/src/pgstac/migrations/pgstac--unreleased.sql similarity index 100% rename from src/pgstac/migrations/pgstac.unreleased.sql rename to src/pgstac/migrations/pgstac--unreleased.sql diff --git a/src/pgstac/pyproject.toml b/src/pgstac/pyproject.toml new file mode 100644 index 00000000..6d74a078 --- /dev/null +++ b/src/pgstac/pyproject.toml @@ -0,0 +1,6 @@ +[tool.pgpkg] +project_name = "pgstac" +prefix = "pgstac" +sql_dir = "sql" +migrations_dir = "migrations" +version_source = "pgstac_migrate.version_source:PgstacVersionSource" diff --git a/src/pypgstac/pyproject.toml b/src/pypgstac/pyproject.toml index af375097..d59703f2 100644 --- a/src/pypgstac/pyproject.toml +++ b/src/pypgstac/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "fire>=0.7.0", "hydraters>=0.1.0", "orjson>=3.11.0", + "pgstac-migrate>=0.9.11.dev0,<0.10", "plpygis>=0.5.0", "pydantic>=2.10,<3", "pydantic-settings>=2,<3", @@ -47,7 +48,7 @@ dev = [ "pre-commit==3.5.0", ] psycopg = ["psycopg[binary]>=3.1.0", "psycopg-pool>=3.1.0"] -migrations = ["psycopg2-binary", "migra"] +migrations = [] docs = [ "jupyter", "pandas", @@ -121,3 +122,6 @@ match = "(?!test).*.py" [tool.pytest.ini_options] addopts = "-vv --benchmark-skip" + +[tool.uv.sources] +pgstac-migrate = { path = "../pgstac-migrate", editable = true } diff --git a/src/pypgstac/src/pypgstac/migrate.py b/src/pypgstac/src/pypgstac/migrate.py index 2dbb97cd..e10e8858 100644 --- a/src/pypgstac/src/pypgstac/migrate.py +++ b/src/pypgstac/src/pypgstac/migrate.py @@ -1,22 +1,25 @@ -"""Utilities to help migrate pgstac schema.""" +"""Compatibility wrappers over pgstac-migrate.""" import glob -import logging import os -import re from collections import defaultdict from collections.abc import Iterator -from typing import Any, cast +from importlib import import_module -from smart_open import open - -from . import __version__ from .db import PgstacDB +from .version import __version__ + +MIGRATION_PREFIX = "pgstac--" + -dirname = os.path.dirname(__file__) -migrations_dir = os.path.join(dirname, "migrations") +def base_migration_filename(version: str) -> str: + """Return the canonical base migration filename for a version.""" + return f"{MIGRATION_PREFIX}{version}.sql" -logger = logging.getLogger(__name__) + +def incremental_migration_filename(from_version: str, to_version: str) -> str: + """Return the canonical incremental migration filename for a version hop.""" + return f"{MIGRATION_PREFIX}{from_version}--{to_version}.sql" class MigrationPath: @@ -38,10 +41,11 @@ def __init__(self, path: str, f: str, t: str) -> None: def parse_filename(self, filename: str) -> list[str]: """Get version numbers from filename.""" filename = os.path.splitext(os.path.basename(filename))[0].replace( - "pgstac.", + MIGRATION_PREFIX, "", + 1, ) - return filename.split("-") + return filename.split("--") def get_files(self) -> Iterator[str]: """Find all migration files available.""" @@ -87,29 +91,24 @@ def migrations(self) -> list[str]: f"Could not determine path to get from {self.f} to {self.t}.", ) if len(path) == 1: - return [f"pgstac.{path[0]}.sql"] + return [] files = [] - for idx in range(len(path) - 1): - f = f"pgstac.{path[idx]}-{path[idx + 1]}.sql" - f = f.replace("--init", "") - files.append(f"pgstac.{path[idx]}-{path[idx + 1]}.sql") + start_idx = 0 + if path[0] == "init": + files.append(base_migration_filename(path[1])) + start_idx = 1 + for idx in range(start_idx, len(path) - 1): + files.append(incremental_migration_filename(path[idx], path[idx + 1])) return files -def get_sql(file: str) -> str: - """Get sql from a file as a string.""" - sqlstrs = [] - file = re.sub("[0-9]+[.][0-9]+[.][0-9]+-dev", "unreleased", file) - fp = os.path.join(migrations_dir, file) - file_handle: Any = open(fp) - - with file_handle as fd: - sqlstrs.extend(fd.readlines()) - return "\n".join(sqlstrs) +def _pgstac_migrate_api(): + """Import the pgstac-migrate API lazily for editor and source-tree compatibility.""" + return import_module("pgstac_migrate.api") class Migrate: - """Utilities for migrating pgstac database.""" + """Compatibility wrapper around pgstac-migrate.""" def __init__(self, db: PgstacDB, schema: str = "pgstac"): """Prepare for migration.""" @@ -118,63 +117,16 @@ def __init__(self, db: PgstacDB, schema: str = "pgstac"): def run_migration(self, toversion: str | None = None) -> str: """Migrate a pgstac database to current version.""" - if toversion is None: - toversion = __version__ - files = [] - if re.search(r"-dev$", toversion): - logger.info("using unreleased version") - toversion = "unreleased" - - major, minor, patch = tuple( - map( - int, - [ - self.db.pg_version[i : i + 2] - for i in range(0, len(self.db.pg_version), 2) - ], - ), + if self.schema != "pgstac": + raise ValueError("pgstac-migrate only supports the pgstac schema.") + + self.db.disconnect() + result = _pgstac_migrate_api().migrate( + target=toversion or __version__, + conninfo=self.db.dsn or None, ) - logger.info(f"Migrating PgSTAC on PostgreSQL Version {major}.{minor}.{patch}") - oldversion = self.db.version - if oldversion == toversion: - logger.info(f"Target database already at version: {toversion}") - return toversion - if oldversion is None: - logger.info(f"No pgstac version set, installing {toversion} from scratch.") - files.append(os.path.join(migrations_dir, f"pgstac.{toversion}.sql")) - else: - logger.info(f"Migrating from {oldversion} to {toversion}.") - m = MigrationPath(migrations_dir, oldversion, toversion) - files = m.migrations() - - if len(files) < 1: - raise Exception("Could not find migration files") - - conn = self.db.connect() - - with conn.cursor() as cur: - conn.autocommit = False - for file in files: - logger.debug(f"Running migration file {file}.") - migration_sql = get_sql(file) - # Migration SQL is loaded from trusted local migration files. - cur.execute(cast(Any, migration_sql)) - logger.debug(cur.statusmessage) - logger.debug(cur.rowcount) - - logger.debug(f"Database migrated to {toversion}") - - newversion = self.db.version - if conn is not None: - if newversion == toversion: - conn.commit() - else: - conn.rollback() - raise Exception( - "Migration failed, database rolled back to previous state.", - ) - - logger.debug(f"New Version: {newversion}") - if newversion is None: - raise Exception("Migration failed to report a new version.") - return newversion + self.db.disconnect() + + if result.final_version is None: + raise RuntimeError("Migration failed to report a new version.") + return result.final_version diff --git a/src/pypgstac/tests/test_migrate.py b/src/pypgstac/tests/test_migrate.py new file mode 100644 index 00000000..a5c42cc4 --- /dev/null +++ b/src/pypgstac/tests/test_migrate.py @@ -0,0 +1,39 @@ +"""Unit tests for migration filename handling.""" + +from pathlib import Path + +from pypgstac.migrate import ( + MigrationPath, + base_migration_filename, + incremental_migration_filename, +) + + +def test_canonical_migration_filename_helpers() -> None: + assert base_migration_filename("0.9.11") == "pgstac--0.9.11.sql" + assert ( + incremental_migration_filename("0.9.10", "0.9.11") + == "pgstac--0.9.10--0.9.11.sql" + ) + + +def test_parse_filename_uses_canonical_layout() -> None: + migration_path = MigrationPath("/tmp", "0.9.10", "0.9.11") + + assert migration_path.parse_filename("/tmp/pgstac--0.9.11.sql") == ["0.9.11"] + assert migration_path.parse_filename("/tmp/pgstac--0.9.10--0.9.11.sql") == [ + "0.9.10", + "0.9.11", + ] + + +def test_migration_path_returns_canonical_filenames(tmp_path: Path) -> None: + (tmp_path / "pgstac--0.9.11.sql").write_text("-- base\n") + (tmp_path / "pgstac--0.9.10.sql").write_text("-- from\n") + (tmp_path / "pgstac--0.9.10--0.9.11.sql").write_text("-- incremental\n") + + fresh_install = MigrationPath(str(tmp_path), "init", "0.9.11") + assert fresh_install.migrations() == ["pgstac--0.9.11.sql"] + + upgrade = MigrationPath(str(tmp_path), "0.9.10", "0.9.11") + assert upgrade.migrations() == ["pgstac--0.9.10--0.9.11.sql"] diff --git a/src/pypgstac/tests/test_migrate_wrapper.py b/src/pypgstac/tests/test_migrate_wrapper.py new file mode 100644 index 00000000..4f0dc01e --- /dev/null +++ b/src/pypgstac/tests/test_migrate_wrapper.py @@ -0,0 +1,60 @@ +from importlib import import_module +from types import SimpleNamespace + +from pypgstac.db import PgstacDB +from pypgstac.migrate import Migrate + + +def test_run_migration_delegates_to_pgstac_migrate(monkeypatch) -> None: + migrate_module = import_module("pypgstac.migrate") + db = PgstacDB(dsn="postgresql:///example") + captured: dict[str, object] = {} + disconnect_calls: list[None] = [] + + def fake_disconnect() -> None: + disconnect_calls.append(None) + + def fake_migrate(**kwargs): + captured.update(kwargs) + return SimpleNamespace(final_version="0.9.11") + + monkeypatch.setattr(db, "disconnect", fake_disconnect) + monkeypatch.setattr( + migrate_module, + "_pgstac_migrate_api", + lambda: SimpleNamespace(migrate=fake_migrate), + ) + + final_version = Migrate(db).run_migration("0.9.11-dev") + + assert final_version == "0.9.11" + assert captured == { + "target": "0.9.11-dev", + "conninfo": "postgresql:///example", + } + assert len(disconnect_calls) == 2 + + +def test_run_migration_defaults_to_package_version(monkeypatch) -> None: + migrate_module = import_module("pypgstac.migrate") + db = PgstacDB(dsn="") + captured: dict[str, object] = {} + + monkeypatch.setattr(db, "disconnect", lambda: None) + monkeypatch.setattr( + migrate_module, + "_pgstac_migrate_api", + lambda: SimpleNamespace( + migrate=lambda **kwargs: ( + captured.update(kwargs) or SimpleNamespace(final_version="unreleased") + ), + ), + ) + + final_version = Migrate(db).run_migration() + + assert final_version == "unreleased" + assert captured == { + "target": "0.9.11-dev", + "conninfo": None, + } From a5a2b5ce675bb94b449c8b0df6bd3781a934b450 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 5 May 2026 17:05:40 -0500 Subject: [PATCH 2/8] chore: switch pgpkg workflows to published packages --- .github/copilot-instructions.md | 3 + .github/instructions/scripts.instructions.md | 4 + .github/workflows/release.yml | 26 ++++++ AGENTS.md | 10 ++- CHANGELOG.md | 1 + CLAUDE.md | 37 +++++---- scripts/container-scripts/makemigration | 83 ++++++++------------ scripts/container-scripts/stageversion | 30 +++++-- scripts/makemigration | 2 +- scripts/runinpypgstac | 16 +++- 10 files changed, 132 insertions(+), 80 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9e307001..deda841e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,3 +2,6 @@ See `CLAUDE.md` for comprehensive project instructions, architecture, and workflows. See `AGENTS.md` for specialized agent definitions (sql-developer, migration-engineer, loader-developer). + +When migration workflows, script entry points, or developer commands change, update `CLAUDE.md`, `AGENTS.md`, and any relevant files under `.github/instructions/` in the same change. +Use `uv` for Python execution, dependency installation, and standalone helper scripts; avoid direct `pip` commands. diff --git a/.github/instructions/scripts.instructions.md b/.github/instructions/scripts.instructions.md index 6cec1b70..40e7aa6f 100644 --- a/.github/instructions/scripts.instructions.md +++ b/.github/instructions/scripts.instructions.md @@ -7,6 +7,10 @@ applyTo: "scripts/**" See CLAUDE.md "Development Workflow" for usage. All scripts require the Docker compose environment. - `runinpypgstac` is the foundation — most scripts delegate to it +- `runinpypgstac` uses the published-package path by default; set `PGPKG_LOCAL_REPO_DIR` to mount a local `pgpkg` checkout at `/pgpkg` when you need an override - `scripts/container-scripts/` contains the in-container script payload copied into the pypgstac image; keep host wrappers in `scripts/` - `stageversion` modifies version files AND generates migrations — see CLAUDE.md "Migration Process" +- `scripts/container-scripts/stageversion` and `scripts/container-scripts/makemigration` now shell through `pgpkg` inside the container rather than assembling/diffing SQL directly +- Set `PGPKG_LOCAL_REPO_DIR` on the host when you need to force a local pgpkg checkout for `stageversion`, `makemigration`, or related container-script testing +- Tagged releases run `.github/workflows/release.yml`, which publishes both `pypgstac` and `pgstac-migrate` to PyPI via the GitHub `pypi` environment; PyPI trusted publishers must exist for both projects - DO NOT run `stageversion` without understanding its side effects diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de2a19fe..fe02e0c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -226,3 +226,29 @@ jobs: uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 with: packages-dir: /home/runner/work/pgstac/pgstac/src/pypgstac/dist + + releasepgstacmigratetopypi: + name: Release pgstac-migrate to PyPI + runs-on: ubuntu-latest + permissions: + id-token: write + environment: + name: pypi + url: https://pypi.org/p/pgstac-migrate + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.x" + - name: Install build + working-directory: /home/runner/work/pgstac/pgstac/src/pgstac-migrate + run: python -m pip install build + - name: Build + working-directory: /home/runner/work/pgstac/pgstac/src/pgstac-migrate + run: python -m build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 + with: + packages-dir: /home/runner/work/pgstac/pgstac/src/pgstac-migrate/dist diff --git a/AGENTS.md b/AGENTS.md index bba2d143..73568f98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,9 +23,13 @@ Migration specialist for PgSTAC. See CLAUDE.md "Migration Process" for full work ### Quick Reference 1. Edit SQL in `src/pgstac/sql/*.sql` -2. `scripts/stageversion VERSION` → generates base + incremental `.staged` migration -3. Review `.staged` file (watch for DROPs, unsafe ALTERs, missing `CREATE OR REPLACE`) -4. Remove `.staged` suffix → `scripts/test --migrations` +2. `src/pgstac/pyproject.toml` is the `pgpkg` project config for the SQL + migrations tree +3. `uv run --directory src/pgstac-migrate pgstac-migrate info|versions|plan` inspects the baked migration artifact during wrapper work +4. `uv run --directory src/pypgstac pypgstac migrate -- --help` remains a backwards-compatible wrapper over `pgstac-migrate`; put new runtime migration behavior in `src/pgstac-migrate/`, not `src/pypgstac/` +5. `scripts/stageversion VERSION` → generates canonical `pgstac--VERSION.sql` plus an incremental `.staged` migration; set `PGPKG_LOCAL_REPO_DIR` when `stageversion` or `makemigration` should run against a local pgpkg checkout. The Docker-backed flow mounts that override at `/pgpkg` and exports `PGPKG_REPO_DIR` to the container scripts. +6. Review `.staged` file (watch for DROPs, unsafe ALTERs, missing `CREATE OR REPLACE`) +7. Remove `.staged` suffix → `scripts/test --migrations` +8. Tagged releases publish both `pypgstac` and `pgstac-migrate` to PyPI from `.github/workflows/release.yml`; keep the PyPI trusted publisher registration aligned with the `pypi` environment and workflow path ### Review Checklist diff --git a/CHANGELOG.md b/CHANGELOG.md index 953a4d2d..f4fcc997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ecosystems with grouped update policies). ### Changed +- Tagged releases now publish the new `pgstac-migrate` package to PyPI alongside `pypgstac` via trusted publishing in `.github/workflows/release.yml`. - In-container helper scripts moved from `docker/pypgstac/bin/` to `scripts/container-scripts/`; container `PATH` updated accordingly. - `docker/pgstac/Dockerfile` and `docker/pypgstac/Dockerfile` base images updated from diff --git a/CLAUDE.md b/CLAUDE.md index 4849c58a..91181424 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,10 +11,12 @@ PgSTAC is a PostgreSQL extension (SQL functions + schema) for Spatio-Temporal As ## Architecture ``` +src/pgstac/pyproject.toml ← pgpkg project config for SQL + migration artifacts src/pgstac/sql/ ← ALL SQL source files (edit ONLY here) src/pgstac/pgstac.sql ← Assembled output (DO NOT edit directly) src/pgstac/migrations/ ← Base + incremental migration files src/pgstac/tests/ ← PGTap and basic SQL tests +src/pgstac-migrate/ ← Standalone pgstac-migrate wrapper package + baked artifact src/pypgstac/src/pypgstac/ ← Python package source src/pypgstac/tests/ ← pytest tests scripts/ ← Host-facing entrypoint scripts @@ -95,6 +97,8 @@ All tests run inside Docker via `scripts/runinpypgstac`. Use `--build` to rebuil - **pgstac** container: PostgreSQL 17 + PostGIS 3 + extensions, port 5439→5432 - **pypgstac** container: Python + Rust build tools, runs scripts +- `scripts/runinpypgstac` uses the published-package path by default; set `PGPKG_LOCAL_REPO_DIR` to mount a local `pgpkg` checkout at `/pgpkg` and export `PGPKG_REPO_DIR` when `stageversion` or `makemigration` should run against a local checkout +- When no local checkout is mounted, the in-container `stageversion` / `makemigration` helpers resolve `pgpkg>=0.1,<0.2` from PyPI with `uv run --no-project --with ...` - Credentials: `username` / `password`, database: `postgis` ## Migration Process @@ -108,21 +112,20 @@ scripts/stageversion 0.9.11 This runs inside Docker and: 1. Removes old `*unreleased*` migration files 2. Writes `SELECT set_version('0.9.11');` to `999_version.sql` -3. Concatenates all `sql/*.sql` → `migrations/pgstac.0.9.11.sql` (base migration) -4. Copies the base migration to `pgstac.sql` +3. Runs `pgpkg stageversion` against `src/pgstac/pyproject.toml` → `migrations/pgstac--0.9.11.sql` +4. Uses `--also-write` to keep `pgstac.sql` synchronized with the latest base migration 5. Updates `version.py` and `pyproject.toml` version strings -6. Runs `makemigration -f 0.9.10 -t 0.9.11` to generate incremental migration +6. Runs `makemigration -f 0.9.10 -t 0.9.11` to generate the wrapped incremental migration via `pgpkg` ### How makemigration Works -`makemigration` (copied from `scripts/container-scripts/makemigration` into the image) generates incremental migrations by diffing schemas: +`makemigration` (copied from `scripts/container-scripts/makemigration` into the image) now prefers a local checkout via `PGPKG_REPO_DIR`, otherwise it resolves the pinned published package with `uv run --no-project --with "pgpkg[diff]>=0.1,<0.2" pgpkg makemigration`: -1. Creates two temp databases: `migra_from`, `migra_to` -2. Loads old base migration into `migra_from` -3. Loads new base migration into `migra_to` -4. Runs `migra --schema pgstac --unsafe` to calculate the SQL diff -5. Wraps the diff with `000_idempotent_pre.sql`, `998_idempotent_post.sql`, and `set_version()` -6. Output: `migrations/pgstac.0.9.10-0.9.11.sql` +1. Uses `src/pgstac/pyproject.toml` to locate the canonical staged base files +2. Uses `results.temporary_local_db` via `pgpkg` to diff the source and target staged bases +3. Prepends `000_idempotent_pre.sql` +4. Appends `998_idempotent_post.sql` and `SELECT set_version(...)` +5. Writes `migrations/pgstac--0.9.10--0.9.11.sql` **Important**: The generated migration is created with a `.staged` suffix. You MUST: 1. Review the `.staged` file for correctness @@ -132,11 +135,17 @@ This runs inside Docker and: ### Running Migrations ```bash -pypgstac migrate # Migrate to current pypgstac version -pypgstac migrate --toversion 0.9.10 # Migrate to specific version +pypgstac migrate # Backwards-compatible wrapper over pgstac-migrate +pypgstac migrate --toversion 0.9.10 # Backwards-compatible wrapper over pgstac-migrate +uv run --directory src/pgstac-migrate pgstac-migrate build-artifact +uv run --directory src/pgstac-migrate pgstac-migrate info +uv run --directory src/pgstac-migrate pgstac-migrate versions ``` -The `Migrate` class (in `migrate.py`) builds a directed graph of all available migration files and uses BFS to find the shortest path from the current DB version to the target. +`pgstac-migrate` owns runtime migration planning and apply logic. `pypgstac migrate` delegates to the same Python API for backwards compatibility and does not execute source-tree SQL files directly. +The source-tree `pgstac-migrate` package prefers the baked artifact at `src/pgstac-migrate/src/pgstac_migrate/migrations.tar.zst` and rebuilds it from the source tree when that file is missing. +`src/pgstac-migrate/pyproject.toml` resolves `pgpkg>=0.1,<0.2` from PyPI. The standalone `src/pgstac-migrate/scripts/build_artifact.py` helper does not use that lockfile; it carries its own inline `pgpkg>=0.1,<0.2` dependency. +`src/pypgstac/pyproject.toml` keeps a local `[tool.uv.sources]` override to the sibling `../pgstac-migrate` project so `uv run --directory src/pypgstac ...` resolves the wrapper stack from the source tree, while `pgpkg` resolves from PyPI. In the Docker-backed dev flow, `scripts/runinpypgstac` can mount a local pgpkg checkout at `/pgpkg` and export `PGPKG_REPO_DIR` for container-script testing. ## Testing Details @@ -178,7 +187,7 @@ Tests create `pgstac_test_db_template` from `pgstac.sql`, then clone it per test 5. Copy updated `CHANGELOG.md` to `docs/src/release-notes.md` (keep identical) 6. Create PR, merge 7. `git tag vVERSION && git push origin vVERSION` -8. CI publishes to PyPI + ghcr.io +8. CI publishes `pypgstac` and `pgstac-migrate` to PyPI plus the ghcr.io images (requires trusted publishers for both PyPI projects on `.github/workflows/release.yml` with the `pypi` environment) ## Common Patterns diff --git a/scripts/container-scripts/makemigration b/scripts/container-scripts/makemigration index bb24831e..ea70b08a 100755 --- a/scripts/container-scripts/makemigration +++ b/scripts/container-scripts/makemigration @@ -36,7 +36,7 @@ Options: -f, --from VERSION Source base version. -t, --to VERSION Target base version. -o, --overwrite Replace an existing migration file. - -d, --debug Print the generated migra SQL before wrapping it. + -d, --debug Print the generated wrapped migration SQL after creation. -h, --help Show this help text. Environment: @@ -44,6 +44,7 @@ Environment: PGSTAC_TO_VERSION Default target version. PGSTAC_OVERWRITE Set to 1 to imply --overwrite. PGSTAC_DEBUG Set to 1 to imply --debug. + PGPKG_REPO_DIR Optional local pgpkg checkout to use instead of the installed package. EOF exit 0 ;; @@ -72,12 +73,27 @@ fi BASEDIR=$SRCDIR +PGSTACDIR=$BASEDIR/pgstac +PGPKGDIR=${PGPKG_REPO_DIR:-} PYPGSTACDIR=$BASEDIR/pypgstac MIGRATIONSDIR=$BASEDIR/pgstac/migrations SQLDIR=$BASEDIR/pgstac/sql +function run_pgpkg_makemigration() { + if [[ -n "$PGPKGDIR" ]]; then + if [[ ! -d "$PGPKGDIR" ]]; then + echo "PGPKG_REPO_DIR points to $PGPKGDIR but no checkout exists there." >&2 + exit 1 + fi + uv run --directory "$PGPKGDIR" --extra diff "$@" + return + fi + + uv run --no-project --with "pgpkg[diff]>=0.1,<0.2" "$@" +} + # Check if from SQL file exists -FROMSQL=$MIGRATIONSDIR/pgstac.$FROM.sql +FROMSQL=$MIGRATIONSDIR/pgstac--$FROM.sql if [ -f $FROMSQL ]; then echo "Migrating From: $FROMSQL" else @@ -86,7 +102,7 @@ else fi # Check if to SQL file exists -TOSQL=$MIGRATIONSDIR/pgstac.$TO.sql +TOSQL=$MIGRATIONSDIR/pgstac--$TO.sql if [ -f $TOSQL ]; then echo "Migrating To: $TOSQL" else @@ -94,71 +110,34 @@ else exit 1 fi -MIGRATIONSQL=$MIGRATIONSDIR/pgstac.$FROM-$TO.sql +MIGRATIONSQL=$MIGRATIONSDIR/pgstac--$FROM--$TO.sql if [[ -f "$MIGRATIONSQL" ]]; then if [[ "$OVERWRITE" != 1 ]]; then echo "ERROR: $MIGRATIONSQL already exists. Use --overwrite to replace." >&2 exit 1 - else - echo "Removing existing $MIGRATIONSQL" - rm $MIGRATIONSQL fi else echo "Creating $MIGRATIONSQL" fi -pg_isready -t 10 -# Create Databases to inspect to create migration -psql -q >/dev/null 2>&1 <<-'EOSQL' - DROP DATABASE IF EXISTS migra_from; - CREATE DATABASE migra_from; - DROP DATABASE IF EXISTS migra_to; - CREATE DATABASE migra_to; -EOSQL - -TODBURL="postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST:-localhost}:${PGPORT:-5432}/migra_to" -FROMDBURL="postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST:-localhost}:${PGPORT:-5432}/migra_from" - -# Make sure to clean up migra databases -function drop_migra_dbs(){ -psql -q >/dev/null 2>&1 <<-'EOSQL' - DROP DATABASE IF EXISTS migra_from; - DROP DATABASE IF EXISTS migra_to; -EOSQL -} - -trap drop_migra_dbs 0 2 3 15 - echo "Creating Migration from $FROM to $TO" -# Install From into Database -psql -q -X -1 -v ON_ERROR_STOP=1 -v CLIENT_MIN_MESSAGES=WARNING -f $FROMSQL $FROMDBURL >/dev/null || exit 1; - -# Install To into Database -psql -q -X -1 -v ON_ERROR_STOP=1 -v CLIENT_MIN_MESSAGES=WARNING -f $TOSQL $TODBURL >/dev/null || exit 1; - +BASE_URL="postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST:-localhost}:${PGPORT:-5432}/postgres" +run_pgpkg_makemigration pgpkg makemigration \ + --project-root "$PGSTACDIR" \ + --from "$FROM" \ + --to "$TO" \ + --output "$MIGRATIONSQL" \ + --prepend-file "$SQLDIR/000_idempotent_pre.sql" \ + --append-file "$SQLDIR/998_idempotent_post.sql" \ + --append-sql "SELECT set_version('${TO}');" \ + --base-url "$BASE_URL" -# Calculate the migration -MIGRATION=$(mktemp) -trap "rm $MIGRATION" 0 2 3 15 - -migra --schema pgstac --unsafe $FROMDBURL $TODBURL >$MIGRATION if [[ $DEBUG == 1 ]]; then echo "*************" - cat $MIGRATION + cat "$MIGRATIONSQL" echo "*************" fi -# Append wrapper around created migration with idempotent and transaction statements - -echo "SET client_min_messages TO WARNING;" >$MIGRATIONSQL -echo "SET SEARCH_PATH to pgstac, public;" >>$MIGRATIONSQL -cat $SQLDIR/000_idempotent_pre.sql >>$MIGRATIONSQL -echo "-- BEGIN migra calculated SQL" >>$MIGRATIONSQL -cat $MIGRATION >>$MIGRATIONSQL -echo "-- END migra calculated SQL" >>$MIGRATIONSQL -cat $SQLDIR/998_idempotent_post.sql >>$MIGRATIONSQL -echo "SELECT set_version('${TO}');" >>$MIGRATIONSQL - echo "Migration created at $MIGRATIONSQL." exit 0 diff --git a/scripts/container-scripts/stageversion b/scripts/container-scripts/stageversion index fcac770c..075892ea 100755 --- a/scripts/container-scripts/stageversion +++ b/scripts/container-scripts/stageversion @@ -3,10 +3,25 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SRCDIR=${PGSTAC_REPO_DIR:-/opt/src} cd $SRCDIR BASEDIR=$SRCDIR +PGSTACDIR=$BASEDIR/pgstac +PGPKGDIR=${PGPKG_REPO_DIR:-} SQLDIR=$BASEDIR/pgstac/sql PYPGSTACDIR=$BASEDIR/pypgstac MIGRATIONSDIR=$BASEDIR/pgstac/migrations +function run_pgpkg() { + if [[ -n "$PGPKGDIR" ]]; then + if [[ ! -d "$PGPKGDIR" ]]; then + echo "PGPKG_REPO_DIR points to $PGPKGDIR but no checkout exists there." >&2 + exit 1 + fi + uv run --directory "$PGPKGDIR" "$@" + return + fi + + uv run --no-project --with "pgpkg>=0.1,<0.2" "$@" +} + function usage() { cat <999_version.sql -cat *.sql >$MIGRATIONSDIR/pgstac.${VERSION}.sql -cd $BASEDIR/pgstac - -# make the base pgstac.sql a symbolic link to the most recent version -rm pgstac.sql -cp migrations/pgstac.${VERSION}.sql pgstac.sql +run_pgpkg pgpkg stageversion "$VERSION" \ + --project-root "$PGSTACDIR" \ + --also-write "$PGSTACDIR/pgstac.sql" +cd $PGSTACDIR # Update the version number in the appropriate places [[ $VERSION == 'unreleased' ]] && PYVERSION="${OLDVERSION}-dev" || PYVERSION="$VERSION" diff --git a/scripts/makemigration b/scripts/makemigration index 64572a5c..d81528a0 100755 --- a/scripts/makemigration +++ b/scripts/makemigration @@ -12,7 +12,7 @@ Options: -f, --from VERSION Source base version. -t, --to VERSION Target base version. -o, --overwrite Replace an existing migration file. - -d, --debug Print the generated migra SQL before wrapping it. + -d, --debug Print the generated wrapped migration SQL after creation. --build-policy POLICY One of: always, missing, never. Default: always. -h, --help Show this help text. diff --git a/scripts/runinpypgstac b/scripts/runinpypgstac index 8e978725..ab26d343 100755 --- a/scripts/runinpypgstac +++ b/scripts/runinpypgstac @@ -57,6 +57,18 @@ function wait_for_pgstac() { CONTAINER_ARGS=() BUILD_POLICY="${PGSTAC_BUILD_POLICY:-always}" +LOCAL_PGPKG_DIR="${PGPKG_LOCAL_REPO_DIR:-}" +COMPOSE_RUN_ARGS=() + +if [[ -n "${PGPKG_LOCAL_REPO_DIR:-}" && ! -d "$LOCAL_PGPKG_DIR" ]]; then + echo "PGPKG_LOCAL_REPO_DIR points to $LOCAL_PGPKG_DIR but no checkout exists there." >&2 + exit 1 +fi + +if [[ -n "$LOCAL_PGPKG_DIR" && -d "$LOCAL_PGPKG_DIR" ]]; then + COMPOSE_RUN_ARGS+=("-e" "PGPKG_REPO_DIR=/pgpkg") + COMPOSE_RUN_ARGS+=("-v" "$LOCAL_PGPKG_DIR:/pgpkg") +fi while [[ $# -gt 0 ]]; do case "$1" in @@ -129,7 +141,7 @@ fi if [[ $CPFILES == 1 ]]; then echo "Running pypgstac worker" - WORKER_ID=$(docker compose run -d --rm pypgstac tail -f /dev/null) + WORKER_ID=$(docker compose run -d --rm "${COMPOSE_RUN_ARGS[@]}" pypgstac tail -f /dev/null) echo "Executing ${CONTAINER_ARGS[@]} in pypgstac worker" docker exec "$WORKER_ID" "${CONTAINER_ARGS[@]}" echo "copying datafiles to host" @@ -138,7 +150,7 @@ if [[ $CPFILES == 1 ]]; then docker kill "$WORKER_ID" >/dev/null else echo "Running ${CONTAINER_ARGS[@]} in pypgstacworker" - docker compose run -T --rm pypgstac "${CONTAINER_ARGS[@]}" + docker compose run -T --rm "${COMPOSE_RUN_ARGS[@]}" pypgstac "${CONTAINER_ARGS[@]}" fi JOBEXITCODE=$? [[ $PGSTAC_RUNNING == "" ]] && docker compose stop pgstac From 3e853c6467ff6df86427a25bee565254f9058743 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 5 May 2026 17:06:23 -0500 Subject: [PATCH 3/8] chore: clean up test warnings --- scripts/container-scripts/test | 1 + src/pypgstac/pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/container-scripts/test b/scripts/container-scripts/test index 6d2e03c6..f8547c6e 100755 --- a/scripts/container-scripts/test +++ b/scripts/container-scripts/test @@ -66,6 +66,7 @@ EOSQL function refresh_collation_versions(){ # Newer container libc versions can make template collation metadata stale. psql -X -q -d postgres -c "ALTER DATABASE template1 REFRESH COLLATION VERSION;" >/dev/null 2>&1 || true + psql -X -q -d postgres -c "ALTER DATABASE postgres REFRESH COLLATION VERSION;" >/dev/null 2>&1 || true psql -X -q -d postgres -c "ALTER DATABASE postgis REFRESH COLLATION VERSION;" >/dev/null 2>&1 || true } diff --git a/src/pypgstac/pyproject.toml b/src/pypgstac/pyproject.toml index d59703f2..4ded3050 100644 --- a/src/pypgstac/pyproject.toml +++ b/src/pypgstac/pyproject.toml @@ -105,6 +105,7 @@ ignore = [ "B008", # do not perform function calls in argument defaults "C901", # too complex "B905", + "COM812", # conflicts with ruff format ] [tool.ruff.lint.isort] From 392c04cc48e055a3d35dead24435bab7e46caca8 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Mon, 11 May 2026 10:19:46 -0500 Subject: [PATCH 4/8] update changelog --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1aac96df..a2468745 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -122,7 +122,7 @@ Individual tests can be run with any combination of the following flags `--forma 6) Once the PR has been merged, start the release process. 7) Create a git tag `git tag v0.2.8` using new version number 8) Push the git tag `git push origin v0.2.8` -9) The CI process will push pypgstac to PyPi, create a docker image on ghcr.io, and create a release on github. +9) The CI process will push `pypgstac` and `pgstac-migrate` to PyPI, create docker images on ghcr.io, and create a release on GitHub. Register PyPI trusted publishers for both projects before the first tagged release. ### Get Involved From f31bcd2b1128f10983f05f498223463d95507a94 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Mon, 11 May 2026 11:29:26 -0500 Subject: [PATCH 5/8] add more tests --- .gitignore | 1 + CHANGELOG.md | 19 ++++ docker/pgstac/dbinit/pgstac.sh | 0 scripts/container-scripts/test | 1 + src/pgstac-migrate/tests/test_cli.py | 44 +++++++++ src/pgstac-migrate/tests/test_parity.py | 92 +++++++++++++++++++ .../migrations/pgstac--0.9.11--unreleased.sql | 10 +- src/pgstac/migrations/pgstac--unreleased.sql | 51 ++++++++++ src/pgstac/pgstac.sql | 51 ++++++++++ src/pypgstac/tests/test_migrate_wrapper.py | 32 +++++++ 10 files changed, 297 insertions(+), 4 deletions(-) mode change 100755 => 100644 docker/pgstac/dbinit/pgstac.sh create mode 100644 src/pgstac-migrate/tests/test_parity.py diff --git a/.gitignore b/.gitignore index a60912e1..c646d853 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ src/pypgstac/python/pypgstac/*.so src/pgstacrust/target/ src/pgstac-migrate/dist/ src/pgstac-migrate/src/pgstac_migrate/migrations.tar.zst +src/pypgstac/uv.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index f4fcc997..91c4d7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- New `pgstac-migrate` package under `src/pgstac-migrate/` with a standalone + CLI, Python API, and tests for migration planning and execution. +- `src/pgstac/pyproject.toml` `tool.pgpkg` project metadata for canonical SQL + + migration staging. - `scripts/makemigration` host wrapper for the in-container `makemigration` helper. - `.env.example` documenting all supported environment variables for local development. - All host-facing scripts (`test`, `format`, `migrate`, `server`, `stageversion`, @@ -31,6 +35,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ecosystems with grouped update policies). ### Changed +- `pypgstac migrate` now delegates runtime migration planning and apply logic to + `pgstac-migrate`; `src/pypgstac/src/pypgstac/migrate.py` remains as a + compatibility wrapper. +- Migration filenames are now canonicalized to + `pgstac--.sql` / `pgstac----.sql` in + `src/pgstac/migrations/` and `src/pypgstac/src/pypgstac/migrations/`. +- `scripts/container-scripts/stageversion` and + `scripts/container-scripts/makemigration` now shell through `pgpkg` + (`uv run --no-project --with "pgpkg>=0.1,<0.2"` and + `uv run --no-project --with "pgpkg[diff]>=0.1,<0.2"`) with optional + `PGPKG_REPO_DIR` override support. +- `scripts/runinpypgstac` now supports a `PGPKG_LOCAL_REPO_DIR` mount override + for local pgpkg development while keeping the default flow PyPI-first. - Tagged releases now publish the new `pgstac-migrate` package to PyPI alongside `pypgstac` via trusted publishing in `.github/workflows/release.yml`. - In-container helper scripts moved from `docker/pypgstac/bin/` to `scripts/container-scripts/`; container `PATH` updated accordingly. @@ -69,6 +86,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `flake8`, `black`, and `mypy` removed from dev dependencies. ### Fixed +- `scripts/container-scripts/test` now refreshes collation metadata for the + `postgres` database during setup to avoid noisy warning output. - `load.py`: Use timezone-aware `MIN_DATETIME_UTC` / `MAX_DATETIME_UTC` sentinel constants (instead of naive `datetime.min` / `datetime.max`) to avoid `TypeError: can't compare offset-naive and offset-aware datetimes`. diff --git a/docker/pgstac/dbinit/pgstac.sh b/docker/pgstac/dbinit/pgstac.sh old mode 100755 new mode 100644 diff --git a/scripts/container-scripts/test b/scripts/container-scripts/test index f8547c6e..a30621a9 100755 --- a/scripts/container-scripts/test +++ b/scripts/container-scripts/test @@ -173,6 +173,7 @@ CREATE DATABASE pgstac_test_pypgstac TEMPLATE $TEMPLATEDB; ALTER DATABASE pgstac_test_pypgstac SET client_min_messages to $CLIENTMESSAGES; EOSQL uv run --extra dev --extra test --extra psycopg pytest tests $VERBOSE + uv run --extra dev --extra test --extra psycopg pytest ../pgstac-migrate/tests $VERBOSE psql -X -q -c "DROP DATABASE IF EXISTS pgstac_test_pypgstac WITH (force)"; } diff --git a/src/pgstac-migrate/tests/test_cli.py b/src/pgstac-migrate/tests/test_cli.py index a144659a..052389bc 100644 --- a/src/pgstac-migrate/tests/test_cli.py +++ b/src/pgstac-migrate/tests/test_cli.py @@ -1,5 +1,6 @@ from importlib import import_module from pathlib import Path +from types import SimpleNamespace import pytest @@ -55,3 +56,46 @@ def test_plan_renders_known_incremental_step(capsys) -> None: assert exit_code == 0 assert "0.9.10 -> 0.9.11" in captured.out assert "pgstac--0.9.10--0.9.11.sql" in captured.out + + +def test_migrate_delegates_to_api(monkeypatch, capsys) -> None: + cli_module = import_module("pgstac_migrate.cli") + captured_kwargs: dict[str, object] = {} + + def fake_migrate_database(**kwargs): + captured_kwargs.update(kwargs) + return SimpleNamespace( + bootstrapped_from="0.9.10", + applied_steps=[("0.9.10", "0.9.11")], + final_version="0.9.11", + ) + + monkeypatch.setattr(cli_module, "migrate_database", fake_migrate_database) + + exit_code = run_cli( + [ + "migrate", + "--to", + "0.9.11", + "--dry-run", + "--dsn", + "postgresql:///example", + ] + ) + + output = capsys.readouterr().out + assert exit_code == 0 + assert captured_kwargs == { + "target": "0.9.11", + "dry_run": True, + "conninfo": "postgresql:///example", + "host": None, + "port": None, + "dbname": None, + "user": None, + "password": None, + } + assert "bootstrapped to 0.9.10" in output + assert "applied 0.9.10 -> 0.9.11" in output + assert "final version: 0.9.11" in output + assert "(dry-run: rolled back)" in output diff --git a/src/pgstac-migrate/tests/test_parity.py b/src/pgstac-migrate/tests/test_parity.py new file mode 100644 index 00000000..adac6e63 --- /dev/null +++ b/src/pgstac-migrate/tests/test_parity.py @@ -0,0 +1,92 @@ +"""Cross-surface migration plan parity tests. + +Asserts that the pgstac-migrate artifact catalog and the pypgstac MigrationPath +compatibility helper produce *identical* ordered file sequences for every +(source, target) pair in the parity matrix. + +This is the canonical regression test for "both tools would apply exactly the +same SQL in exactly the same order". +""" + +from __future__ import annotations + +import tempfile +from importlib import import_module +from pathlib import Path + +import pytest + +# --------------------------------------------------------------------------- +# Shared fixture: load the baked artifact once and extract migration files +# to a temporary directory so MigrationPath can resolve filenames. +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def artifact_catalog_and_migrations_dir(): + """Return (catalog, migrations_dir) using the baked pgstac-migrate artifact.""" + pgpkg_artifact_mod = import_module("pgpkg.artifact") + cli = import_module("pgstac_migrate.cli") + + artifact = pgpkg_artifact_mod.load_artifact(cli._artifact_path()) + catalog = cli._catalog_from_artifact(artifact) + + tmp_root = Path(tempfile.mkdtemp(prefix="pgstac_parity_")) + migrations_dir = tmp_root / "migrations" + migrations_dir.mkdir() + for name, data in artifact.migrations_files().items(): + (migrations_dir / Path(name).name).write_bytes(data) + + return catalog, str(migrations_dir) + + +# --------------------------------------------------------------------------- +# Parity cases: (source, target) tuples. +# source=None means a fresh install (no prior pgstac version). +# --------------------------------------------------------------------------- + +PARITY_CASES = [ + # Fresh install + (None, "0.9.11"), + (None, "0.9.10"), + # Single-hop incremental upgrade + ("0.9.10", "0.9.11"), + ("0.9.9", "0.9.10"), + # Multi-hop incremental upgrade + ("0.9.9", "0.9.11"), + ("0.9.8", "0.9.11"), + ("0.8.6", "0.9.11"), +] + + +@pytest.mark.parametrize("source,target", PARITY_CASES) +def test_plan_parity_across_surfaces( + artifact_catalog_and_migrations_dir, + source: str | None, + target: str, +) -> None: + """pgstac-migrate catalog plan == pypgstac MigrationPath for every test case.""" + pgpkg_planner = import_module("pgpkg.planner") + compat = import_module("pgstac_migrate.compat") + + catalog, migrations_dir = artifact_catalog_and_migrations_dir + + # ---- pgstac-migrate catalog path ----------------------------------------- + migration_plan = pgpkg_planner.plan(catalog, source=source, target=target) + + pgpkg_files: list[str] = [] + if migration_plan.bootstrap_base is not None: + pgpkg_files.append(migration_plan.bootstrap_base.name) + pgpkg_files.extend(step.file.name for step in migration_plan.steps) + + # ---- pypgstac MigrationPath compat path ---------------------------------- + compat_source = "init" if source is None else source + compat_files = compat.MigrationPath( + migrations_dir, compat_source, target + ).migrations() + + assert pgpkg_files == compat_files, ( + f"Plan mismatch for {source!r} → {target!r}:\n" + f" pgstac-migrate catalog: {pgpkg_files}\n" + f" pypgstac MigrationPath: {compat_files}" + ) diff --git a/src/pgstac/migrations/pgstac--0.9.11--unreleased.sql b/src/pgstac/migrations/pgstac--0.9.11--unreleased.sql index cddaf7eb..82443707 100644 --- a/src/pgstac/migrations/pgstac--0.9.11--unreleased.sql +++ b/src/pgstac/migrations/pgstac--0.9.11--unreleased.sql @@ -1,5 +1,9 @@ -SET client_min_messages TO WARNING; -SET SEARCH_PATH to pgstac, public; +-- Generated by pgpkg makemigration +-- Project: pgstac +-- From: 0.9.11 +-- To: unreleased +-- Review the diff before applying. + RESET ROLE; DO $$ DECLARE @@ -193,8 +197,6 @@ RETURNS timestamptz AS $$ END ; $$ LANGUAGE SQL IMMUTABLE STRICT; --- BEGIN migra calculated SQL --- END migra calculated SQL DO $$ BEGIN INSERT INTO queryables (name, definition, property_wrapper, property_index_type) VALUES diff --git a/src/pgstac/migrations/pgstac--unreleased.sql b/src/pgstac/migrations/pgstac--unreleased.sql index 4c836762..e12c4bd6 100644 --- a/src/pgstac/migrations/pgstac--unreleased.sql +++ b/src/pgstac/migrations/pgstac--unreleased.sql @@ -1,3 +1,8 @@ +-- Generated by pgpkg stageversion +-- Project: pgstac +-- Version: unreleased + +-- BEGIN FRAGMENT: 000_idempotent_pre.sql RESET ROLE; DO $$ DECLARE @@ -191,6 +196,9 @@ RETURNS timestamptz AS $$ END ; $$ LANGUAGE SQL IMMUTABLE STRICT; +-- END FRAGMENT: 000_idempotent_pre.sql + +-- BEGIN FRAGMENT: 001_core.sql CREATE TABLE IF NOT EXISTS migrations ( version text PRIMARY KEY, @@ -518,6 +526,9 @@ BEGIN END; $$ LANGUAGE PLPGSQL SET SEARCH_PATH TO pgstac, public SET CLIENT_MIN_MESSAGES TO NOTICE; +-- END FRAGMENT: 001_core.sql + +-- BEGIN FRAGMENT: 001a_jsonutils.sql CREATE OR REPLACE FUNCTION to_int(jsonb) RETURNS int AS $$ SELECT floor(($1->>0)::float)::int; $$ LANGUAGE SQL IMMUTABLE STRICT COST 5000 PARALLEL SAFE; @@ -793,6 +804,9 @@ CREATE OR REPLACE AGGREGATE jsonb_max(jsonb) ( STYPE = jsonb, SFUNC = jsonb_greatest ); +-- END FRAGMENT: 001a_jsonutils.sql + +-- BEGIN FRAGMENT: 001s_stacutils.sql /* looks for a geometry in a stac item first from geometry and falling back to bbox */ CREATE OR REPLACE FUNCTION stac_geom(value jsonb) RETURNS geometry AS $$ SELECT @@ -856,6 +870,9 @@ CREATE TABLE IF NOT EXISTS stac_extensions( url text PRIMARY KEY, content jsonb ); +-- END FRAGMENT: 001s_stacutils.sql + +-- BEGIN FRAGMENT: 002_collections.sql CREATE OR REPLACE FUNCTION collection_base_item(content jsonb) RETURNS jsonb AS $$ SELECT jsonb_build_object( 'type', 'Feature', @@ -947,6 +964,9 @@ $$ LANGUAGE PLPGSQL; CREATE TRIGGER collection_delete_trigger BEFORE DELETE ON collections FOR EACH ROW EXECUTE FUNCTION collection_delete_trigger_func(); +-- END FRAGMENT: 002_collections.sql + +-- BEGIN FRAGMENT: 002a_queryables.sql CREATE OR REPLACE FUNCTION queryable_signature(n text, c text[]) RETURNS text AS $$ SELECT concat(n, c); $$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; @@ -1516,6 +1536,9 @@ CREATE OR REPLACE FUNCTION missing_queryables(_tablesample float DEFAULT 5) RETU ORDER BY 2,1 ; $$ LANGUAGE SQL; +-- END FRAGMENT: 002a_queryables.sql + +-- BEGIN FRAGMENT: 002b_cql.sql CREATE OR REPLACE FUNCTION parse_dtrange( _indate jsonb, relative_base timestamptz DEFAULT date_trunc('hour', CURRENT_TIMESTAMP) @@ -2035,6 +2058,9 @@ BEGIN RETURN collections; END; $$ LANGUAGE PLPGSQL STABLE STRICT; +-- END FRAGMENT: 002b_cql.sql + +-- BEGIN FRAGMENT: 003a_items.sql CREATE TABLE items ( id text NOT NULL, geometry geometry NOT NULL, @@ -2388,6 +2414,9 @@ UPDATE collections ) ; $$ LANGUAGE SQL; +-- END FRAGMENT: 003a_items.sql + +-- BEGIN FRAGMENT: 003b_partitions.sql CREATE TABLE partition_stats ( partition text PRIMARY KEY, dtrange tstzrange, @@ -2987,6 +3016,9 @@ INSERT OR UPDATE ON collections FOR EACH ROW EXECUTE FUNCTION collections_trigger_func(); +-- END FRAGMENT: 003b_partitions.sql + +-- BEGIN FRAGMENT: 004_search.sql CREATE OR REPLACE FUNCTION chunker( IN _where text, @@ -4117,6 +4149,9 @@ BEGIN RETURN curs; END; $$ LANGUAGE PLPGSQL; +-- END FRAGMENT: 004_search.sql + +-- BEGIN FRAGMENT: 004a_collectionsearch.sql CREATE OR REPLACE VIEW collections_asitems AS SELECT id, @@ -4260,6 +4295,9 @@ BEGIN END; $$ LANGUAGE PLPGSQL STABLE PARALLEL SAFE; +-- END FRAGMENT: 004a_collectionsearch.sql + +-- BEGIN FRAGMENT: 005_tileutils.sql SET SEARCH_PATH TO pgstac, public; CREATE OR REPLACE FUNCTION tileenvelope(zoom int, x int, y int) RETURNS geometry AS $$ @@ -4282,6 +4320,9 @@ $$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;DROP FUNCTION IF EXISTS mercgrid; CREATE OR REPLACE FUNCTION ftime() RETURNS interval as $$ SELECT age(clock_timestamp(), transaction_timestamp()); $$ LANGUAGE SQL; +-- END FRAGMENT: 005_tileutils.sql + +-- BEGIN FRAGMENT: 006_tilesearch.sql SET SEARCH_PATH to pgstac, public; DROP FUNCTION IF EXISTS geometrysearch; @@ -4443,6 +4484,9 @@ CREATE OR REPLACE FUNCTION xyzsearch( skipcovered ); $$ LANGUAGE SQL; +-- END FRAGMENT: 006_tilesearch.sql + +-- BEGIN FRAGMENT: 997_maintenance.sql CREATE OR REPLACE PROCEDURE analyze_items() AS $$ DECLARE @@ -4530,6 +4574,9 @@ BEGIN RETURN NULL; END; $$ LANGUAGE PLPGSQL; +-- END FRAGMENT: 997_maintenance.sql + +-- BEGIN FRAGMENT: 998_idempotent_post.sql DO $$ BEGIN INSERT INTO queryables (name, definition, property_wrapper, property_index_type) VALUES @@ -4656,4 +4703,8 @@ RESET ROLE; SET ROLE pgstac_ingest; SELECT update_partition_stats_q(partition) FROM partitions_view; +-- END FRAGMENT: 998_idempotent_post.sql + +-- BEGIN FRAGMENT: 999_version.sql SELECT set_version('unreleased'); +-- END FRAGMENT: 999_version.sql diff --git a/src/pgstac/pgstac.sql b/src/pgstac/pgstac.sql index 4c836762..e12c4bd6 100644 --- a/src/pgstac/pgstac.sql +++ b/src/pgstac/pgstac.sql @@ -1,3 +1,8 @@ +-- Generated by pgpkg stageversion +-- Project: pgstac +-- Version: unreleased + +-- BEGIN FRAGMENT: 000_idempotent_pre.sql RESET ROLE; DO $$ DECLARE @@ -191,6 +196,9 @@ RETURNS timestamptz AS $$ END ; $$ LANGUAGE SQL IMMUTABLE STRICT; +-- END FRAGMENT: 000_idempotent_pre.sql + +-- BEGIN FRAGMENT: 001_core.sql CREATE TABLE IF NOT EXISTS migrations ( version text PRIMARY KEY, @@ -518,6 +526,9 @@ BEGIN END; $$ LANGUAGE PLPGSQL SET SEARCH_PATH TO pgstac, public SET CLIENT_MIN_MESSAGES TO NOTICE; +-- END FRAGMENT: 001_core.sql + +-- BEGIN FRAGMENT: 001a_jsonutils.sql CREATE OR REPLACE FUNCTION to_int(jsonb) RETURNS int AS $$ SELECT floor(($1->>0)::float)::int; $$ LANGUAGE SQL IMMUTABLE STRICT COST 5000 PARALLEL SAFE; @@ -793,6 +804,9 @@ CREATE OR REPLACE AGGREGATE jsonb_max(jsonb) ( STYPE = jsonb, SFUNC = jsonb_greatest ); +-- END FRAGMENT: 001a_jsonutils.sql + +-- BEGIN FRAGMENT: 001s_stacutils.sql /* looks for a geometry in a stac item first from geometry and falling back to bbox */ CREATE OR REPLACE FUNCTION stac_geom(value jsonb) RETURNS geometry AS $$ SELECT @@ -856,6 +870,9 @@ CREATE TABLE IF NOT EXISTS stac_extensions( url text PRIMARY KEY, content jsonb ); +-- END FRAGMENT: 001s_stacutils.sql + +-- BEGIN FRAGMENT: 002_collections.sql CREATE OR REPLACE FUNCTION collection_base_item(content jsonb) RETURNS jsonb AS $$ SELECT jsonb_build_object( 'type', 'Feature', @@ -947,6 +964,9 @@ $$ LANGUAGE PLPGSQL; CREATE TRIGGER collection_delete_trigger BEFORE DELETE ON collections FOR EACH ROW EXECUTE FUNCTION collection_delete_trigger_func(); +-- END FRAGMENT: 002_collections.sql + +-- BEGIN FRAGMENT: 002a_queryables.sql CREATE OR REPLACE FUNCTION queryable_signature(n text, c text[]) RETURNS text AS $$ SELECT concat(n, c); $$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; @@ -1516,6 +1536,9 @@ CREATE OR REPLACE FUNCTION missing_queryables(_tablesample float DEFAULT 5) RETU ORDER BY 2,1 ; $$ LANGUAGE SQL; +-- END FRAGMENT: 002a_queryables.sql + +-- BEGIN FRAGMENT: 002b_cql.sql CREATE OR REPLACE FUNCTION parse_dtrange( _indate jsonb, relative_base timestamptz DEFAULT date_trunc('hour', CURRENT_TIMESTAMP) @@ -2035,6 +2058,9 @@ BEGIN RETURN collections; END; $$ LANGUAGE PLPGSQL STABLE STRICT; +-- END FRAGMENT: 002b_cql.sql + +-- BEGIN FRAGMENT: 003a_items.sql CREATE TABLE items ( id text NOT NULL, geometry geometry NOT NULL, @@ -2388,6 +2414,9 @@ UPDATE collections ) ; $$ LANGUAGE SQL; +-- END FRAGMENT: 003a_items.sql + +-- BEGIN FRAGMENT: 003b_partitions.sql CREATE TABLE partition_stats ( partition text PRIMARY KEY, dtrange tstzrange, @@ -2987,6 +3016,9 @@ INSERT OR UPDATE ON collections FOR EACH ROW EXECUTE FUNCTION collections_trigger_func(); +-- END FRAGMENT: 003b_partitions.sql + +-- BEGIN FRAGMENT: 004_search.sql CREATE OR REPLACE FUNCTION chunker( IN _where text, @@ -4117,6 +4149,9 @@ BEGIN RETURN curs; END; $$ LANGUAGE PLPGSQL; +-- END FRAGMENT: 004_search.sql + +-- BEGIN FRAGMENT: 004a_collectionsearch.sql CREATE OR REPLACE VIEW collections_asitems AS SELECT id, @@ -4260,6 +4295,9 @@ BEGIN END; $$ LANGUAGE PLPGSQL STABLE PARALLEL SAFE; +-- END FRAGMENT: 004a_collectionsearch.sql + +-- BEGIN FRAGMENT: 005_tileutils.sql SET SEARCH_PATH TO pgstac, public; CREATE OR REPLACE FUNCTION tileenvelope(zoom int, x int, y int) RETURNS geometry AS $$ @@ -4282,6 +4320,9 @@ $$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;DROP FUNCTION IF EXISTS mercgrid; CREATE OR REPLACE FUNCTION ftime() RETURNS interval as $$ SELECT age(clock_timestamp(), transaction_timestamp()); $$ LANGUAGE SQL; +-- END FRAGMENT: 005_tileutils.sql + +-- BEGIN FRAGMENT: 006_tilesearch.sql SET SEARCH_PATH to pgstac, public; DROP FUNCTION IF EXISTS geometrysearch; @@ -4443,6 +4484,9 @@ CREATE OR REPLACE FUNCTION xyzsearch( skipcovered ); $$ LANGUAGE SQL; +-- END FRAGMENT: 006_tilesearch.sql + +-- BEGIN FRAGMENT: 997_maintenance.sql CREATE OR REPLACE PROCEDURE analyze_items() AS $$ DECLARE @@ -4530,6 +4574,9 @@ BEGIN RETURN NULL; END; $$ LANGUAGE PLPGSQL; +-- END FRAGMENT: 997_maintenance.sql + +-- BEGIN FRAGMENT: 998_idempotent_post.sql DO $$ BEGIN INSERT INTO queryables (name, definition, property_wrapper, property_index_type) VALUES @@ -4656,4 +4703,8 @@ RESET ROLE; SET ROLE pgstac_ingest; SELECT update_partition_stats_q(partition) FROM partitions_view; +-- END FRAGMENT: 998_idempotent_post.sql + +-- BEGIN FRAGMENT: 999_version.sql SELECT set_version('unreleased'); +-- END FRAGMENT: 999_version.sql diff --git a/src/pypgstac/tests/test_migrate_wrapper.py b/src/pypgstac/tests/test_migrate_wrapper.py index 4f0dc01e..5e376b11 100644 --- a/src/pypgstac/tests/test_migrate_wrapper.py +++ b/src/pypgstac/tests/test_migrate_wrapper.py @@ -1,8 +1,11 @@ from importlib import import_module +from pathlib import Path from types import SimpleNamespace from pypgstac.db import PgstacDB from pypgstac.migrate import Migrate +from pypgstac.migrate import MigrationPath as PypgstacMigrationPath +from pypgstac.pypgstac import PgstacCLI def test_run_migration_delegates_to_pgstac_migrate(monkeypatch) -> None: @@ -58,3 +61,32 @@ def test_run_migration_defaults_to_package_version(monkeypatch) -> None: "target": "0.9.11-dev", "conninfo": None, } + + +def test_cli_migrate_delegates_to_migrate_wrapper(monkeypatch) -> None: + captured: dict[str, object] = {} + + def fake_run_migration(self, toversion=None): + captured["toversion"] = toversion + return "0.9.11" + + monkeypatch.setattr(Migrate, "run_migration", fake_run_migration) + + result = PgstacCLI(dsn="postgresql:///example").migrate("0.9.11") + + assert result == "0.9.11" + assert captured == {"toversion": "0.9.11"} + + +def test_migration_path_matches_pgstac_migrate_compat(tmp_path: Path) -> None: + compat = import_module("pgstac_migrate.compat") + + (tmp_path / "pgstac--0.9.10.sql").write_text("-- base\n") + (tmp_path / "pgstac--0.9.11.sql").write_text("-- base\n") + (tmp_path / "pgstac--0.9.10--0.9.11.sql").write_text("-- step\n") + + left = PypgstacMigrationPath(str(tmp_path), "0.9.10", "0.9.11").migrations() + right = compat.MigrationPath(str(tmp_path), "0.9.10", "0.9.11").migrations() + + assert left == ["pgstac--0.9.10--0.9.11.sql"] + assert left == right From 5982900b207ea782c144f7fcc00904a12d1f4b0c Mon Sep 17 00:00:00 2001 From: David Bitner Date: Tue, 12 May 2026 09:15:43 -0500 Subject: [PATCH 6/8] Update scripts/makemigration Co-authored-by: Pete Gadomski --- scripts/makemigration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/makemigration b/scripts/makemigration index d81528a0..d6a5a675 100755 --- a/scripts/makemigration +++ b/scripts/makemigration @@ -12,7 +12,7 @@ Options: -f, --from VERSION Source base version. -t, --to VERSION Target base version. -o, --overwrite Replace an existing migration file. - -d, --debug Print the generated wrapped migration SQL after creation. + -d, --debug Print the generated wrapped migration SQL after creation. --build-policy POLICY One of: always, missing, never. Default: always. -h, --help Show this help text. From 85a299f9c0ccabeabb1228705552bf781d3a6d7e Mon Sep 17 00:00:00 2001 From: David Bitner Date: Tue, 12 May 2026 09:15:55 -0500 Subject: [PATCH 7/8] Update .github/workflows/release.yml Co-authored-by: Pete Gadomski --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df8b6d6a..6ac3221d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -243,7 +243,7 @@ jobs: with: python-version: "3.x" - name: Install build - working-directory: /home/runner/work/pgstac/pgstac/src/pgstac-migrate + working-directory: src/pgstac-migrate run: python -m pip install build - name: Build working-directory: /home/runner/work/pgstac/pgstac/src/pgstac-migrate From 5a38220ac58a5e22deb6cb08bbba1b7ed432dd0d Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 12 May 2026 12:22:57 -0500 Subject: [PATCH 8/8] Address PR review feedback and harden migration docs/deps - expand pgstac-migrate README with full CLI/API/env var docs and troubleshooting - make psycopg[binary] mandatory in pgstac-migrate and pypgstac - make psycopg-pool mandatory in pypgstac - remove redundant psycopg optional/group wiring and update test script flags - remove pgstac-migrate upper bound in pypgstac dependency - update release workflow paths and uv setup/build step - refresh docs/changelog references for pgpkg>=0.1.1 - regenerate uv lockfiles --- .github/workflows/release.yml | 21 +- CHANGELOG.md | 4 +- CLAUDE.md | 6 +- README.md | 4 +- docs/src/pypgstac.md | 42 ++- scripts/container-scripts/makemigration | 2 +- scripts/container-scripts/stageversion | 2 +- scripts/container-scripts/test | 18 +- src/pgstac-migrate/README.md | 279 ++++++++++++++++++- src/pgstac-migrate/pyproject.toml | 3 +- src/pgstac-migrate/scripts/build_artifact.py | 2 +- src/pgstac-migrate/uv.lock | 12 +- src/pypgstac/pyproject.toml | 21 +- src/pypgstac/src/pypgstac/migrate.py | 7 + 14 files changed, 359 insertions(+), 64 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ac3221d..56b9ba1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -217,15 +217,15 @@ jobs: with: python-version: "3.x" - name: Install build - working-directory: /home/runner/work/pgstac/pgstac/src/pypgstac + working-directory: src/pypgstac run: pip install build - name: Build - working-directory: /home/runner/work/pgstac/pgstac/src/pypgstac + working-directory: src/pypgstac run: python -m build - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 with: - packages-dir: /home/runner/work/pgstac/pgstac/src/pypgstac/dist + packages-dir: src/pypgstac/dist releasepgstacmigratetopypi: name: Release pgstac-migrate to PyPI @@ -238,17 +238,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.x" - - name: Install build - working-directory: src/pgstac-migrate - run: python -m pip install build + - name: Setup uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Build - working-directory: /home/runner/work/pgstac/pgstac/src/pgstac-migrate - run: python -m build + working-directory: src/pgstac-migrate + run: uvx --from build pyproject-build - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 with: - packages-dir: /home/runner/work/pgstac/pgstac/src/pgstac-migrate/dist + packages-dir: src/pgstac-migrate/dist diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c4d7cf..41a541a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,8 +43,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). `src/pgstac/migrations/` and `src/pypgstac/src/pypgstac/migrations/`. - `scripts/container-scripts/stageversion` and `scripts/container-scripts/makemigration` now shell through `pgpkg` - (`uv run --no-project --with "pgpkg>=0.1,<0.2"` and - `uv run --no-project --with "pgpkg[diff]>=0.1,<0.2"`) with optional + (`uv run --no-project --with "pgpkg>=0.1.1,<0.2"` and + `uv run --no-project --with "pgpkg[diff]>=0.1.1,<0.2"`) with optional `PGPKG_REPO_DIR` override support. - `scripts/runinpypgstac` now supports a `PGPKG_LOCAL_REPO_DIR` mount override for local pgpkg development while keeping the default flow PyPI-first. diff --git a/CLAUDE.md b/CLAUDE.md index 91181424..96fc13eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,7 +98,7 @@ All tests run inside Docker via `scripts/runinpypgstac`. Use `--build` to rebuil - **pgstac** container: PostgreSQL 17 + PostGIS 3 + extensions, port 5439→5432 - **pypgstac** container: Python + Rust build tools, runs scripts - `scripts/runinpypgstac` uses the published-package path by default; set `PGPKG_LOCAL_REPO_DIR` to mount a local `pgpkg` checkout at `/pgpkg` and export `PGPKG_REPO_DIR` when `stageversion` or `makemigration` should run against a local checkout -- When no local checkout is mounted, the in-container `stageversion` / `makemigration` helpers resolve `pgpkg>=0.1,<0.2` from PyPI with `uv run --no-project --with ...` +- When no local checkout is mounted, the in-container `stageversion` / `makemigration` helpers resolve `pgpkg>=0.1.1,<0.2` from PyPI with `uv run --no-project --with ...` - Credentials: `username` / `password`, database: `postgis` ## Migration Process @@ -119,7 +119,7 @@ This runs inside Docker and: ### How makemigration Works -`makemigration` (copied from `scripts/container-scripts/makemigration` into the image) now prefers a local checkout via `PGPKG_REPO_DIR`, otherwise it resolves the pinned published package with `uv run --no-project --with "pgpkg[diff]>=0.1,<0.2" pgpkg makemigration`: +`makemigration` (copied from `scripts/container-scripts/makemigration` into the image) now prefers a local checkout via `PGPKG_REPO_DIR`, otherwise it resolves the pinned published package with `uv run --no-project --with "pgpkg[diff]>=0.1.1,<0.2" pgpkg makemigration`: 1. Uses `src/pgstac/pyproject.toml` to locate the canonical staged base files 2. Uses `results.temporary_local_db` via `pgpkg` to diff the source and target staged bases @@ -144,7 +144,7 @@ uv run --directory src/pgstac-migrate pgstac-migrate versions `pgstac-migrate` owns runtime migration planning and apply logic. `pypgstac migrate` delegates to the same Python API for backwards compatibility and does not execute source-tree SQL files directly. The source-tree `pgstac-migrate` package prefers the baked artifact at `src/pgstac-migrate/src/pgstac_migrate/migrations.tar.zst` and rebuilds it from the source tree when that file is missing. -`src/pgstac-migrate/pyproject.toml` resolves `pgpkg>=0.1,<0.2` from PyPI. The standalone `src/pgstac-migrate/scripts/build_artifact.py` helper does not use that lockfile; it carries its own inline `pgpkg>=0.1,<0.2` dependency. +`src/pgstac-migrate/pyproject.toml` resolves `pgpkg>=0.1.1,<0.2` from PyPI. The standalone `src/pgstac-migrate/scripts/build_artifact.py` helper does not use that lockfile; it carries its own inline `pgpkg>=0.1.1,<0.2` dependency. `src/pypgstac/pyproject.toml` keeps a local `[tool.uv.sources]` override to the sibling `../pgstac-migrate` project so `uv run --directory src/pypgstac ...` resolves the wrapper stack from the source tree, while `pgpkg` resolves from PyPI. In the Docker-backed dev flow, `scripts/runinpypgstac` can mount a local pgpkg checkout at `/pgpkg` and export `PGPKG_REPO_DIR` for container-script testing. ## Testing Details diff --git a/README.md b/README.md index 21d85e46..1b33f11a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ --- -**PgSTAC** is a set of SQL functions and schema to build highly performant databases for Spatio-Temporal Asset Catalogs ([STAC](https://stacspec.org/)). The project also provides **pypgstac** (a Python module) to help with database migrations and document ingestion (collections and items). +**PgSTAC** is a set of SQL functions and schema to build highly performant databases for Spatio-Temporal Asset Catalogs ([STAC](https://stacspec.org/)). The project also provides **pgstac-migrate** (a focused migration package) and **pypgstac** (a Python module for compatibility migration commands and document ingestion). PgSTAC provides functionality for STAC Filters, CQL2 search, and utilities to help manage the indexing and partitioning of STAC Collections and Items. @@ -34,6 +34,8 @@ PgSTAC Documentation: https://stac-utils.github.io/pgstac/pgstac pyPgSTAC Documentation: https://stac-utils.github.io/pgstac/pypgstac +pgstac-migrate package: `src/pgstac-migrate` + ## Project structure ``` diff --git a/docs/src/pypgstac.md b/docs/src/pypgstac.md index 5a0719e7..3256aabe 100644 --- a/docs/src/pypgstac.md +++ b/docs/src/pypgstac.md @@ -7,11 +7,7 @@ pyPgSTAC is available on PyPI python -m pip install pypgstac ``` -By default, pyPgSTAC does not install the `psycopg` dependency. If you want the database driver installed, use: - -``` -python -m pip install pypgstac[psycopg] -``` +pyPgSTAC installs the PostgreSQL driver dependencies (`psycopg[binary]` and `psycopg-pool`) by default. Or can be built locally ``` @@ -50,14 +46,40 @@ pyPgSTAC will get the database connection settings from the **standard PG enviro It can also take a DSN database url "postgresql://..." via the **--dsn** flag. ### Migrations -pyPgSTAC has a utility to help apply migrations to an existing PgSTAC instance to bring it up to date. -There are two types of migrations: +`pypgstac migrate` is a compatibility wrapper over the standalone `pgstac-migrate` package. + +- Runtime planning and apply logic lives in `pgstac-migrate`. +- `pypgstac migrate` remains supported for backward compatibility. + +Migration filenames use canonical PostgreSQL extension naming: + +- **Base migrations:** `pgstac--.sql` +- **Incremental migrations:** `pgstac----.sql` + +These files are bundled in the `pgstac-migrate` artifact and used by both CLIs. + +### `pgstac-migrate` CLI and API - - **Base migrations** install PgSTAC into a database with no current PgSTAC installation. These migrations follow the file pattern `"pgstac.[version].sql"` - - **Incremental migrations** are used to move PgSTAC from one version to the next. These migrations follow the file pattern `"pgstac.[version].[fromversion].sql"` +For direct migration operations (recommended for new integrations): + +```bash +pgstac-migrate migrate --help +pgstac-migrate plan +pgstac-migrate versions +pgstac-migrate info +``` + +Python API example: + +```python +from pgstac_migrate.api import migrate + +result = migrate(target=None, conninfo="postgresql://...") +print(result.final_version) +``` -Migrations are stored in ```pypgstac/pypgstac/migrations``` and are distributed with the pyPgSTAC package. +Use `target=None` for latest, or set `target=""`. ### Running Migrations pyPgSTAC has a utility for checking the version of an existing PgSTAC database and applying the appropriate migrations in the correct order. It can also be used to setup a database from scratch. diff --git a/scripts/container-scripts/makemigration b/scripts/container-scripts/makemigration index ea70b08a..0aacb351 100755 --- a/scripts/container-scripts/makemigration +++ b/scripts/container-scripts/makemigration @@ -89,7 +89,7 @@ function run_pgpkg_makemigration() { return fi - uv run --no-project --with "pgpkg[diff]>=0.1,<0.2" "$@" + uv run --no-project --with "pgpkg[diff]>=0.1.1,<0.2" "$@" } # Check if from SQL file exists diff --git a/scripts/container-scripts/stageversion b/scripts/container-scripts/stageversion index 075892ea..d02f2008 100755 --- a/scripts/container-scripts/stageversion +++ b/scripts/container-scripts/stageversion @@ -19,7 +19,7 @@ function run_pgpkg() { return fi - uv run --no-project --with "pgpkg>=0.1,<0.2" "$@" + uv run --no-project --with "pgpkg>=0.1.1,<0.2" "$@" } function usage() { diff --git a/scripts/container-scripts/test b/scripts/container-scripts/test index a30621a9..4bbc525d 100755 --- a/scripts/container-scripts/test +++ b/scripts/container-scripts/test @@ -74,11 +74,11 @@ function test_formatting(){ cd $SRCDIR/pypgstac echo "Running ruff" - uv run --extra dev ruff check src/pypgstac tests - uv run --extra dev ruff format --check src/pypgstac tests + uv run --group dev ruff check src/pypgstac tests + uv run --group dev ruff format --check src/pypgstac tests echo "Running ty" - uv run --extra dev --extra test --extra psycopg ty check + uv run --group dev --group test ty check echo "Checking if there are any staged migrations." find $SRCDIR/pgstac/migrations | grep 'staged' && { echo "There are staged migrations in pgstac/migrations. Please check migrations and remove the staged suffix."; exit 1; } @@ -172,8 +172,8 @@ DROP DATABASE IF EXISTS pgstac_test_pypgstac WITH (force); CREATE DATABASE pgstac_test_pypgstac TEMPLATE $TEMPLATEDB; ALTER DATABASE pgstac_test_pypgstac SET client_min_messages to $CLIENTMESSAGES; EOSQL - uv run --extra dev --extra test --extra psycopg pytest tests $VERBOSE - uv run --extra dev --extra test --extra psycopg pytest ../pgstac-migrate/tests $VERBOSE + uv run --group dev --group test pytest tests $VERBOSE + uv run --group dev --group test pytest ../pgstac-migrate/tests $VERBOSE psql -X -q -c "DROP DATABASE IF EXISTS pgstac_test_pypgstac WITH (force)"; } @@ -289,11 +289,11 @@ EOSQL export PGDATABASE=pgstac_test_migration echo "Migrating from version 0.3.0" cd $SRCDIR/pypgstac - uv run --extra dev --extra test --extra psycopg pypgstac migrate --toversion 0.3.0 - uv run --extra dev --extra test --extra psycopg pypgstac --version + uv run --group dev --group test pypgstac migrate --toversion 0.3.0 + uv run --group dev --group test pypgstac --version - uv run --extra dev --extra test --extra psycopg pypgstac migrate - uv run --extra dev --extra test --extra psycopg pypgstac --version + uv run --group dev --group test pypgstac migrate + uv run --group dev --group test pypgstac --version echo "Running all tests against incrementally migrated database." test_pgtap pgstac_test_migration diff --git a/src/pgstac-migrate/README.md b/src/pgstac-migrate/README.md index c290bd09..f827471f 100644 --- a/src/pgstac-migrate/README.md +++ b/src/pgstac-migrate/README.md @@ -1,22 +1,285 @@ # pgstac-migrate -Apply baked PgSTAC migrations with `pgpkg`. +Standalone PgSTAC migration CLI and Python API. -Source-tree development resolves `pgpkg>=0.1,<0.2` from PyPI by default. +This package applies PgSTAC schema migrations to a PostgreSQL database from a bundled migration artifact. + +## Install + +```bash +pip install pgstac-migrate +``` + +## Quick start + +```bash +pgstac-migrate --help +pgstac-migrate migrate +``` + +## CLI command reference + +Top-level commands: + +- migrate: apply migrations to a live database +- plan: show the migration plan without applying changes +- versions: list all versions available in the bundled artifact +- info: show artifact metadata and bundled migration file info +- build-artifact: build or refresh the local artifact from source SQL files + +### migrate + +Usage: + +```bash +pgstac-migrate migrate [--dsn DSN] [-h HOST] [-p PORT] [-d DBNAME] [-U USER] [-W] [--to TARGET] [--dry-run] +``` + +Parameters: + +- --dsn DSN + - Full libpq connection string. When provided, it takes precedence over individual host/user/db flags. +- -h, --host HOST + - Database host. Same meaning as PGHOST. +- -p, --port PORT + - Database port. Same meaning as PGPORT. +- -d, --dbname DBNAME + - Database name. Same meaning as PGDATABASE. +- -U, --user USER + - Database user. Same meaning as PGUSER. +- -W, --password-prompt + - Prompt for password interactively. +- --to TARGET + - Target PgSTAC version to migrate to. If omitted, migrates to the latest version in the artifact. +- --dry-run + - Computes and executes the migration plan, then rolls back before commit. + +Examples: + +```bash +pgstac-migrate migrate +pgstac-migrate migrate --to 0.9.11 +pgstac-migrate migrate --dry-run +pgstac-migrate migrate --dsn "postgresql://user:pass@localhost:5432/postgis" +pgstac-migrate migrate --host localhost --port 5432 --dbname postgis --user username -W +``` + +### plan + +Usage: + +```bash +pgstac-migrate plan [--source SOURCE] [--to TARGET] +``` + +Parameters: + +- --source SOURCE + - Starting version for planning. Omit for fresh install planning. +- --to TARGET + - Target version. If omitted, plans to the latest version in the artifact. Examples: +```bash +pgstac-migrate plan +pgstac-migrate plan --source 0.9.10 --to 0.9.11 +``` + +### versions + +Usage: + +```bash +pgstac-migrate versions +``` + +Prints all versions available in the bundled artifact catalog. + +### info + +Usage: + +```bash +pgstac-migrate info +``` + +Prints artifact manifest metadata, plus checksums and sizes for bundled entries. + +### build-artifact + +Usage: + +```bash +pgstac-migrate build-artifact +``` + +What it does: + +- Reads PgSTAC SQL and migration sources from the repository source tree. +- Builds a compressed artifact file named migrations.tar.zst. +- Writes the artifact to src/pgstac-migrate/src/pgstac_migrate/migrations.tar.zst. + +When to use it: + +- During source-tree development after SQL or migration files change. +- Before testing commands like plan, versions, info, or migrate against local unreleased migration changes. + +When you do not need it: + +- Typical PyPI package usage, where an artifact is already bundled in the installed wheel. + +## Connection parameters and environment variables + +pgstac-migrate follows libpq/psql connection conventions. + +Resolution order: + +1. Explicit CLI arguments +2. libpq environment variables +3. libpq defaults + +If --dsn is provided, it overrides individual connection flags. + +Supported libpq environment variables for connection behavior include: + +- PGHOST: database host name +- PGHOSTADDR: database host IP address +- PGPORT: database port +- PGDATABASE: database name +- PGUSER: database user +- PGPASSWORD: database password +- PGPASSFILE: password file path +- PGSERVICE: named service to load connection options +- PGSERVICEFILE: service file path +- PGCONNECT_TIMEOUT: connection timeout in seconds +- PGTARGETSESSIONATTRS: target session attributes for multi-host connection routing +- PGLOADBALANCEHOSTS: host load balancing policy + +SSL and TLS environment variables: + +- PGSSLMODE +- PGSSLROOTCERT +- PGSSLCERT +- PGSSLKEY +- PGSSLPASSWORD +- PGSSLCRL +- PGSSLCRLDIR +- PGSSLSNI +- PGSSLNEGOTIATION + +Additional libpq environment variables commonly used with PostgreSQL are also honored by libpq. See PostgreSQL libpq connection settings for complete semantics. + +## Python API reference + +Module: pgstac_migrate.api + +Functions: + +- artifact_path() -> pathlib.Path + - Returns the artifact path used by the package. +- normalize_target_version(target: str | None) -> str | None + - Maps source-tree development targets like 0.9.11-dev to unreleased. +- migrate(...) + - Applies migrations and returns an ApplyResult object. + +migrate parameters: + +- target: str | None = None + - Target version. None means latest available. +- dry_run: bool = False + - Run migration in rollback mode. +- conninfo: str | None = None + - Full DSN/libpq conninfo string. +- host: str | None = None +- port: int | str | None = None +- dbname: str | None = None +- user: str | None = None +- password: str | None = None + +Return value: + +- final_version: resulting database version +- bootstrapped_from: base version used when bootstrapping from an empty state +- applied_steps: ordered list of migration steps applied + +Example: + +```python +from pgstac_migrate.api import migrate + +result = migrate( + target="0.9.11", + dry_run=False, + host="localhost", + port=5432, + dbname="postgis", + user="username", + password="password", +) + +print(result.final_version) +print(result.bootstrapped_from) +print(result.applied_steps) +``` + +## Source checkout usage + ```bash uv run --directory src/pgstac-migrate pgstac-migrate build-artifact uv run --directory src/pgstac-migrate pgstac-migrate info uv run --directory src/pgstac-migrate pgstac-migrate versions -uv run --directory src/pgstac-migrate pgstac-migrate migrate --help +uv run --directory src/pgstac-migrate pgstac-migrate plan +uv run --directory src/pgstac-migrate pgstac-migrate migrate --dry-run ``` -Standalone post-release bootstrap helper: +## Operational notes -```bash -uv run --script src/pgstac-migrate/scripts/build_artifact.py -``` +- The `migrate` command is safe to re-run. If a database is already at target version, no migration steps are applied. +- Use `plan` before `migrate` when changing environments or moving between non-adjacent versions. +- Use `--dry-run` in CI or release validation to verify pathing and SQL execution without committing changes. + +## Troubleshooting + +### Connection/authentication errors + +Symptoms: + +- connection refused +- password authentication failed +- timeout expired + +Checks: + +- verify `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, and credentials +- verify SSL settings (`PGSSLMODE`, certificate paths) when required +- try a known-good `psql` connection with the same DSN/env values + +### Target version not found + +Symptoms: + +- requested `--to` version is rejected +- plan cannot reach target + +Checks: + +- run `pgstac-migrate versions` to see available targets +- run `pgstac-migrate info` to confirm artifact contents +- in source checkouts, run `pgstac-migrate build-artifact` after migration source changes + +### No steps applied + +If `migrate` reports no applied steps, this usually means either: + +- database is already at target version, or +- source/target are equal for the selected plan + +Use `plan` to confirm the expected path. + +### Dry-run behavior + +`--dry-run` executes the migration sequence and then rolls back. -That helper does not use `uv.lock`; it resolves its own inline dependency on `pgpkg>=0.1,<0.2` directly from PyPI. +- It is expected to report a final version in command output while leaving the database unchanged. +- Use this mode to validate migration viability, not to persist schema changes. diff --git a/src/pgstac-migrate/pyproject.toml b/src/pgstac-migrate/pyproject.toml index f507e8a9..d20f34c2 100644 --- a/src/pgstac-migrate/pyproject.toml +++ b/src/pgstac-migrate/pyproject.toml @@ -6,7 +6,8 @@ readme = "README.md" requires-python = ">=3.11" license = "MIT" dependencies = [ - "pgpkg>=0.1,<0.2", + "pgpkg>=0.1.1,<0.2", + "psycopg[binary]>=3.1.0", ] [project.scripts] diff --git a/src/pgstac-migrate/scripts/build_artifact.py b/src/pgstac-migrate/scripts/build_artifact.py index 880c5f1e..b9ef3568 100644 --- a/src/pgstac-migrate/scripts/build_artifact.py +++ b/src/pgstac-migrate/scripts/build_artifact.py @@ -2,7 +2,7 @@ # /// script # requires-python = ">=3.11" # dependencies = [ -# "pgpkg>=0.1,<0.2", +# "pgpkg>=0.1.1,<0.2", # ] # /// """Build the local pgstac-migrate baked artifact with the published pgpkg API.""" diff --git a/src/pgstac-migrate/uv.lock b/src/pgstac-migrate/uv.lock index c5ba4f2b..751c5223 100644 --- a/src/pgstac-migrate/uv.lock +++ b/src/pgstac-migrate/uv.lock @@ -13,16 +13,16 @@ wheels = [ [[package]] name = "pgpkg" -version = "0.1.0" +version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "psycopg", extra = ["binary"] }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/12/bd74a956815835a0a1d318f54deab5ebfc8d807178e99421f6232d806111/pgpkg-0.1.0.tar.gz", hash = "sha256:fecfea66c84c5976eb4058f3325e4d601a4a47378b1499f56ba413b7222b5838", size = 43573, upload-time = "2026-05-05T21:24:41.292Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/4c/c2557e77821fb7c53b327975d544ee7873042caaad8cf6a6f0416fdbd4cc/pgpkg-0.1.1.tar.gz", hash = "sha256:a3abf53a6b7e8c88774e0280a199bf752a4f0d17cb848c6d1119984c871b7ff9", size = 45678, upload-time = "2026-05-12T15:37:28.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/8f/7153e33850f68867b340c93cde3b17d3784dbf28880169383cc4b01cff95/pgpkg-0.1.0-py3-none-any.whl", hash = "sha256:1d68d2b2287bf68ee3c47012678eac4247bad79fcefbb9fc53cff1480d4f9d73", size = 30600, upload-time = "2026-05-05T21:24:39.768Z" }, + { url = "https://files.pythonhosted.org/packages/40/ce/2046b80e9a6e80088479f594b28c861d6ee68a515c109ff99343f4ec6d92/pgpkg-0.1.1-py3-none-any.whl", hash = "sha256:b8d2e6fc7a5118abc9529b37dba63ae4adc83e120650063e8563ef89f7c4e011", size = 31935, upload-time = "2026-05-12T15:37:26.899Z" }, ] [[package]] @@ -31,10 +31,14 @@ version = "0.9.11.dev0" source = { editable = "." } dependencies = [ { name = "pgpkg" }, + { name = "psycopg", extra = ["binary"] }, ] [package.metadata] -requires-dist = [{ name = "pgpkg", specifier = ">=0.1,<0.2" }] +requires-dist = [ + { name = "pgpkg", specifier = ">=0.1.1,<0.2" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.1.0" }, +] [[package]] name = "psycopg" diff --git a/src/pypgstac/pyproject.toml b/src/pypgstac/pyproject.toml index ed49b114..b874589a 100644 --- a/src/pypgstac/pyproject.toml +++ b/src/pypgstac/pyproject.toml @@ -22,7 +22,9 @@ dependencies = [ "fire>=0.7.0", "hydraters>=0.1.0", "orjson>=3.11.0", - "pgstac-migrate>=0.9.11.dev0,<0.10", + "pgstac-migrate>=0.9.11.dev0", + "psycopg[binary]>=3.1.0", + "psycopg-pool>=3.1.0", "plpygis>=0.5.0", "pydantic>=2.10,<3", "pydantic-settings>=2,<3", @@ -32,7 +34,13 @@ dependencies = [ "version-parser>=1.0.1", ] -[project.optional-dependencies] +[dependency-groups] +dev = [ + "types-setuptools", + "ruff==0.15.12", + "ty==0.0.35", + "pre-commit==4.6.0", +] test = [ "morecantile>=6.2,<7.1", "pytest>=8.3,<9.1", @@ -41,20 +49,13 @@ test = [ "pystac[validation]==1.*", "types-cachetools>=5.5", ] -dev = [ - "types-setuptools", - "ruff==0.15.12", - "ty==0.0.35", - "pre-commit==4.6.0", -] -psycopg = ["psycopg[binary]>=3.1.0", "psycopg-pool>=3.1.0"] migrations = [] docs = [ "jupyter", "pandas", "seaborn", "mkdocs-jupyter", - "folium" + "folium", ] diff --git a/src/pypgstac/src/pypgstac/migrate.py b/src/pypgstac/src/pypgstac/migrate.py index e10e8858..5ad94cbb 100644 --- a/src/pypgstac/src/pypgstac/migrate.py +++ b/src/pypgstac/src/pypgstac/migrate.py @@ -2,6 +2,7 @@ import glob import os +import warnings from collections import defaultdict from collections.abc import Iterator from importlib import import_module @@ -11,6 +12,12 @@ MIGRATION_PREFIX = "pgstac--" +warnings.warn( + "pypgstac.migrate is a compatibility wrapper and will be deprecated in a future minor release; use pgstac_migrate.api or the pgstac-migrate CLI directly.", + DeprecationWarning, + stacklevel=2, +) + def base_migration_filename(version: str) -> str: """Return the canonical base migration filename for a version."""