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:
@@ -14,19 +14,19 @@ COFFEE_COMMODITY_CODE = 711100
|
|||||||
|
|
||||||
# Metrics safe for user-facing queries (prevents SQL injection in dynamic column refs)
|
# Metrics safe for user-facing queries (prevents SQL injection in dynamic column refs)
|
||||||
ALLOWED_METRICS = frozenset({
|
ALLOWED_METRICS = frozenset({
|
||||||
"Production",
|
"production",
|
||||||
"Imports",
|
"imports",
|
||||||
"Exports",
|
"exports",
|
||||||
"Total_Distribution",
|
"total_distribution",
|
||||||
"Ending_Stocks",
|
"ending_stocks",
|
||||||
"Beginning_Stocks",
|
"beginning_stocks",
|
||||||
"Total_Supply",
|
"total_supply",
|
||||||
"Domestic_Consumption",
|
"domestic_consumption",
|
||||||
"Net_Supply",
|
"net_supply",
|
||||||
"Trade_Balance",
|
"trade_balance",
|
||||||
"Supply_Demand_Balance",
|
"supply_demand_balance",
|
||||||
"Stock_to_Use_Ratio_pct",
|
"stock_to_use_ratio_pct",
|
||||||
"Production_YoY_pct",
|
"production_yoy_pct",
|
||||||
})
|
})
|
||||||
|
|
||||||
_conn: duckdb.DuckDBPyConnection | None = None
|
_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."""
|
"""Global stock-to-use ratio over time."""
|
||||||
return await fetch_analytics(
|
return await fetch_analytics(
|
||||||
"""
|
"""
|
||||||
SELECT market_year, Stock_to_Use_Ratio_pct
|
SELECT market_year, stock_to_use_ratio_pct
|
||||||
FROM serving.commodity_metrics
|
FROM serving.commodity_metrics
|
||||||
WHERE commodity_code = ?
|
WHERE commodity_code = ?
|
||||||
AND country_name = 'Global'
|
AND country_name = 'Global'
|
||||||
@@ -166,7 +166,7 @@ async def get_supply_demand_balance(commodity_code: int) -> list[dict]:
|
|||||||
"""Global supply-demand balance trend."""
|
"""Global supply-demand balance trend."""
|
||||||
return await fetch_analytics(
|
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
|
FROM serving.commodity_metrics
|
||||||
WHERE commodity_code = ?
|
WHERE commodity_code = ?
|
||||||
AND country_name = 'Global'
|
AND country_name = 'Global'
|
||||||
@@ -189,13 +189,13 @@ async def get_production_yoy_by_country(
|
|||||||
WHERE commodity_code = ? AND country_code IS NOT NULL
|
WHERE commodity_code = ? AND country_code IS NOT NULL
|
||||||
)
|
)
|
||||||
SELECT country_name, country_code, market_year,
|
SELECT country_name, country_code, market_year,
|
||||||
Production, Production_YoY_pct
|
production, production_yoy_pct
|
||||||
FROM serving.commodity_metrics, latest
|
FROM serving.commodity_metrics, latest
|
||||||
WHERE commodity_code = ?
|
WHERE commodity_code = ?
|
||||||
AND country_code IS NOT NULL
|
AND country_code IS NOT NULL
|
||||||
AND market_year = latest.max_year
|
AND market_year = latest.max_year
|
||||||
AND Production > 0
|
AND production > 0
|
||||||
ORDER BY ABS(Production_YoY_pct) DESC
|
ORDER BY ABS(production_yoy_pct) DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""",
|
""",
|
||||||
[commodity_code, commodity_code, limit],
|
[commodity_code, commodity_code, limit],
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ async def index():
|
|||||||
time_series, top_producers, stu_trend, balance, yoy = await asyncio.gather(
|
time_series, top_producers, stu_trend, balance, yoy = await asyncio.gather(
|
||||||
analytics.get_global_time_series(
|
analytics.get_global_time_series(
|
||||||
analytics.COFFEE_COMMODITY_CODE,
|
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_stock_to_use_trend(analytics.COFFEE_COMMODITY_CODE),
|
||||||
analytics.get_supply_demand_balance(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),
|
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")
|
plan = (g.get("subscription") or {}).get("plan", "free")
|
||||||
|
|
||||||
# Get available countries for coffee
|
# 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
|
# Parse query params
|
||||||
selected_codes = request.args.getlist("country")
|
selected_codes = request.args.getlist("country")
|
||||||
metric = request.args.get("metric", "Production")
|
metric = request.args.get("metric", "production")
|
||||||
|
|
||||||
comparison_data = []
|
comparison_data = []
|
||||||
if selected_codes:
|
if selected_codes:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label for="metric" class="form-label">Metric</label>
|
<label for="metric" class="form-label">Metric</label>
|
||||||
<select name="metric" id="metric" class="form-input" onchange="this.form.submit()">
|
<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>
|
<option value="{{ m }}" {{ "selected" if metric == m }}>{{ m | replace("_", " ") }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<div class="grid-4 mb-8">
|
<div class="grid-4 mb-8">
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<div class="metric-label">Global Production (latest year)</div>
|
<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 class="metric-sub">1,000 60-kg bags</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="metric-label">Stock-to-Use Ratio</div>
|
<div class="metric-label">Stock-to-Use Ratio</div>
|
||||||
<div class="metric-value">
|
<div class="metric-value">
|
||||||
{% if stu_trend %}
|
{% 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 %}
|
{% else %}
|
||||||
--
|
--
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<div class="metric-label">Trade Balance</div>
|
<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 class="metric-sub">Exports minus imports</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,10 +90,10 @@
|
|||||||
{% for row in yoy %}
|
{% for row in yoy %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ row.country_name }}</td>
|
<td>{{ row.country_name }}</td>
|
||||||
<td class="text-right">{{ "{:,.0f}".format(row.Production) }}</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 %}">
|
<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 %}
|
{% if row.production_yoy_pct is not none %}
|
||||||
{{ "{:+.1f}%".format(row.Production_YoY_pct) }}
|
{{ "{:+.1f}%".format(row.production_yoy_pct) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
--
|
--
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -163,11 +163,11 @@ if (tsData.length > 0) {
|
|||||||
data: {
|
data: {
|
||||||
labels: tsData.map(r => r.market_year),
|
labels: tsData.map(r => r.market_year),
|
||||||
datasets: [
|
datasets: [
|
||||||
{label: 'Production', data: tsData.map(r => r.Production), borderColor: CHART_PALETTE[0], 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: '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: '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: '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: 'Total Distribution', data: tsData.map(r => r.total_distribution), borderColor: CHART_PALETTE[4], tension: 0.3},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@@ -187,7 +187,7 @@ if (stuData.length > 0) {
|
|||||||
labels: stuData.map(r => r.market_year),
|
labels: stuData.map(r => r.market_year),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Stock-to-Use Ratio (%)',
|
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,
|
borderColor: CHART_COLORS.copper,
|
||||||
backgroundColor: 'rgba(180, 83, 9, 0.08)',
|
backgroundColor: 'rgba(180, 83, 9, 0.08)',
|
||||||
fill: true,
|
fill: true,
|
||||||
@@ -211,7 +211,7 @@ if (topData.length > 0) {
|
|||||||
labels: topData.map(r => r.country_name),
|
labels: topData.map(r => r.country_name),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Production',
|
label: 'Production',
|
||||||
data: topData.map(r => r.Production),
|
data: topData.map(r => r.production),
|
||||||
backgroundColor: CHART_COLORS.copper
|
backgroundColor: CHART_COLORS.copper
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user