Skip to content

Commit 7f06e7c

Browse files
committed
Fix Postgres live snapshot queries
1 parent 94e4bf5 commit 7f06e7c

4 files changed

Lines changed: 171 additions & 15 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ jobs:
7777
- run: uv run mypy python services
7878
- run: env -u DATABASE_URL uv run pytest tests/backend
7979
- run: uv run python -m seekandscore.db.migrate upgrade
80+
- name: Postgres repository integration
81+
run: SEEKANDSCORE_TEST_DATABASE_URL="$DATABASE_URL" env -u DATABASE_URL uv run pytest tests/backend/test_postgres_repository_integration.py
8082

8183
web:
8284
name: Web quality gates

python/seekandscore/acquisition/repository.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,11 @@ def latest_complete_run(
249249
sa.select(source_run_table.c.payload)
250250
.where(
251251
source_run_table.c.source_id == source_id,
252-
source_run_table.c.payload["run_profile"].astext == run_profile.value,
253-
source_run_table.c.payload["status"].astext.in_(
254-
("succeeded", "succeeded_unchanged")
255-
),
256-
source_run_table.c.payload["partial"].astext == "false",
252+
source_run_table.c.payload["run_profile"].as_string() == run_profile.value,
253+
source_run_table.c.payload["status"]
254+
.as_string()
255+
.in_(("succeeded", "succeeded_unchanged")),
256+
source_run_table.c.payload["partial"].as_boolean().is_(False),
257257
)
258258
.order_by(source_run_table.c.started_at.desc(), source_run_table.c.id.desc())
259259
)
@@ -373,15 +373,15 @@ def list_latest_observations(
373373
if artifact_ids is not None:
374374
filters.append(observation_table.c.artifact_id.in_(artifact_ids))
375375
if cities is not None:
376-
filters.append(observation_table.c.payload["situs_city"].astext.in_(cities))
376+
filters.append(observation_table.c.payload["situs_city"].as_string().in_(cities))
377377
ranked = (
378378
sa.select(
379379
observation_table.c.payload,
380380
sa.func.row_number()
381381
.over(
382382
partition_by=observation_table.c.source_record_id,
383383
order_by=sa.cast(
384-
observation_table.c.payload["observed_at"].astext,
384+
observation_table.c.payload["observed_at"].as_string(),
385385
sa.DateTime(timezone=True),
386386
).desc(),
387387
)
@@ -394,13 +394,8 @@ def list_latest_observations(
394394
sa.select(ranked.c.payload)
395395
.where(ranked.c.version_rank == 1)
396396
.order_by(
397-
sa.cast(
398-
ranked.c.payload["tcad_acres"].astext,
399-
sa.Float(),
400-
)
401-
.desc()
402-
.nullslast(),
403-
ranked.c.payload["local_parcel_id"].astext,
397+
ranked.c.payload["tcad_acres"].as_float().desc().nullslast(),
398+
ranked.c.payload["local_parcel_id"].as_string(),
404399
)
405400
.limit(limit)
406401
.offset(offset)
@@ -424,7 +419,7 @@ def count_latest_observations(
424419
statement = statement.where(observation_table.c.artifact_id.in_(artifact_ids))
425420
if cities is not None:
426421
statement = statement.where(
427-
observation_table.c.payload["situs_city"].astext.in_(cities)
422+
observation_table.c.payload["situs_city"].as_string().in_(cities)
428423
)
429424
with self.engine.connect() as connection:
430425
return int(connection.scalar(statement) or 0)

tests/backend/test_live_candidates.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,35 @@ def test_postgres_insert_count_uses_returned_ids_not_indeterminate_rowcount() ->
109109
assert inserted == 2
110110

111111

112+
def test_postgres_snapshot_queries_use_supported_sqlalchemy_json_accessors() -> None:
113+
engine = MagicMock()
114+
connection = engine.connect.return_value.__enter__.return_value
115+
connection.scalars.return_value.all.return_value = []
116+
connection.scalar.return_value = 0
117+
repository = PostgresAcquisitionRepository(engine)
118+
artifact_id = UUID("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")
119+
120+
assert (
121+
repository.latest_complete_run("travis_tcad_parcels", run_profile=SourceRunProfile.COHORT)
122+
is None
123+
)
124+
assert (
125+
repository.list_latest_observations(
126+
limit=25,
127+
artifact_ids=(artifact_id,),
128+
cities=("DEL VALLE", "MANOR"),
129+
)
130+
== ()
131+
)
132+
assert (
133+
repository.count_latest_observations(
134+
artifact_ids=(artifact_id,),
135+
cities=("DEL VALLE", "MANOR"),
136+
)
137+
== 0
138+
)
139+
140+
112141
def test_live_projection_labels_assessor_values_and_oz_as_unverified() -> None:
113142
repository = LiveCandidateRepository(
114143
seeded_repository(),
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Optional real-Postgres regression for JSON-backed live snapshot queries."""
2+
3+
import os
4+
from datetime import UTC, datetime
5+
from uuid import uuid4
6+
7+
import pytest
8+
import sqlalchemy as sa
9+
10+
from seekandscore.acquisition.models import (
11+
NormalizedParcelObservation,
12+
RawArtifact,
13+
SourceRun,
14+
SourceRunProfile,
15+
SourceRunStatus,
16+
)
17+
from seekandscore.acquisition.repository import (
18+
PostgresAcquisitionRepository,
19+
observation_table,
20+
raw_artifact_table,
21+
source_run_table,
22+
)
23+
from seekandscore.readmodels import LiveCandidateRepository
24+
from seekandscore.registry import InMemorySourceRegistry
25+
26+
TEST_DATABASE_URL = os.getenv("SEEKANDSCORE_TEST_DATABASE_URL")
27+
28+
29+
@pytest.mark.skipif(not TEST_DATABASE_URL, reason="SEEKANDSCORE_TEST_DATABASE_URL is not set")
30+
def test_artifact_scoped_snapshot_queries_execute_on_postgres() -> None:
31+
assert TEST_DATABASE_URL is not None
32+
database_name = sa.engine.make_url(TEST_DATABASE_URL).database or ""
33+
if not database_name.endswith("_test"):
34+
pytest.fail("SEEKANDSCORE_TEST_DATABASE_URL must name a database ending in _test")
35+
36+
engine = sa.create_engine(TEST_DATABASE_URL)
37+
repository = PostgresAcquisitionRepository(engine)
38+
now = datetime.now(UTC)
39+
artifact_id = uuid4()
40+
observation_id = uuid4()
41+
run_id = uuid4()
42+
sha256 = artifact_id.hex * 2
43+
artifact = RawArtifact(
44+
id=artifact_id,
45+
source_id="travis_tcad_parcels",
46+
sha256=sha256,
47+
byte_count=2,
48+
media_type="application/json",
49+
storage_uri=f"s3://private-test/{sha256}.json",
50+
original_uri="https://example.invalid/query",
51+
request_params={},
52+
retrieved_at=now,
53+
)
54+
observation = NormalizedParcelObservation(
55+
id=observation_id,
56+
source_id="travis_tcad_parcels",
57+
source_record_id=str(observation_id.int),
58+
artifact_id=artifact_id,
59+
artifact_sha256=sha256,
60+
parser_version="travis-tcad-parcel-v1",
61+
jurisdiction_id="us-tx-travis",
62+
local_parcel_id=str(observation_id.int),
63+
geographic_id=None,
64+
situs_address="100 TEST RD",
65+
situs_city="DEL VALLE",
66+
situs_zip="78617",
67+
tcad_acres=2.0,
68+
observed_at=now,
69+
)
70+
run = SourceRun(
71+
id=run_id,
72+
source_id="travis_tcad_parcels",
73+
status=SourceRunStatus.SUCCEEDED,
74+
requested_at=now,
75+
started_at=now,
76+
completed_at=now,
77+
retrieved_at=now,
78+
records_fetched=1,
79+
observations_created=1,
80+
artifact_ids=(artifact_id,),
81+
adapter_version="travis-tcad-arcgis-v1",
82+
parser_version="travis-tcad-parcel-v1",
83+
run_profile=SourceRunProfile.COHORT,
84+
configuration_hash="a" * 64,
85+
activation_id="test-only",
86+
)
87+
88+
try:
89+
repository.save_artifact(artifact)
90+
assert repository.save_observations((observation,)) == 1
91+
repository.save_run(run)
92+
93+
assert (
94+
repository.latest_complete_run(
95+
"travis_tcad_parcels", run_profile=SourceRunProfile.COHORT
96+
)
97+
== run
98+
)
99+
assert repository.list_latest_observations(
100+
limit=25,
101+
artifact_ids=(artifact_id,),
102+
cities=("DEL VALLE", "MANOR"),
103+
) == (observation,)
104+
assert (
105+
repository.count_latest_observations(
106+
artifact_ids=(artifact_id,),
107+
cities=("DEL VALLE", "MANOR"),
108+
)
109+
== 1
110+
)
111+
candidates = LiveCandidateRepository(
112+
repository,
113+
InMemorySourceRegistry(),
114+
display_enabled=True,
115+
)
116+
page = candidates.list(limit=25, cursor=None, dataset_mode="live")
117+
assert page.dataset_status == "current"
118+
assert page.total == 1
119+
assert len(page.items) == 1
120+
assert candidates.get(page.items[0].id) == page.items[0]
121+
finally:
122+
with engine.begin() as connection:
123+
connection.execute(sa.delete(source_run_table).where(source_run_table.c.id == run_id))
124+
connection.execute(
125+
sa.delete(observation_table).where(observation_table.c.id == observation_id)
126+
)
127+
connection.execute(
128+
sa.delete(raw_artifact_table).where(raw_artifact_table.c.id == artifact_id)
129+
)
130+
engine.dispose()

0 commit comments

Comments
 (0)