Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/backend-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Backend tests

on:
pull_request:
paths:
- "backend/**"
- ".github/workflows/backend-tests.yml"
push:
branches: [main]
paths:
- "backend/**"
- ".github/workflows/backend-tests.yml"

permissions:
contents: read

jobs:
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: false
- uses: astral-sh/setup-uv@v6
with:
version: "0.11.21"
enable-cache: true
cache-dependency-glob: backend/uv.lock
- name: Install locked Python dependencies
working-directory: backend
run: uv sync --frozen --python 3.13
# No warehouse.duckdb or DuckLake exists here: tests that read real data
# are marked `requires_warehouse` and skip (see tests/conftest.py).
# test_base.py still imports the deleted data_collection_OLD package; drop
# the --ignore once it is ported to data_collection.base.
- name: Run the backend test suite
working-directory: backend
run: uv run --frozen pytest -q --ignore=tests/test_base.py
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ npm run format:check # Check formatting without modifying
uvicorn api.main:app --reload --port 6767 # API server on localhost:6767
pip install -r requirements.txt # Install dependencies
streamlit run Home.py # Legacy Streamlit interface (not primary)
uv run sqlfluff lint query/sql build/sql # Lint the Jinja SQL templates
uv run sqlfluff fix query/sql build/sql # Auto-fix SQL lint issues
uv run sqlfluff lint query/sql data_cleaning/sql # Lint the Jinja SQL templates
uv run sqlfluff fix query/sql data_cleaning/sql # Auto-fix SQL lint issues
```

SQL templates under `backend/query/sql/` and `backend/build/sql/` are Jinja
SQL templates under `backend/query/sql/` and `backend/data_cleaning/sql/` are Jinja
templates rendered by `query/sql_render.py`, which also holds the
general-purpose filter compiler (it emits `$N` prepared-statement placeholders
plus a params list — execute with `DB.execute(sql, params)`). Lint-time
Expand Down Expand Up @@ -72,15 +72,15 @@ template context lives in `backend/.sqlfluff`.

- `api/routes/get_routes/get_filters.py` — `GET /load/mapping/{dataset}/filters` — returns hierarchical filter tree
- `api/routes/get_routes/get_wholedata.py` — full dataset fetch with optional filters
- `api/routes/post_routes/post_census.py` — Census/ACS-5 data
- `api/routes/post_routes/post_acs5_db.py` — Census ACS-5 tidy and time-series data (`/load/acs5-db/...`)
- `api/routes/post_routes/post_zoning.py` — Zoning data

**Response model:** `api/models/response_models.py` — `APIResponse` with `data`, `tableData`, and `metadata` fields.

**Query layer (`query/`):**

- `sql_render.py` — Jinja SQL rendering and the filter compiler (`sql_filter_block`, `compile_where`)
- `timeseries_db.py` — DuckDB access to census time-series tables
- `acs5.py` — ACS-5 tidy and time-series queries against `warehouse.duckdb`
- `production_db.py` — lazy DuckDB connection

**ETL census helpers:** `data_collection/census.py` (`tidy_census`, `split_name_col`).
Expand Down
4 changes: 2 additions & 2 deletions backend/.sqlfluff
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SQL linting for the Jinja SQL templates in query/sql/ and build/sql/.
# Run from backend/: uv run sqlfluff lint query/sql build/sql
# SQL linting for the Jinja SQL templates in query/sql/ and data_cleaning/sql/.
# Run from backend/: uv run sqlfluff lint query/sql data_cleaning/sql
#
# The [templater:jinja:context] section supplies representative values for the
# fragments that Python (sql_render.py) injects at runtime, so the linter
Expand Down
2 changes: 0 additions & 2 deletions backend/api/routes/post_routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from .post_acs5_db import router as post_acs5_router
from .post_ambulance import router as post_ambulance_router
from .post_cdc import router as post_cdc_router
from .post_census import router as post_census_router
from .post_comparison import router as post_comparison_router
from .post_export import router as post_export_router
from .post_flood import router as post_flood_router
Expand All @@ -12,7 +11,6 @@

all_post_routers = [
post_zoning_router,
post_census_router,
post_acs5_router,
post_qcew_router,
post_wastewater_router,
Expand Down
67 changes: 0 additions & 67 deletions backend/api/routes/post_routes/post_census.py

This file was deleted.

4 changes: 2 additions & 2 deletions backend/data_cleaning/sql/zoning_empty_geom.sql
Comment thread
iansargent marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ gaps AS (
t.id,
t.NAME,
ST_DIFFERENCE(
ST_GeomFromWKB(geometry),
ST_GeomFromWKB(t.geometry),
COALESCE(c.geom, ST_GEOMFROMTEXT('POLYGON EMPTY'))
) AS geom
FROM town_boundaries AS t
Expand All @@ -41,7 +41,7 @@ exploded AS (
SELECT
g.id,
g.NAME,
UNNEST(ST_DUMP(geom), recursive := true) -- noqa: AL03
UNNEST(ST_DUMP(g.geom), recursive := true) -- noqa: AL03
FROM gaps AS g
WHERE NOT ST_ISEMPTY(g.geom)
),
Expand Down
2 changes: 1 addition & 1 deletion backend/data_cleaning/sql/zoning_rules.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ FROM (
INTO
NAME col_name
VALUE val
)
)
4 changes: 2 additions & 2 deletions backend/query/sql/acs5/acs5_county_compare.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ SELECT
c.county,
ST_ASGEOJSON(ST_GeomFromWKB(c.geometry)) AS geometry
FROM {{ table }} AS a
-- County rows in the tidy tables carry no geoid (it's only populated for
-- towns); the county FIPS is what identifies them.
LEFT JOIN vt_county_lines_geom AS c
-- County rows in the tidy tables carry no geoid (it's only populated for
-- towns); the county FIPS is what identifies them.
ON a.county_fips = c.geoid

{{ where_string }}
2 changes: 1 addition & 1 deletion backend/query/sql/acs5/acs5_timeseries.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SELECT *
FROM {{ table }}
{{ where_string }}
ORDER BY year
ORDER BY year
4 changes: 2 additions & 2 deletions backend/query/sql/ambulance/ambulance_geo_query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ FROM (
'type', 'Feature',
'geometry', ST_AsGeoJSON(
ST_Simplify(ST_GeomFromWKB(g.geometry), 0.0001)
)::JSON,
)::JSON,
'properties', json_object(
'Certification Level', i.cert_level,
'Acres', ROUND(g.shape__area, 2),
Expand All @@ -29,4 +29,4 @@ FROM (
LEFT JOIN VCGI_ambulanceService_colors AS c
ON i.cert_level = c.certification_level
{{ join_filter_block }}
) AS features
) AS features
16 changes: 10 additions & 6 deletions backend/query/sql/cdc/county_places.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
-- Filter PLACES before joining the county lines: the compiled WHERE clause
-- names columns unqualified, and both tables have county, county_fips and geoid.
WITH places AS (
SELECT *
FROM cdc_places_county
{{ where_string }}
)

SELECT
p.geoid,
p.measure,
Comment thread
duaneatat marked this conversation as resolved.
Resolved
Expand All @@ -6,10 +14,6 @@ SELECT
ROUND(p.natl_pct * 100, 2) AS natl_pct,
p.county,
ST_ASGEOJSON(ST_GeomFromWKB(c.geometry)) AS geometry
FROM cdc_places_county AS p
LEFT JOIN (
SELECT * EXCLUDE(county)
FROM vt_county_lines_geom
) AS c
FROM places AS p
LEFT JOIN vt_county_lines_geom AS c
ON p.geoid = c.geoid
{{ where_string }}
8 changes: 4 additions & 4 deletions backend/query/sql/wastewater/soil_suitability_geo_query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ FROM (
json_object(
'type', 'Feature',
'geometry',
ST_AsGeoJSON(
ST_Simplify(g.geometry, 0.0001)
)::JSON,
ST_AsGeoJSON(
ST_Simplify(g.geometry, 0.0001)
)::JSON,
'properties', json_object(
'Suitability', i.suitability,
'Acres', ROUND(i.acres, 2),
Expand All @@ -29,4 +29,4 @@ FROM (
LEFT JOIN VersoWastewater_soilSuitability_colors AS c
ON i.suitability = c.soil_suitability
{{ where_string }}
) AS features;
) AS features;
6 changes: 3 additions & 3 deletions backend/query/sql/zoning/footprints.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{{ cte_filter_block }}
SELECT
SELECT
b.object_id,
b.town,
b.county,
b.geoid,
b.geoid,
b.name,
b.height_ft,
b.building_type,
ST_AsGeoJSON(b.geometry) AS geometry,
ST_AsGeoJSON(b.geometry) AS geometry
FROM VCGI_buildingFootprints_geom AS b
{{ join_filter_block }}
4 changes: 2 additions & 2 deletions backend/query/sql/zoning/unzoned.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- Towns' unzoned areas as a GeoJSON FeatureCollection, drawn as a grey layer
-- underneath the zoning districts. Built by build/sql/zoning_empty_geom.sql,
-- which already drops sliver-sized gaps.
-- underneath the zoning districts. Built by
-- data_cleaning/sql/zoning_empty_geom.sql, which already drops sliver-sized gaps.
--
-- This layer does not depend on the sidebar filters: it is the same "we have no
-- data here" backdrop regardless of which districts are being shown.
Expand Down
7 changes: 4 additions & 3 deletions backend/query/sql_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
**Description**:
Jinja SQL rendering plus the general-purpose filter compiler.

SQL files under `query/sql/` and `build/sql/` are Jinja templates, so they can
be linted with sqlfluff (`uv run sqlfluff lint query/sql build/sql` from
`backend/`; lint-time template context lives in `backend/.sqlfluff`).
SQL files under `query/sql/` and `data_cleaning/sql/` are Jinja templates, so
they can be linted with sqlfluff (`uv run sqlfluff lint query/sql
data_cleaning/sql` from `backend/`; lint-time template context lives in
`backend/.sqlfluff`).

Python compiles FilterSource objects into SQL fragments (filter CTEs, joins,
and WHERE clauses) and injects them through the Jinja context. This keeps the
Expand Down
Loading
Loading