diff --git a/changelog.d/economy-cache-decile-fractions.fixed.md b/changelog.d/economy-cache-decile-fractions.fixed.md new file mode 100644 index 000000000..3bd169d74 --- /dev/null +++ b/changelog.d/economy-cache-decile-fractions.fixed.md @@ -0,0 +1 @@ +Raised the economy result cache schema version from 3 to 4, so economy results cached while the simulation API returned decile and UK wealth decile relative changes as percentages (x100) instead of fractions are no longer served, and are recomputed on the next request. Budget-window results share this cache version, so they are recomputed too. diff --git a/policyengine_api/constants.py b/policyengine_api/constants.py index d78a7fb2e..e753f1b57 100644 --- a/policyengine_api/constants.py +++ b/policyengine_api/constants.py @@ -194,7 +194,13 @@ def get_bundle_default_dataset_option(country_id: str) -> dict: ) or _resolve_distribution_version(_dist_versions, "policyengine-core", "policyengine") RUNTIME_CACHE_SCHEMA_VERSIONS = { - "economy_impact": 3, + # 4: economy results cached under 3 can hold decile.relative and the UK + # wealth_decile.relative as percentages (x100) instead of fractions, from + # the simulation API (PolicyEngine/policyengine-sim-api#827). Economy cache + # keys do not change when only the simulation API's code changes, so this + # version was raised to stop serving those results. The budget-window + # cache key uses the same version. + "economy_impact": 4, "report_output": 3, } # Runtime cache versions are stored in legacy VARCHAR(10) columns, and keep at diff --git a/tests/unit/services/test_reform_impacts_service.py b/tests/unit/services/test_reform_impacts_service.py index 00f7af708..70c802193 100644 --- a/tests/unit/services/test_reform_impacts_service.py +++ b/tests/unit/services/test_reform_impacts_service.py @@ -3,6 +3,8 @@ import pytest +from policyengine_api import constants +from policyengine_api.constants import get_economy_impact_cache_version from policyengine_api.runtime_cache.core import CacheNamespace from policyengine_api.runtime_cache.fake import InMemoryCacheBackend from policyengine_api.runtime_cache.reform_impacts import ( @@ -197,3 +199,58 @@ def test_reform_impact_service_deletes_only_matching_computing_rows( def test_reform_impact_transitions_return_none_for_missing_execution(service): assert service.set_error_reform_impact("missing-job", message="missing") is None + + +def test_impact_cached_under_previous_economy_schema_version_is_not_matched( + service, +): + # Economy results cached under schema version 3 can hold decile relative + # changes as percentages (x100) from the simulation API + # (PolicyEngine/policyengine-sim-api#827). A lookup built from the + # current cache version must not find them. + current_cache_version = get_economy_impact_cache_version(SCOPE.country_id) + with pytest.MonkeyPatch.context() as schema_version_patch: + schema_version_patch.setitem( + constants.RUNTIME_CACHE_SCHEMA_VERSIONS, "economy_impact", 3 + ) + percentage_decile_cache_version = get_economy_impact_cache_version( + SCOPE.country_id + ) + + options_hash = "decile-hash" + stale_identity = ReformImpactIdentity( + scope=SCOPE, + api_version=percentage_decile_cache_version, + options_hash=options_hash, + ) + service.set_reform_impact( + stale_identity, + ReformImpactExecution( + execution_id="percentage-decile-job", + start_time=datetime(2026, 7, 1), + ), + options={"hash": options_hash}, + status="computing", + reform_impact_json={}, + ) + service.set_complete_reform_impact( + "percentage-decile-job", + reform_impact_json={"decile": {"relative": {"1": 1.5}}}, + ) + + current_identity = ReformImpactIdentity( + scope=SCOPE, + api_version=current_cache_version, + options_hash=options_hash, + ) + + assert [ + impact.execution_id for impact in service.get_all_reform_impacts(stale_identity) + ] == ["percentage-decile-job"] + assert service.get_all_reform_impacts(current_identity) == [] + assert ( + service.get_all_reform_impacts_by_options_hash_prefix( + current_identity, f"{options_hash}%" + ) + == [] + ) diff --git a/tests/unit/test_constants.py b/tests/unit/test_constants.py index 3861b0c30..328151b12 100644 --- a/tests/unit/test_constants.py +++ b/tests/unit/test_constants.py @@ -9,12 +9,15 @@ POLICYENGINE_CORE_VERSION, POLICYENGINE_VERSION, REGION_PREFIXES, + RUNTIME_CACHE_SCHEMA_VERSIONS, + RUNTIME_CACHE_VERSION_LENGTH, UK_REGION_TYPES, US_REGION_TYPES, _bundle_dataset_display_label, _load_policyengine_bundle, _normalize_distribution_name, _resolve_distribution_version, + get_economy_impact_cache_version, get_py_manifest, ) @@ -111,6 +114,24 @@ def test__has_exactly_three_prefixes(self): assert len(REGION_PREFIXES["us"]) == 3 +class TestEconomyImpactCacheVersion: + # Since its move to .py 4.x, the simulation API returned decile.relative + # and the UK wealth_decile.relative as percentages (x100) instead of + # fractions (PolicyEngine/policyengine-sim-api#827). Economy results + # cached under schema version 3 can hold those values, and the cache key + # does not change when the simulation API's code does, so version 4 makes + # every economy cache lookup miss them and recompute. + def test__economy_impact_schema_version_is_4(self): + assert RUNTIME_CACHE_SCHEMA_VERSIONS["economy_impact"] == 4 + + @pytest.mark.parametrize("country_id", ["us", "uk"]) + def test__economy_impact_cache_version_carries_schema_version_4(self, country_id): + cache_version = get_economy_impact_cache_version(country_id) + + assert cache_version.startswith("e4") + assert len(cache_version) == RUNTIME_CACHE_VERSION_LENGTH + + class TestDistributionVersionHelpers: def test__normalize_distribution_name(self): assert _normalize_distribution_name("policyengine_core") == (