PgSTAC is a PostgreSQL extension (SQL functions + schema) for Spatio-Temporal Asset Catalogs (STAC), paired with pypgstac, a Python package for database migrations and bulk data ingestion.
- Repository: stac-utils/pgstac
- License: MIT
- Docs: https://stac-utils.github.io/pgstac/
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
scripts/container-scripts/ ← Scripts copied into the pypgstac container image
CHANGELOG.md— the single source of truth for release notesdocs/src/release-notes.md— a manual copy ofCHANGELOG.md, served by mkdocs. Keep them identical; update both when changing either.
- pgstac_admin – schema owner, migrations
- pgstac_ingest – read/write, execute functions
- pgstac_read – SELECT only
ONLY edit files in src/pgstac/sql/. Never edit pgstac.sql directly — it is assembled by concatenating all sql/*.sql files in alphabetical order during stageversion.
File execution order (alphabetical by prefix):
000_idempotent_pre → 001_core → 001a_jsonutils → 001s_stacutils → 002_collections → 002a_queryables → 002b_cql → 003a_items → 003b_partitions → 004_search → 004a_collectionsearch → 005_tileutils → 006_tilesearch → 997_maintenance → 998_idempotent_post → 999_version
Prefix ranges: 000-001 setup/core, 002 collections, 003 items/partitions, 004-006 search/tiles, 997-998 maintenance/post, 999 version (auto-generated).
000_idempotent_pre.sql and 998_idempotent_post.sql are included in both base installs and incremental migrations. Use IF NOT EXISTS, CREATE OR REPLACE, ON CONFLICT DO NOTHING.
Items partitioned by LIST(collection), optionally sub-partitioned by RANGE(datetime) (year/month via collections.partition_trunc). Naming: _items_{key}[_{YYYY|YYYYMM}].
Key functions: check_partition() (create/update), update_partition_stats() (recalculate constraints), partition_sys_meta (live VIEW — always current), partitions (MATERIALIZED VIEW — stale between refreshes).
PgSTAC installs into the pgstac schema. All connections must have search_path set to pgstac, public.
PgSTAC functions reference PostGIS functions (e.g. st_makeenvelope, st_geomfromgeojson) without schema qualification because PostGIS may be installed in either public or postgis schema. pg_dump clears search_path during restore, breaking these references.
Rules to maintain dump/restore compatibility:
- Do NOT schema-qualify PostGIS function calls in PgSTAC SQL
- Avoid cross-function dependencies in SQL functions used by GENERATED columns — pg_dump orders functions alphabetically, so
func_acallingfunc_bmay be created beforefunc_bexists. Inline the logic instead. - Use
pgstac_restore(viascripts/container-scripts/pgstac_restorein the image) to restore dumps — it installs a temporary event trigger that sets the correctsearch_pathbefore each DDL command - Test with
scripts/test --pgdump
scripts/setup # Build Docker images, start database
scripts/server # Start database (use --detach for background)scripts/test # All test suites
scripts/test --pypgstac # pytest only
scripts/test --pgtap # PGTap SQL tests
scripts/test --basicsql # SQL output comparison tests
scripts/test --migrations # Full migration chain test
scripts/test --formatting # ruff + ty
scripts/test --pgdump # pg_dump/pg_restore round-trip testAll tests run inside Docker via scripts/runinpypgstac. Use --build to rebuild images first.
- pgstac container: PostgreSQL 17 + PostGIS 3 + extensions, port 5439→5432
- pypgstac container: Python + Rust build tools, runs scripts
scripts/runinpypgstacuses the published-package path by default; setPGPKG_LOCAL_REPO_DIRto mount a localpgpkgcheckout at/pgpkgand exportPGPKG_REPO_DIRwhenstageversionormakemigrationshould run against a local checkout- When no local checkout is mounted, the in-container
stageversion/makemigrationhelpers resolvepgpkg>=0.1.1,<0.2from PyPI withuv run --no-project --with ... - Credentials:
username/password, database:postgis
scripts/stageversion 0.9.11This runs inside Docker and:
- Removes old
*unreleased*migration files - Writes
SELECT set_version('0.9.11');to999_version.sql - Runs
pgpkg stageversionagainstsrc/pgstac/pyproject.toml→migrations/pgstac--0.9.11.sql - Uses
--also-writeto keeppgstac.sqlsynchronized with the latest base migration - Updates
version.pyandpyproject.tomlversion strings - Runs
makemigration -f 0.9.10 -t 0.9.11to generate the wrapped incremental migration viapgpkg
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:
- Uses
src/pgstac/pyproject.tomlto locate the canonical staged base files - Uses
results.temporary_local_dbviapgpkgto diff the source and target staged bases - Prepends
000_idempotent_pre.sql - Appends
998_idempotent_post.sqlandSELECT set_version(...) - Writes
migrations/pgstac--0.9.10--0.9.11.sql
Important:
scripts/stageversionregenerates*unreleased*migration files on each run.- If you hand-edit an incremental migration, do not rerun
stageversionunless you want those edits overwritten. - After hand-editing an incremental migration, rebuild the baked artifact:
uv run --directory src/pgstac-migrate pgstac-migrate build-artifact - Validate with
scripts/test --migrations(orscripts/testfor the full gate).
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 versionspgstac-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.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.
Tests create pgstac_test_db_template from pgstac.sql, then clone it per test suite:
pgstac_test_pgtap– PGTap testspgstac_test_basicsql– basic SQL testspgstac_test_pypgstac– pytest (function-scoped fixture creates fresh DB per test)
- PGTap: SQL assertions in
src/pgstac/tests/pgtap.sql - Basic SQL:
.sqlfiles insrc/pgstac/tests/basic/, output compared to.sql.out - Pytest:
src/pypgstac/tests/test_load.py,test_benchmark.py,test_queryables.py,hydration/ - Migration: Installs v0.3.0, migrates to latest, runs all test suites against migrated DB
- pg_dump: Dumps a database with sample data, restores via
pgstac_restore, verifies counts match
db– function-scopedPgstacDBconnected to fresh test DBloader–Loader(db)instance
- Changes only in
src/pgstac/sql/for SQL,src/pypgstac/for Python - Tests added if appropriate
CHANGELOG.mdupdated under## [UNRELEASED]docs/src/release-notes.mdupdated to matchCHANGELOG.md(they must stay identical)- Docs updated if needed
- All tests pass:
scripts/test(orscripts/runinpypgstac --build test --pypgstac)
scripts/stageversion VERSION- Review generated incremental migration for correctness
- If hand-edited, run
uv run --directory src/pgstac-migrate pgstac-migrate build-artifact scripts/test --migrations- Move CHANGELOG "Unreleased" → new version
- Copy updated
CHANGELOG.mdtodocs/src/release-notes.md(keep identical) - Create PR, merge
git tag vVERSION && git push origin vVERSION- CI publishes
pypgstacandpgstac-migrateto PyPI plus the ghcr.io images (requires trusted publishers for both PyPI projects on.github/workflows/release.ymlwith thepypienvironment)
- Edit the appropriate file in
src/pgstac/sql/(useCREATE OR REPLACE FUNCTION) - Add
SECURITY DEFINERif the function modifies tables - Grant execute in
998_idempotent_post.sqlif needed - Add PGTap or basic SQL tests
INSERT INTO queryables (name, definition, property_wrapper, property_index_type)
VALUES ('prop_name', '{"$ref": "..."}', 'to_int', 'BTREE')
ON CONFLICT DO NOTHING;scripts/loadsampledata