Fix metric column name casing: DuckDB returns lowercase, align everywhere

SQLMesh normalizes unquoted identifiers to lowercase in physical tables,
so commodity_metrics columns are e.g. 'production' not 'Production'.
Update ALLOWED_METRICS, all analytics.py SQL queries, dashboard routes,
and both dashboard templates (Jinja + JS chart references) to use
lowercase column names consistently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-20 17:14:52 +01:00
parent 423fb8c619
commit d569ba0162
4 changed files with 37 additions and 37 deletions

View File

@@ -14,19 +14,19 @@ COFFEE_COMMODITY_CODE = 711100
# Metrics safe for user-facing queries (prevents SQL injection in dynamic column refs)
ALLOWED_METRICS = frozenset({
"Production",
"Imports",
"Exports",
"Total_Distribution",
"Ending_Stocks",
"Beginning_Stocks",
"Total_Supply",
"Domestic_Consumption",
"Net_Supply",
"Trade_Balance",
"Supply_Demand_Balance",
"Stock_to_Use_Ratio_pct",
"Production_YoY_pct",
"production",
"imports",
"exports",
"total_distribution",
"ending_stocks",
"beginning_stocks",
"total_supply",
"domestic_consumption",
"net_supply",
"trade_balance",
"supply_demand_balance",
"stock_to_use_ratio_pct",
"production_yoy_pct",
})
_conn: duckdb.DuckDBPyConnection | None = None
@@ -152,7 +152,7 @@ async def get_stock_to_use_trend(commodity_code: int) -> list[dict]:
"""Global stock-to-use ratio over time."""
return await fetch_analytics(
"""
SELECT market_year, Stock_to_Use_Ratio_pct
SELECT market_year, stock_to_use_ratio_pct
FROM serving.commodity_metrics
WHERE commodity_code = ?
AND country_name = 'Global'
@@ -166,7 +166,7 @@ async def get_supply_demand_balance(commodity_code: int) -> list[dict]:
"""Global supply-demand balance trend."""
return await fetch_analytics(
"""
SELECT market_year, Production, Total_Distribution, Supply_Demand_Balance
SELECT market_year, production, total_distribution, supply_demand_balance
FROM serving.commodity_metrics
WHERE commodity_code = ?
AND country_name = 'Global'
@@ -189,13 +189,13 @@ async def get_production_yoy_by_country(
WHERE commodity_code = ? AND country_code IS NOT NULL
)
SELECT country_name, country_code, market_year,
Production, Production_YoY_pct
production, production_yoy_pct
FROM serving.commodity_metrics, latest
WHERE commodity_code = ?
AND country_code IS NOT NULL
AND market_year = latest.max_year
AND Production > 0
ORDER BY ABS(Production_YoY_pct) DESC
AND production > 0
ORDER BY ABS(production_yoy_pct) DESC
LIMIT ?
""",
[commodity_code, commodity_code, limit],

View File

@@ -104,9 +104,9 @@ async def index():
time_series, top_producers, stu_trend, balance, yoy = await asyncio.gather(
analytics.get_global_time_series(
analytics.COFFEE_COMMODITY_CODE,
["Production", "Exports", "Imports", "Ending_Stocks", "Total_Distribution"],
["production", "exports", "imports", "ending_stocks", "total_distribution"],
),
analytics.get_top_countries(analytics.COFFEE_COMMODITY_CODE, "Production", limit=10),
analytics.get_top_countries(analytics.COFFEE_COMMODITY_CODE, "production", limit=10),
analytics.get_stock_to_use_trend(analytics.COFFEE_COMMODITY_CODE),
analytics.get_supply_demand_balance(analytics.COFFEE_COMMODITY_CODE),
analytics.get_production_yoy_by_country(analytics.COFFEE_COMMODITY_CODE, limit=15),
@@ -147,11 +147,11 @@ async def countries():
plan = (g.get("subscription") or {}).get("plan", "free")
# Get available countries for coffee
all_countries = await analytics.get_top_countries(analytics.COFFEE_COMMODITY_CODE, "Production", limit=50)
all_countries = await analytics.get_top_countries(analytics.COFFEE_COMMODITY_CODE, "production", limit=50)
# Parse query params
selected_codes = request.args.getlist("country")
metric = request.args.get("metric", "Production")
metric = request.args.get("metric", "production")
comparison_data = []
if selected_codes:

View File

@@ -21,7 +21,7 @@
<div>
<label for="metric" class="form-label">Metric</label>
<select name="metric" id="metric" class="form-input" onchange="this.form.submit()">
{% for m in ["Production", "Exports", "Imports", "Ending_Stocks"] %}
{% for m in ["production", "exports", "imports", "ending_stocks"] %}
<option value="{{ m }}" {{ "selected" if metric == m }}>{{ m | replace("_", " ") }}</option>
{% endfor %}
</select>

View File

@@ -18,7 +18,7 @@
<div class="grid-4 mb-8">
<div class="metric-card">
<div class="metric-label">Global Production (latest year)</div>
<div class="metric-value">{{ "{:,.0f}".format(latest.get("Production", 0)) }}</div>
<div class="metric-value">{{ "{:,.0f}".format(latest.get("production", 0)) }}</div>
<div class="metric-sub">1,000 60-kg bags</div>
</div>
@@ -26,7 +26,7 @@
<div class="metric-label">Stock-to-Use Ratio</div>
<div class="metric-value">
{% if stu_trend %}
{{ "{:.1f}".format(stu_trend[-1].get("Stock_to_Use_Ratio_pct", 0)) }}%
{{ "{:.1f}".format(stu_trend[-1].get("stock_to_use_ratio_pct", 0)) }}%
{% else %}
--
{% endif %}
@@ -36,7 +36,7 @@
<div class="metric-card">
<div class="metric-label">Trade Balance</div>
<div class="metric-value">{{ "{:,.0f}".format(latest.get("Exports", 0) - latest.get("Imports", 0)) }}</div>
<div class="metric-value">{{ "{:,.0f}".format(latest.get("exports", 0) - latest.get("imports", 0)) }}</div>
<div class="metric-sub">Exports minus imports</div>
</div>
@@ -90,10 +90,10 @@
{% for row in yoy %}
<tr>
<td>{{ row.country_name }}</td>
<td class="text-right">{{ "{:,.0f}".format(row.Production) }}</td>
<td class="text-right {% if row.Production_YoY_pct and row.Production_YoY_pct > 0 %}text-bean-green{% elif row.Production_YoY_pct and row.Production_YoY_pct < 0 %}text-danger{% endif %}">
{% if row.Production_YoY_pct is not none %}
{{ "{:+.1f}%".format(row.Production_YoY_pct) }}
<td class="text-right">{{ "{:,.0f}".format(row.production) }}</td>
<td class="text-right {% if row.production_yoy_pct and row.production_yoy_pct > 0 %}text-bean-green{% elif row.production_yoy_pct and row.production_yoy_pct < 0 %}text-danger{% endif %}">
{% if row.production_yoy_pct is not none %}
{{ "{:+.1f}%".format(row.production_yoy_pct) }}
{% else %}
--
{% endif %}
@@ -163,11 +163,11 @@ if (tsData.length > 0) {
data: {
labels: tsData.map(r => r.market_year),
datasets: [
{label: 'Production', data: tsData.map(r => r.Production), borderColor: CHART_PALETTE[0], tension: 0.3},
{label: 'Exports', data: tsData.map(r => r.Exports), borderColor: CHART_PALETTE[1], tension: 0.3},
{label: 'Imports', data: tsData.map(r => r.Imports), borderColor: CHART_PALETTE[2], tension: 0.3},
{label: 'Ending Stocks', data: tsData.map(r => r.Ending_Stocks), borderColor: CHART_PALETTE[3], tension: 0.3},
{label: 'Total Distribution', data: tsData.map(r => r.Total_Distribution), borderColor: CHART_PALETTE[4], tension: 0.3},
{label: 'Production', data: tsData.map(r => r.production), borderColor: CHART_PALETTE[0], tension: 0.3},
{label: 'Exports', data: tsData.map(r => r.exports), borderColor: CHART_PALETTE[1], tension: 0.3},
{label: 'Imports', data: tsData.map(r => r.imports), borderColor: CHART_PALETTE[2], tension: 0.3},
{label: 'Ending Stocks', data: tsData.map(r => r.ending_stocks), borderColor: CHART_PALETTE[3], tension: 0.3},
{label: 'Total Distribution', data: tsData.map(r => r.total_distribution), borderColor: CHART_PALETTE[4], tension: 0.3},
]
},
options: {
@@ -187,7 +187,7 @@ if (stuData.length > 0) {
labels: stuData.map(r => r.market_year),
datasets: [{
label: 'Stock-to-Use Ratio (%)',
data: stuData.map(r => r.Stock_to_Use_Ratio_pct),
data: stuData.map(r => r.stock_to_use_ratio_pct),
borderColor: CHART_COLORS.copper,
backgroundColor: 'rgba(180, 83, 9, 0.08)',
fill: true,
@@ -211,7 +211,7 @@ if (topData.length > 0) {
labels: topData.map(r => r.country_name),
datasets: [{
label: 'Production',
data: topData.map(r => r.Production),
data: topData.map(r => r.production),
backgroundColor: CHART_COLORS.copper
}]
},