Skip to content

Commit b787662

Browse files
andre-salvaticlaude
andcommitted
feat: standardize silver/gold field names, fix dashboard KPIs, add total_orders
Drop ds_kpi dataset from the dashboard — KPI counters (Total Value, Total Orders, Number of Customers) now bind to ds_orders so all five filters update them. Number of Orders removed (no order_id in gold layer). Standardize field names across silver (curated.order_enriched) and gold (report.order_agg) per three rules: {entity}_id suffix, entity-qualified names, item_* prefix for item-level fields, no abbreviations. date cast to DateType in silver so the type coercion lives in the right layer. Add total_orders (COUNT DISTINCT order_id) to report.order_agg and a third KPI tile on the dashboard. commonSchemas.py now defines order_enriched_schema and order_agg_schema as the canonical schemas for silver and gold; tests import from there instead of inlining StructType definitions. Also fix pre-existing unit_test_sdp.py bug where df_order tuples were missing product_id and prod_category_id (4 values vs 6-field schema). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bca56c7 commit b787662

10 files changed

Lines changed: 144 additions & 165 deletions

File tree

.github/workflows/onpush.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
paths-ignore:
1010
- 'README.md'
1111
- 'CLAUDE.md'
12+
- 'CHANGELOG.md'
1213
- 'docs/**'
1314
# Manual trigger for re-running CI without a new commit (e.g. after a transient
1415
# GitHub Actions hiccup that silently drops a push event):

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ Medallion schemas (`MEDALLION_SCHEMAS` in `config.py`):
9595

9696
Each task's input/output tables are **hardcoded** in the task module (e.g. `raw.customer``curated.order_enriched`). The medallion layer is a semantic contract, not a runtime parameter — this is the dbt `ref()` pattern. Don't parameterize the layer; if a task genuinely needs a configurable target, that's a different task.
9797

98-
`curated.order_enriched` columns: `name, country, id_customer, id_order, total, date, product_id, prod_category_id, seq, desc_item, qty, total_item`
99-
`report.order_agg` columns: `name, country, date, product_id, prod_category_id, total_qty, total_value`
98+
`curated.order_enriched` columns: `customer_name, country, customer_id, order_id, order_total, order_date (DateType), product_id, product_category_id, item_seq, item_description, item_quantity, item_total`
99+
`report.order_agg` columns: `customer_name, country, order_date (DateType), product_id, product_category_id, total_quantity, total_value, total_orders`
100100

101101
### Job-level parameters (runtime, overridable per-run)
102102

scripts/sdk_generate_template_job.py

Lines changed: 18 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -480,41 +480,15 @@ def _build_dashboard_json(catalog: str) -> dict:
480480
"""
481481
return {
482482
"datasets": [
483-
{
484-
"name": "ds_kpi",
485-
"displayName": "KPIs",
486-
"queryLines": [
487-
f"SELECT ROUND(SUM(total_item), 2) AS total_value, "
488-
f"COUNT(DISTINCT id_order) AS num_orders, "
489-
f"COUNT(DISTINCT id_customer) AS num_customers "
490-
f"FROM {catalog}.curated.order_enriched "
491-
f"WHERE date BETWEEN :date_range.min AND :date_range.max"
492-
],
493-
"parameters": [
494-
{
495-
"keyword": "date_range",
496-
"displayName": "Date Range",
497-
"dataType": "DATE",
498-
"complexType": "RANGE",
499-
"defaultSelection": {
500-
"range": {
501-
"dataType": "DATE",
502-
"min": {"value": "now-1y"},
503-
"max": {"value": "now"},
504-
}
505-
},
506-
}
507-
],
508-
},
509483
{
510484
"name": "ds_orders",
511485
"displayName": "Orders",
512486
"queryLines": [
513-
f"SELECT CAST(date AS DATE) AS order_date, country, name AS customer, "
514-
f"CAST(product_id AS STRING) AS product_id, CAST(prod_category_id AS STRING) AS category_id, "
515-
f"SUM(total_value) AS total_value "
487+
f"SELECT order_date, country, customer_name AS customer, "
488+
f"CAST(product_id AS STRING) AS product_id, CAST(product_category_id AS STRING) AS category_id, "
489+
f"SUM(total_value) AS total_value, SUM(total_orders) AS total_orders "
516490
f"FROM {catalog}.report.order_agg "
517-
f"WHERE date BETWEEN :date_range.min AND :date_range.max "
491+
f"WHERE order_date BETWEEN :date_range.min AND :date_range.max "
518492
f"GROUP BY 1, 2, 3, 4, 5"
519493
],
520494
"parameters": [
@@ -566,9 +540,9 @@ def _build_dashboard_json(catalog: str) -> dict:
566540
{
567541
"name": "main_query",
568542
"query": {
569-
"datasetName": "ds_kpi",
570-
"fields": [{"name": "total_value", "expression": "`total_value`"}],
571-
"disaggregated": True,
543+
"datasetName": "ds_orders",
544+
"fields": [{"name": "total_value", "expression": "SUM(`total_value`)"}],
545+
"disaggregated": False,
572546
},
573547
}
574548
],
@@ -583,22 +557,22 @@ def _build_dashboard_json(catalog: str) -> dict:
583557
},
584558
{
585559
"widget": {
586-
"name": "kpi-num-orders",
560+
"name": "kpi-total-orders",
587561
"queries": [
588562
{
589563
"name": "main_query",
590564
"query": {
591-
"datasetName": "ds_kpi",
592-
"fields": [{"name": "num_orders", "expression": "`num_orders`"}],
593-
"disaggregated": True,
565+
"datasetName": "ds_orders",
566+
"fields": [{"name": "total_orders", "expression": "SUM(`total_orders`)"}],
567+
"disaggregated": False,
594568
},
595569
}
596570
],
597571
"spec": {
598572
"version": 2,
599573
"widgetType": "counter",
600-
"encodings": {"value": {"fieldName": "num_orders", "displayName": "Number of Orders"}},
601-
"frame": {"title": "Number of Orders", "showTitle": True},
574+
"encodings": {"value": {"fieldName": "total_orders", "displayName": "Total Orders"}},
575+
"frame": {"title": "Total Orders", "showTitle": True},
602576
},
603577
},
604578
"position": {"x": 2, "y": 2, "width": 2, "height": 3},
@@ -610,9 +584,11 @@ def _build_dashboard_json(catalog: str) -> dict:
610584
{
611585
"name": "main_query",
612586
"query": {
613-
"datasetName": "ds_kpi",
614-
"fields": [{"name": "num_customers", "expression": "`num_customers`"}],
615-
"disaggregated": True,
587+
"datasetName": "ds_orders",
588+
"fields": [
589+
{"name": "num_customers", "expression": "COUNT(DISTINCT `customer`)"}
590+
],
591+
"disaggregated": False,
616592
},
617593
}
618594
],
@@ -784,22 +760,13 @@ def _build_dashboard_json(catalog: str) -> dict:
784760
"disaggregated": False,
785761
},
786762
},
787-
{
788-
"name": "q_date_kpi",
789-
"query": {
790-
"datasetName": "ds_kpi",
791-
"parameters": [{"name": "date_range", "keyword": "date_range"}],
792-
"disaggregated": False,
793-
},
794-
},
795763
],
796764
"spec": {
797765
"version": 2,
798766
"widgetType": "filter-date-range-picker",
799767
"encodings": {
800768
"fields": [
801769
{"parameterName": "date_range", "queryName": "q_date"},
802-
{"parameterName": "date_range", "queryName": "q_date_kpi"},
803770
]
804771
},
805772
"frame": {"showTitle": True, "title": "Date Range"},

src/template/commonSchemas.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from pyspark.sql.types import (
2+
DateType,
3+
DoubleType,
24
FloatType,
35
IntegerType,
6+
LongType,
47
StringType,
58
StructField,
69
StructType,
@@ -34,3 +37,33 @@
3437
StructField("total_item", FloatType(), True),
3538
]
3639
)
40+
41+
order_enriched_schema = StructType(
42+
[
43+
StructField("customer_name", StringType(), True),
44+
StructField("country", StringType(), True),
45+
StructField("customer_id", IntegerType(), True),
46+
StructField("order_id", IntegerType(), True),
47+
StructField("order_total", FloatType(), True),
48+
StructField("order_date", DateType(), True),
49+
StructField("product_id", IntegerType(), True),
50+
StructField("product_category_id", IntegerType(), True),
51+
StructField("item_seq", IntegerType(), True),
52+
StructField("item_description", StringType(), True),
53+
StructField("item_quantity", IntegerType(), True),
54+
StructField("item_total", FloatType(), True),
55+
]
56+
)
57+
58+
order_agg_schema = StructType(
59+
[
60+
StructField("customer_name", StringType(), True),
61+
StructField("country", StringType(), True),
62+
StructField("order_date", DateType(), True),
63+
StructField("product_id", IntegerType(), True),
64+
StructField("product_category_id", IntegerType(), True),
65+
StructField("total_quantity", LongType(), True),
66+
StructField("total_value", DoubleType(), True),
67+
StructField("total_orders", LongType(), True),
68+
]
69+
)

src/template/job1/generate_orders.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from pyspark.sql import functions as F
2+
13
from ..baseTask import BaseTask
24

35

@@ -12,18 +14,18 @@ def enrich_order(self, df_customer, df_order, df_order_item):
1214
df_order_item.join(df_order, df_order_item["id_order"] == df_order["id"])
1315
.join(df_customer, df_order["id_customer"] == df_customer["id"])
1416
.select(
15-
"name",
17+
df_customer["name"].alias("customer_name"),
1618
"country",
17-
"id_customer",
18-
"id_order",
19-
"total",
20-
"date",
19+
df_order["id_customer"].alias("customer_id"),
20+
df_order_item["id_order"].alias("order_id"),
21+
df_order["total"].alias("order_total"),
22+
F.col("date").cast("date").alias("order_date"),
2123
"product_id",
22-
"prod_category_id",
23-
"seq",
24-
"desc_item",
25-
"qty",
26-
"total_item",
24+
df_order["prod_category_id"].alias("product_category_id"),
25+
df_order_item["seq"].alias("item_seq"),
26+
df_order_item["desc_item"].alias("item_description"),
27+
df_order_item["qty"].alias("item_quantity"),
28+
df_order_item["total_item"].alias("item_total"),
2729
)
2830
)
2931

src/template/job1/generate_orders_agg.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pyspark.sql.functions import sum
1+
from pyspark.sql.functions import countDistinct, sum
22

33
from ..baseTask import BaseTask
44

@@ -10,9 +10,10 @@ def __init__(self, config):
1010
def aggregate_orders(self, df_order):
1111
# TODO code your transformations here...
1212

13-
return df_order.groupBy("name", "country", "date", "product_id", "prod_category_id").agg(
14-
sum("qty").alias("total_qty"),
15-
sum("total_item").alias("total_value"),
13+
return df_order.groupBy("customer_name", "country", "order_date", "product_id", "product_category_id").agg(
14+
sum("item_quantity").alias("total_quantity"),
15+
sum("item_total").alias("total_value"),
16+
countDistinct("order_id").alias("total_orders"),
1617
)
1718

1819
def run(self):

src/template/job1_sdp/transforms.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,25 @@ def enrich_order(df_customer: DataFrame, df_order: DataFrame, df_order_item: Dat
2626
2727
Returns:
2828
Enriched DataFrame with columns:
29-
name, country, id_customer, id_order, total, date, product_id, prod_category_id, seq, desc_item, qty, total_item
29+
customer_name, country, customer_id, order_id, order_total, order_date, product_id,
30+
product_category_id, item_seq, item_description, item_quantity, item_total
3031
"""
3132
return (
3233
df_order_item.join(df_order, df_order_item["id_order"] == df_order["id"])
3334
.join(df_customer, df_order["id_customer"] == df_customer["id"])
3435
.select(
35-
"name",
36+
df_customer["name"].alias("customer_name"),
3637
"country",
37-
"id_customer",
38-
"id_order",
39-
"total",
40-
"date",
38+
df_order["id_customer"].alias("customer_id"),
39+
df_order_item["id_order"].alias("order_id"),
40+
df_order["total"].alias("order_total"),
41+
F.col("date").cast("date").alias("order_date"),
4142
"product_id",
42-
"prod_category_id",
43-
"seq",
44-
"desc_item",
45-
"qty",
46-
"total_item",
43+
df_order["prod_category_id"].alias("product_category_id"),
44+
df_order_item["seq"].alias("item_seq"),
45+
df_order_item["desc_item"].alias("item_description"),
46+
df_order_item["qty"].alias("item_quantity"),
47+
df_order_item["total_item"].alias("item_total"),
4748
)
4849
)
4950

@@ -58,10 +59,11 @@ def aggregate_orders(df_order_enriched: DataFrame) -> DataFrame:
5859
df_order_enriched: curated.order_enriched
5960
6061
Returns:
61-
DataFrame with columns: name, country, date, product_id, prod_category_id,
62-
total_qty (LongType), total_value (DoubleType)
62+
DataFrame with columns: customer_name, country, order_date, product_id,
63+
product_category_id, total_quantity (LongType), total_value (DoubleType), total_orders (LongType)
6364
"""
64-
return df_order_enriched.groupBy("name", "country", "date", "product_id", "prod_category_id").agg(
65-
F.sum("qty").alias("total_qty"),
66-
F.sum("total_item").alias("total_value"),
65+
return df_order_enriched.groupBy("customer_name", "country", "order_date", "product_id", "product_category_id").agg(
66+
F.sum("item_quantity").alias("total_quantity"),
67+
F.sum("item_total").alias("total_value"),
68+
F.countDistinct("order_id").alias("total_orders"),
6769
)

tests/job1/integration_validate.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
1+
from datetime import date
2+
13
from pyspark.sql import functions as F
2-
from pyspark.sql.types import DoubleType, IntegerType, LongType, StringType, StructField, StructType
34
from pyspark.testing import assertDataFrameEqual
45

56
from template.baseTask import BaseTask
7+
from template.commonSchemas import order_agg_schema
68

79

810
class Validate(BaseTask):
911
def __init__(self, config):
1012
super().__init__(config)
1113

1214
def _validate_standard(self, catalog):
13-
# groupBy(name, country, date, product_id, prod_category_id) → still 2 rows (one per order)
15+
# groupBy(customer_name, country, order_date, product_id, product_category_id) → still 2 rows (one per order)
1416
expected_data = [
15-
("John Doe", "USA", "2023-01-01", 1, 1, 3, 100.0),
16-
("Jane Smith", "UK", "2023-01-02", 2, 1, 3, 151.0),
17+
("John Doe", "USA", date(2023, 1, 1), 1, 1, 3, 100.0, 1),
18+
("Jane Smith", "UK", date(2023, 1, 2), 2, 1, 3, 151.0, 1),
1719
]
18-
expected_schema = StructType(
19-
[
20-
StructField("name", StringType(), True),
21-
StructField("country", StringType(), True),
22-
StructField("date", StringType(), True),
23-
StructField("product_id", IntegerType(), True),
24-
StructField("prod_category_id", IntegerType(), True),
25-
StructField("total_qty", LongType(), True),
26-
StructField("total_value", DoubleType(), True),
27-
]
28-
)
29-
df_expected = self.spark.createDataFrame(expected_data, schema=expected_schema)
20+
df_expected = self.spark.createDataFrame(expected_data, schema=order_agg_schema)
3021

3122
for table in (f"{catalog}.report.order_agg", f"{catalog}.report.order_agg_sdp"):
3223
df_out = self.spark.table(table)
@@ -44,7 +35,7 @@ def _validate_load_test(self, catalog):
4435
count = df_out.count()
4536
if count != 50_000:
4637
raise RuntimeError(f"Expected 50,000 rows in {table}, got {count}")
47-
wrong = df_out.filter((F.col("total_qty") != 240) | (F.col("total_value") != 6_000.0)).count()
38+
wrong = df_out.filter((F.col("total_quantity") != 240) | (F.col("total_value") != 6_000.0)).count()
4839
if wrong > 0:
4940
raise RuntimeError(f"{wrong} rows in {table} have unexpected total_qty/total_value")
5041

0 commit comments

Comments
 (0)