generate_iceberg_demo.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. #!/usr/bin/env python3
  2. from __future__ import annotations
  3. import csv
  4. import json
  5. import random
  6. from dataclasses import dataclass
  7. from datetime import date, datetime, timedelta
  8. from pathlib import Path
  9. from typing import Iterable
  10. ROOT = Path(__file__).resolve().parents[1]
  11. OUT_DIR = ROOT / "backend" / "src" / "main" / "resources" / "db" / "iceberg_demo"
  12. RANDOM_SEED = 20260327
  13. @dataclass(frozen=True)
  14. class ProductRow:
  15. product_id: int
  16. sku: str
  17. product_name: str
  18. category: str
  19. price: float
  20. stock_qty: int
  21. launch_date: str
  22. active_flag: str
  23. def ensure_output_dir() -> None:
  24. OUT_DIR.mkdir(parents=True, exist_ok=True)
  25. def date_range(start: date, end: date) -> Iterable[date]:
  26. current = start
  27. while current <= end:
  28. yield current
  29. current += timedelta(days=1)
  30. def pick_region(rng: random.Random) -> str:
  31. return rng.choice(["North", "South", "East", "West"])
  32. def noisy_text(rng: random.Random, value: str, probability: float = 0.35) -> str:
  33. if rng.random() > probability:
  34. return value
  35. choice = rng.randint(0, 4)
  36. if choice == 0:
  37. return f" {value} "
  38. if choice == 1:
  39. return value.upper()
  40. if choice == 2:
  41. return value.lower()
  42. if choice == 3:
  43. return value.title()
  44. return f" {value} "
  45. def build_customers(rng: random.Random) -> list[dict[str, object]]:
  46. first_names = [
  47. "Ava", "Ben", "Cody", "Dora", "Evan", "Fiona", "Gina", "Hank",
  48. "Iris", "Jack", "Kira", "Liam", "Mia", "Nora", "Owen", "Pia",
  49. "Quinn", "Rita", "Sean", "Tina", "Uma", "Vera", "Wade", "Xena",
  50. "Yara", "Zane",
  51. ]
  52. last_names = [
  53. "Adams", "Brown", "Carter", "Diaz", "Evans", "Foster", "Grant", "Hayes",
  54. "Irwin", "Jones", "King", "Lopez", "Moore", "Nguyen", "Owens", "Price",
  55. "Quinn", "Reed", "Stone", "Turner", "Usher", "Vance", "Walker", "Xu",
  56. "Young", "Zimmer",
  57. ]
  58. cities = [
  59. "Shanghai", "Beijing", "Shenzhen", "Hangzhou", "Nanjing", "Chengdu",
  60. "Wuhan", "Suzhou", "Guangzhou", "Xian", "Tianjin", "Chongqing",
  61. ]
  62. tiers = ["bronze", "silver", "gold", "platinum"]
  63. rows = []
  64. base_date = date(2024, 1, 1)
  65. for customer_id in range(1, 1001):
  66. first = first_names[(customer_id - 1) % len(first_names)]
  67. last = last_names[((customer_id - 1) * 3) % len(last_names)]
  68. full_name = noisy_text(rng, f"{first} {last}", probability=0.45)
  69. email = f"{first}.{last}{customer_id}@example.com".lower()
  70. if customer_id % 37 == 0:
  71. email = f" {email.upper()} "
  72. elif customer_id % 41 == 0:
  73. email = f"{first.lower()}{customer_id}"
  74. city = noisy_text(rng, cities[(customer_id - 1) % len(cities)], probability=0.25)
  75. region = noisy_text(rng, pick_region(rng), probability=0.25)
  76. signup_date = base_date + timedelta(days=(customer_id * 17) % 780)
  77. loyalty_tier = noisy_text(rng, tiers[(customer_id - 1) % len(tiers)], probability=0.40)
  78. status = "ACTIVE" if customer_id % 9 else "inactive"
  79. if customer_id % 53 == 0:
  80. status = " suspended "
  81. age = 18 + (customer_id * 7) % 45
  82. rows.append({
  83. "customer_id": customer_id,
  84. "full_name": full_name,
  85. "email": email,
  86. "city": city,
  87. "region": region,
  88. "signup_date": signup_date.isoformat(),
  89. "loyalty_tier": loyalty_tier,
  90. "status": status,
  91. "age": age,
  92. })
  93. return rows
  94. def build_products(rng: random.Random) -> tuple[list[dict[str, object]], dict[int, ProductRow]]:
  95. categories = [
  96. "electronics", "home", "fashion", "sports", "beauty", "books",
  97. ]
  98. rows = []
  99. valid_lookup: dict[int, ProductRow] = {}
  100. base_date = date(2023, 6, 1)
  101. for product_id in range(1, 121):
  102. category = categories[(product_id - 1) % len(categories)]
  103. category_value = noisy_text(rng, category, probability=0.55)
  104. price = round(8.0 + (product_id * 3.37) % 180 + rng.uniform(0.25, 19.75), 2)
  105. stock_qty = 20 + (product_id * 29) % 900
  106. if product_id % 29 == 0:
  107. price = -abs(price)
  108. elif product_id % 37 == 0:
  109. stock_qty = -stock_qty
  110. active_flag = "Y" if product_id % 5 else "n"
  111. if product_id % 11 == 0:
  112. active_flag = " true "
  113. sku = noisy_text(rng, f"SKU-{product_id:04d}", probability=0.15)
  114. product_name = f"{category.title()} Item {product_id:03d}"
  115. launch_date = base_date + timedelta(days=(product_id * 13) % 560)
  116. row = {
  117. "product_id": product_id,
  118. "sku": sku,
  119. "product_name": product_name,
  120. "category": category_value,
  121. "price": price,
  122. "stock_qty": stock_qty,
  123. "launch_date": launch_date.isoformat(),
  124. "active_flag": active_flag,
  125. }
  126. rows.append(row)
  127. if price > 0 and stock_qty >= 0:
  128. valid_lookup[product_id] = ProductRow(
  129. product_id=product_id,
  130. sku=f"SKU-{product_id:04d}",
  131. product_name=product_name,
  132. category=category.title(),
  133. price=price,
  134. stock_qty=stock_qty,
  135. launch_date=launch_date.isoformat(),
  136. active_flag=active_flag.strip().upper(),
  137. )
  138. return rows, valid_lookup
  139. def build_orders(
  140. rng: random.Random,
  141. customers: list[dict[str, object]],
  142. products: dict[int, ProductRow],
  143. ) -> list[dict[str, object]]:
  144. valid_customer_ids = [row["customer_id"] for row in customers if row["customer_id"] % 41 != 0]
  145. valid_product_ids = list(products.keys())
  146. statuses = ["PAID", " shipped ", "returned", "CANCELLED", "pending"]
  147. channels = ["web", "app", "store", "partner"]
  148. notes = ["", "priority", "gift", "repeat_customer", "bulk_order"]
  149. start = date(2025, 1, 1)
  150. end = date(2026, 3, 20)
  151. total_days = (end - start).days
  152. rows = []
  153. for offset in range(1, 10001):
  154. customer_id = rng.choice(valid_customer_ids)
  155. product_id = rng.choice(valid_product_ids)
  156. product = products[product_id]
  157. order_date = start + timedelta(days=(offset * 19 + product_id) % total_days)
  158. quantity = 1 + (offset * 7) % 8
  159. if offset % 97 == 0:
  160. quantity = 0
  161. unit_price = round(product.price * (0.92 + ((offset % 13) * 0.01)), 2)
  162. if offset % 151 == 0:
  163. unit_price = -unit_price
  164. discount_rate = round(((offset % 9) * 0.04), 2)
  165. if offset % 53 == 0:
  166. discount_rate = ""
  167. status = statuses[offset % len(statuses)]
  168. channel = channels[(offset + product_id) % len(channels)]
  169. region = noisy_text(rng, pick_region(rng), probability=0.22)
  170. note = notes[offset % len(notes)]
  171. rows.append({
  172. "order_id": 7000000 + offset,
  173. "customer_id": customer_id,
  174. "product_id": product_id,
  175. "order_date": order_date.isoformat(),
  176. "quantity": quantity,
  177. "unit_price": unit_price,
  178. "discount_rate": discount_rate,
  179. "status": status,
  180. "channel": channel,
  181. "region": region,
  182. "note": note,
  183. })
  184. return rows
  185. def write_csv(path: Path, rows: list[dict[str, object]], header: list[str]) -> None:
  186. with path.open("w", newline="", encoding="utf-8") as fp:
  187. writer = csv.DictWriter(fp, fieldnames=header)
  188. writer.writeheader()
  189. writer.writerows(rows)
  190. def sql_escape(value: str) -> str:
  191. return value.replace("'", "''")
  192. def build_workflow_sql() -> str:
  193. dag_json = {
  194. "nodes": [
  195. {"taskId": 964011},
  196. {"taskId": 964012},
  197. {"taskId": 964013},
  198. {"taskId": 964014},
  199. {"taskId": 964015},
  200. ],
  201. "edges": [
  202. {"fromTaskId": 964011, "toTaskId": 964014},
  203. {"fromTaskId": 964012, "toTaskId": 964014},
  204. {"fromTaskId": 964013, "toTaskId": 964014},
  205. {"fromTaskId": 964014, "toTaskId": 964015},
  206. ],
  207. }
  208. dag_json_text = json.dumps(dag_json, ensure_ascii=False, separators=(",", ":"))
  209. customer_clean_sql = (
  210. "CREATE OR REPLACE TABLE polaris.demo_iceberg.customer_clean USING iceberg AS "
  211. "SELECT customer_id, "
  212. "trim(regexp_replace(full_name, '\\\\s+', ' ')) AS full_name, "
  213. "lower(trim(email)) AS email, "
  214. "initcap(trim(city)) AS city, "
  215. "upper(trim(region)) AS region, "
  216. "to_date(signup_date) AS signup_date, "
  217. "upper(trim(loyalty_tier)) AS loyalty_tier, "
  218. "CASE WHEN lower(trim(status)) IN ('active','1','y') THEN 'ACTIVE' ELSE 'INACTIVE' END AS status, "
  219. "CAST(age AS INT) AS age "
  220. "FROM polaris.demo_iceberg.demo_customers "
  221. "WHERE customer_id IS NOT NULL AND trim(email) <> '' AND email LIKE '%@%';"
  222. )
  223. product_clean_sql = (
  224. "CREATE OR REPLACE TABLE polaris.demo_iceberg.product_clean USING iceberg AS "
  225. "SELECT product_id, "
  226. "upper(trim(sku)) AS sku, "
  227. "trim(product_name) AS product_name, "
  228. "initcap(lower(trim(category))) AS category, "
  229. "CAST(price AS DECIMAL(12,2)) AS price, "
  230. "CAST(stock_qty AS INT) AS stock_qty, "
  231. "to_date(launch_date) AS launch_date, "
  232. "CASE WHEN lower(trim(active_flag)) IN ('y','1','true') THEN true ELSE false END AS is_active "
  233. "FROM polaris.demo_iceberg.demo_products "
  234. "WHERE product_id IS NOT NULL AND price > 0 AND stock_qty >= 0;"
  235. )
  236. order_clean_sql = (
  237. "CREATE OR REPLACE TABLE polaris.demo_iceberg.order_clean USING iceberg AS "
  238. "SELECT order_id, "
  239. "customer_id, "
  240. "product_id, "
  241. "to_date(order_date) AS order_date, "
  242. "CAST(quantity AS INT) AS quantity, "
  243. "CAST(unit_price AS DECIMAL(12,2)) AS unit_price, "
  244. "CAST(COALESCE(discount_rate, 0) AS DECIMAL(5,4)) AS discount_rate, "
  245. "upper(trim(status)) AS status, "
  246. "upper(trim(channel)) AS channel, "
  247. "upper(trim(region)) AS region, "
  248. "NULLIF(trim(note), '') AS note "
  249. "FROM polaris.demo_iceberg.demo_orders "
  250. "WHERE order_id IS NOT NULL AND customer_id IS NOT NULL AND product_id IS NOT NULL "
  251. "AND quantity > 0 AND unit_price > 0;"
  252. )
  253. enriched_sql = (
  254. "CREATE OR REPLACE TABLE polaris.demo_iceberg.order_enriched USING iceberg AS "
  255. "SELECT o.order_id, "
  256. "o.order_date, "
  257. "o.customer_id, "
  258. "c.full_name, "
  259. "c.city, "
  260. "c.region AS customer_region, "
  261. "p.category, "
  262. "p.sku, "
  263. "o.channel, "
  264. "o.status, "
  265. "o.quantity, "
  266. "o.unit_price, "
  267. "o.discount_rate, "
  268. "CAST(o.quantity * o.unit_price AS DECIMAL(14,2)) AS gross_amount, "
  269. "CAST(o.quantity * o.unit_price * (1 - COALESCE(o.discount_rate, 0)) AS DECIMAL(14,2)) AS net_amount "
  270. "FROM polaris.demo_iceberg.order_clean o "
  271. "JOIN polaris.demo_iceberg.customer_clean c ON o.customer_id = c.customer_id "
  272. "JOIN polaris.demo_iceberg.product_clean p ON o.product_id = p.product_id;"
  273. )
  274. summary_sql = (
  275. "CREATE OR REPLACE TABLE polaris.demo_iceberg.sales_kpi_daily USING iceberg AS "
  276. "SELECT order_date, "
  277. "customer_region, "
  278. "category, "
  279. "COUNT(*) AS order_count, "
  280. "COUNT(DISTINCT customer_id) AS customer_count, "
  281. "SUM(quantity) AS total_quantity, "
  282. "ROUND(SUM(net_amount), 2) AS net_revenue, "
  283. "ROUND(AVG(net_amount), 2) AS avg_net_order_value, "
  284. "ROUND(MAX(net_amount), 2) AS max_net_order_value "
  285. "FROM polaris.demo_iceberg.order_enriched "
  286. "GROUP BY order_date, customer_region, category;"
  287. )
  288. workflow_name = "Iceberg_Sales_Clean_Analysis"
  289. description = "Import three related Iceberg tables, clean dirty records, and generate sales KPIs"
  290. now = "2026-03-27 10:00:00"
  291. dag_json_sql = sql_escape(dag_json_text)
  292. return f"""USE wenshu_platform;
  293. INSERT INTO workflow_definition (
  294. workflow_id,
  295. workflow_name,
  296. description,
  297. dag_json,
  298. timeout_seconds,
  299. create_time,
  300. failure_strategy
  301. ) VALUES
  302. (
  303. 964001,
  304. '{sql_escape(workflow_name)}',
  305. '{sql_escape(description)}',
  306. '{dag_json_sql}',
  307. 9000,
  308. '{now}',
  309. 'STOP'
  310. )
  311. ON DUPLICATE KEY UPDATE
  312. workflow_name = VALUES(workflow_name),
  313. description = VALUES(description),
  314. dag_json = VALUES(dag_json),
  315. timeout_seconds = VALUES(timeout_seconds),
  316. create_time = VALUES(create_time),
  317. failure_strategy = VALUES(failure_strategy);
  318. INSERT INTO task_definition (
  319. task_id,
  320. task_name,
  321. workflow_id,
  322. task_type,
  323. task_content,
  324. exector_id,
  325. timeout_seconds,
  326. retry_times
  327. ) VALUES
  328. (964011, '清洗客户表', 964001, 'SPARK_SQL', '{sql_escape(customer_clean_sql)}', 3001, 1200, 1),
  329. (964012, '清洗产品表', 964001, 'SPARK_SQL', '{sql_escape(product_clean_sql)}', 3001, 1200, 1),
  330. (964013, '清洗订单表', 964001, 'SPARK_SQL', '{sql_escape(order_clean_sql)}', 3001, 1500, 1),
  331. (964014, '构建明细宽表', 964001, 'SPARK_SQL', '{sql_escape(enriched_sql)}', 3001, 1800, 1),
  332. (964015, '生成日粒度指标', 964001, 'SPARK_SQL', '{sql_escape(summary_sql)}', 3001, 1200, 1)
  333. ON DUPLICATE KEY UPDATE
  334. task_name = VALUES(task_name),
  335. workflow_id = VALUES(workflow_id),
  336. task_type = VALUES(task_type),
  337. task_content = VALUES(task_content),
  338. exector_id = VALUES(exector_id),
  339. timeout_seconds = VALUES(timeout_seconds),
  340. retry_times = VALUES(retry_times);
  341. INSERT INTO workflow_instance (
  342. workflow_instance_id,
  343. workflow_id,
  344. workflow_name,
  345. start_time,
  346. end_time,
  347. state,
  348. error_message
  349. ) VALUES
  350. (965001, 964001, 'Iceberg_Sales_Clean_Analysis', '2026-03-27 10:05:00', '2026-03-27 10:12:48', 'SUCCESS', NULL)
  351. ON DUPLICATE KEY UPDATE
  352. workflow_id = VALUES(workflow_id),
  353. workflow_name = VALUES(workflow_name),
  354. start_time = VALUES(start_time),
  355. end_time = VALUES(end_time),
  356. state = VALUES(state),
  357. error_message = VALUES(error_message);
  358. INSERT INTO task_instance (
  359. task_instance_id,
  360. workflow_instance_id,
  361. task_id,
  362. task_name,
  363. execution_script,
  364. state,
  365. start_time,
  366. end_time,
  367. executor_id,
  368. retry_count,
  369. engine_task_id
  370. ) VALUES
  371. (965011, 965001, 964011, '清洗客户表', '{sql_escape(customer_clean_sql)}', 'SUCCESS', '2026-03-27 10:05:00', '2026-03-27 10:06:05', 3001, 0, 'spark-demo-clean-customers'),
  372. (965012, 965001, 964012, '清洗产品表', '{sql_escape(product_clean_sql)}', 'SUCCESS', '2026-03-27 10:05:00', '2026-03-27 10:06:10', 3001, 0, 'spark-demo-clean-products'),
  373. (965013, 965001, 964013, '清洗订单表', '{sql_escape(order_clean_sql)}', 'SUCCESS', '2026-03-27 10:05:00', '2026-03-27 10:07:35', 3001, 0, 'spark-demo-clean-orders'),
  374. (965014, 965001, 964014, '构建明细宽表', '{sql_escape(enriched_sql)}', 'SUCCESS', '2026-03-27 10:07:35', '2026-03-27 10:09:40', 3001, 0, 'spark-demo-build-fact'),
  375. (965015, 965001, 964015, '生成日粒度指标', '{sql_escape(summary_sql)}', 'SUCCESS', '2026-03-27 10:09:40', '2026-03-27 10:12:48', 3001, 0, 'spark-demo-build-kpi')
  376. ON DUPLICATE KEY UPDATE
  377. workflow_instance_id = VALUES(workflow_instance_id),
  378. task_id = VALUES(task_id),
  379. task_name = VALUES(task_name),
  380. execution_script = VALUES(execution_script),
  381. state = VALUES(state),
  382. start_time = VALUES(start_time),
  383. end_time = VALUES(end_time),
  384. executor_id = VALUES(executor_id),
  385. retry_count = VALUES(retry_count),
  386. engine_task_id = VALUES(engine_task_id);
  387. """
  388. def main() -> None:
  389. rng = random.Random(RANDOM_SEED)
  390. ensure_output_dir()
  391. customers = build_customers(rng)
  392. products, valid_products = build_products(rng)
  393. orders = build_orders(rng, customers, valid_products)
  394. write_csv(
  395. OUT_DIR / "demo_customers.csv",
  396. customers,
  397. [
  398. "customer_id",
  399. "full_name",
  400. "email",
  401. "city",
  402. "region",
  403. "signup_date",
  404. "loyalty_tier",
  405. "status",
  406. "age",
  407. ],
  408. )
  409. write_csv(
  410. OUT_DIR / "demo_products.csv",
  411. products,
  412. [
  413. "product_id",
  414. "sku",
  415. "product_name",
  416. "category",
  417. "price",
  418. "stock_qty",
  419. "launch_date",
  420. "active_flag",
  421. ],
  422. )
  423. write_csv(
  424. OUT_DIR / "demo_orders.csv",
  425. orders,
  426. [
  427. "order_id",
  428. "customer_id",
  429. "product_id",
  430. "order_date",
  431. "quantity",
  432. "unit_price",
  433. "discount_rate",
  434. "status",
  435. "channel",
  436. "region",
  437. "note",
  438. ],
  439. )
  440. (OUT_DIR / "test-iceberg-demo-data.sql").write_text(build_workflow_sql(), encoding="utf-8")
  441. print("Generated demo Iceberg data:")
  442. print(f" {OUT_DIR / 'demo_customers.csv'}")
  443. print(f" {OUT_DIR / 'demo_products.csv'}")
  444. print(f" {OUT_DIR / 'demo_orders.csv'}")
  445. print(f" {OUT_DIR / 'test-iceberg-demo-data.sql'}")
  446. print(f"Rows: customers={len(customers)}, products={len(products)}, orders={len(orders)}")
  447. if __name__ == "__main__":
  448. main()