diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e04a346..f13d3ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 36 PostgreSQL collation maintenance visibility (2026-06-17) + +- Added database collation version reporting to `scripts/live_migration_smoke.sh`. +- The live smoke now prints `COLLATION_VERSION_MISMATCH` plus the exact `ALTER DATABASE ... REFRESH COLLATION VERSION` acknowledgement command when an old PostGIS volume is reused on a newer runtime. +- Documented the Unraid maintenance procedure and backup/index review guidance. +- Added regression coverage for the collation mismatch reporting path. +- No API contracts, migrations, product features, provider fetching or AI behavior were introduced. + ## Sprint 35 Docker runtime secret hygiene (2026-06-17) - Removed embedded PostGIS database name/user/password defaults from `deploy/unraid/Dockerfile.all-in-one` image metadata. diff --git a/backend/tests/test_live_migration_smoke_script.py b/backend/tests/test_live_migration_smoke_script.py index 1302fab0..039da361 100644 --- a/backend/tests/test_live_migration_smoke_script.py +++ b/backend/tests/test_live_migration_smoke_script.py @@ -22,3 +22,13 @@ def test_live_migration_smoke_checks_required_runtime_schema_objects() -> None: assert '"public.detections"' in content assert '"public.segmentations"' in content assert '"public.ix_segmentations_geometry"' in content + + +def test_live_migration_smoke_reports_collation_version_mismatch_without_failing() -> None: + script = Path(__file__).resolve().parents[2] / "scripts" / "live_migration_smoke.sh" + content = script.read_text(encoding="utf-8") + + assert "pg_database_collation_actual_version(oid)" in content + assert "COLLATION_VERSION_MISMATCH" in content + assert "REFRESH COLLATION VERSION" in content + assert "Database collation version: ok" in content diff --git a/deploy/unraid/README.md b/deploy/unraid/README.md index be700c6e..f01f1493 100644 --- a/deploy/unraid/README.md +++ b/deploy/unraid/README.md @@ -91,6 +91,22 @@ curl -I "http://192.168.10.150:${GEOINTEL_FRONTEND_PORT:-1202}/geointel-icon.svg curl -I "http://192.168.10.150:${GEOINTEL_FRONTEND_PORT:-1202}/geointel-icon.png" ``` +The live migration smoke also checks PostgreSQL database collation metadata. +When reusing a PostGIS volume created by an older Debian/glibc runtime, it may +print `COLLATION_VERSION_MISMATCH`. This is a maintenance warning, not an app +startup failure. Review backups first, then acknowledge the new runtime +collation version inside the running container: + +```bash +docker exec -it geointel psql -U "${GEOINTEL_POSTGRES_USER:-geointel}" -d "${GEOINTEL_POSTGRES_DB:-geointel}" +ALTER DATABASE "geointel" REFRESH COLLATION VERSION; +``` + +If you rely on text indexes with locale-specific ordering, plan a maintenance +window and rebuild the affected indexes before acknowledging the version. The +current GeoIntel V1 spatial workflows primarily use UUIDs, JSON metadata and +PostGIS geometry indexes, but the warning should still be tracked explicitly. + ## Change the browser port Edit `.env`: diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 53a369b3..015e088b 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1627,3 +1627,30 @@ Limitations: Next recommended pass: - Run release readiness, rebuild/deploy Tower and verify browser/GIS/demo smoke again. + +## Sprint 36 PostgreSQL collation maintenance visibility (2026-06-17) + +Changed: +- Added PostgreSQL database collation version reporting to `scripts/live_migration_smoke.sh`. +- The live smoke now prints `COLLATION_VERSION_MISMATCH` with stored and actual versions when a reused PostGIS volume was created under an older libc/collation runtime. +- The smoke also prints the exact `ALTER DATABASE "... " REFRESH COLLATION VERSION;` acknowledgement command, but does not run it automatically. +- Documented the Unraid maintenance procedure and backup/index review guidance. +- Added regression coverage for the collation reporting path. + +Tested: +- `cd backend && python -m pytest tests/test_live_migration_smoke_script.py -q` +- `bash -n scripts/live_migration_smoke.sh` +- `python -m compileall backend/app` +- `cd backend && python -m pytest -W error::DeprecationWarning` +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- `bash scripts/run_readiness_check.sh` + +Open: +- Run the updated smoke against Tower to confirm whether the reused volume still reports a collation mismatch. + +Limitations: +- The smoke reports and documents the maintenance action. It intentionally does not mutate collation metadata automatically. + +Next recommended pass: +- Run release readiness, deploy Tower and decide whether to manually acknowledge the current collation version. diff --git a/docs/TODO.md b/docs/TODO.md index a72fa776..c89c6b60 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -13,6 +13,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add Unraid Compose template assets with editable ports, storage path and app icon. - [x] Add single-container Unraid runtime with embedded PostGIS, backend and frontend. - [x] Remove embedded PostGIS password defaults from all-in-one Docker image metadata. +- [x] Report reused-volume PostgreSQL collation mismatches in live migration smoke. ## Current implementation status diff --git a/scripts/live_migration_smoke.sh b/scripts/live_migration_smoke.sh index 2b662c6a..9a89ed55 100755 --- a/scripts/live_migration_smoke.sh +++ b/scripts/live_migration_smoke.sh @@ -69,6 +69,26 @@ required_objects = [ with get_engine().connect() as connection: postgis_version = connection.execute(text("SELECT PostGIS_Version()")).scalar() print(f"PostGIS version: {postgis_version}") + collation_row = connection.execute( + text( + "SELECT datname, datcollversion, pg_database_collation_actual_version(oid) AS actual_version " + "FROM pg_database WHERE datname = current_database()" + ) + ).mappings().one() + stored_version = collation_row["datcollversion"] + actual_version = collation_row["actual_version"] + if stored_version != actual_version: + database_name = str(collation_row["datname"]).replace('"', '""') + print( + "COLLATION_VERSION_MISMATCH: " + f"database={collation_row['datname']} stored={stored_version} actual={actual_version}" + ) + print( + "After reviewing backups and indexes, acknowledge with: " + f'ALTER DATABASE "{database_name}" REFRESH COLLATION VERSION;' + ) + else: + print("Database collation version: ok") missing = [ object_name @@ -154,6 +174,26 @@ required_objects = [ with get_engine().connect() as connection: postgis_version = connection.execute(text("SELECT PostGIS_Version()")).scalar() print(f"PostGIS version: {postgis_version}") + collation_row = connection.execute( + text( + "SELECT datname, datcollversion, pg_database_collation_actual_version(oid) AS actual_version " + "FROM pg_database WHERE datname = current_database()" + ) + ).mappings().one() + stored_version = collation_row["datcollversion"] + actual_version = collation_row["actual_version"] + if stored_version != actual_version: + database_name = str(collation_row["datname"]).replace('"', '""') + print( + "COLLATION_VERSION_MISMATCH: " + f"database={collation_row['datname']} stored={stored_version} actual={actual_version}" + ) + print( + "After reviewing backups and indexes, acknowledge with: " + f'ALTER DATABASE "{database_name}" REFRESH COLLATION VERSION;' + ) + else: + print("Database collation version: ok") missing = [ object_name @@ -267,6 +307,26 @@ required_objects = [ with get_engine().connect() as connection: postgis_version = connection.execute(text("SELECT PostGIS_Version()")).scalar() print(f"PostGIS version: {postgis_version}") + collation_row = connection.execute( + text( + "SELECT datname, datcollversion, pg_database_collation_actual_version(oid) AS actual_version " + "FROM pg_database WHERE datname = current_database()" + ) + ).mappings().one() + stored_version = collation_row["datcollversion"] + actual_version = collation_row["actual_version"] + if stored_version != actual_version: + database_name = str(collation_row["datname"]).replace('"', '""') + print( + "COLLATION_VERSION_MISMATCH: " + f"database={collation_row['datname']} stored={stored_version} actual={actual_version}" + ) + print( + "After reviewing backups and indexes, acknowledge with: " + f'ALTER DATABASE "{database_name}" REFRESH COLLATION VERSION;' + ) + else: + print("Database collation version: ok") missing = [ object_name