load_iceberg_demo.sh 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
  5. read_dotenv_value() {
  6. local key="$1"
  7. local dotenv_file="${ROOT_DIR}/.env"
  8. if [[ ! -f "$dotenv_file" ]]; then
  9. return 0
  10. fi
  11. local raw
  12. raw="$(grep -m1 -E "^${key}=" "$dotenv_file" || true)"
  13. if [[ -z "$raw" ]]; then
  14. return 0
  15. fi
  16. printf '%s' "${raw#*=}"
  17. }
  18. DOTENV_BACKEND_URL="$(read_dotenv_value BACKEND_URL)"
  19. BACKEND_URL="${BACKEND_URL:-${DOTENV_BACKEND_URL:-http://localhost:8080}}"
  20. ICEBERG_DB="${ICEBERG_DB:-demo_iceberg}"
  21. ICEBERG_USERNAME="${ICEBERG_USERNAME:-iceberg_demo}"
  22. ICEBERG_PASSWORD="${ICEBERG_PASSWORD:-Demo1234}"
  23. ICEBERG_EMAIL="${ICEBERG_EMAIL:-iceberg_demo@example.com}"
  24. ICEBERG_ROLE="${ICEBERG_ROLE:-ANALYST}"
  25. WORKFLOW_NAME="${WORKFLOW_NAME:-Iceberg_Sales_Clean_Analysis}"
  26. json_field() {
  27. python3 - "$1" "$2" <<'PY'
  28. import json
  29. import sys
  30. path = sys.argv[1]
  31. field = sys.argv[2]
  32. with open(path, "r", encoding="utf-8") as fh:
  33. data = json.load(fh)
  34. value = data.get(field, "")
  35. if value is None:
  36. value = ""
  37. print(value)
  38. PY
  39. }
  40. obtain_token() {
  41. local login_body login_code register_body register_code token
  42. login_body="$(mktemp)"
  43. login_code="$(curl -sS -o "$login_body" -w '%{http_code}' \
  44. -H 'Content-Type: application/json' \
  45. -d "{\"username\":\"${ICEBERG_USERNAME}\",\"password\":\"${ICEBERG_PASSWORD}\"}" \
  46. "${BACKEND_URL}/api/auth/login" || true)"
  47. if [[ "$login_code" != "200" ]]; then
  48. register_body="$(mktemp)"
  49. register_code="$(curl -sS -o "$register_body" -w '%{http_code}' \
  50. -H 'Content-Type: application/json' \
  51. -d "{\"username\":\"${ICEBERG_USERNAME}\",\"password\":\"${ICEBERG_PASSWORD}\",\"email\":\"${ICEBERG_EMAIL}\",\"role\":\"${ICEBERG_ROLE}\"}" \
  52. "${BACKEND_URL}/api/auth/register" || true)"
  53. if [[ "$register_code" != "200" ]]; then
  54. cat "$register_body" >&2
  55. rm -f "$login_body" "$register_body"
  56. echo "failed to register iceberg demo user" >&2
  57. exit 1
  58. fi
  59. rm -f "$register_body"
  60. login_body="$(mktemp)"
  61. login_code="$(curl -sS -o "$login_body" -w '%{http_code}' \
  62. -H 'Content-Type: application/json' \
  63. -d "{\"username\":\"${ICEBERG_USERNAME}\",\"password\":\"${ICEBERG_PASSWORD}\"}" \
  64. "${BACKEND_URL}/api/auth/login" || true)"
  65. fi
  66. if [[ "$login_code" != "200" ]]; then
  67. cat "$login_body" >&2
  68. rm -f "$login_body"
  69. echo "failed to login iceberg demo user" >&2
  70. exit 1
  71. fi
  72. token="$(json_field "$login_body" token)"
  73. rm -f "$login_body"
  74. if [[ -z "$token" ]]; then
  75. echo "login response did not contain a token" >&2
  76. exit 1
  77. fi
  78. printf '%s' "$token"
  79. }
  80. create_db() {
  81. local token="$1"
  82. local tmp code
  83. tmp="$(mktemp)"
  84. code="$(curl -sS -o "$tmp" -w '%{http_code}' \
  85. -H "Authorization: Bearer ${token}" \
  86. -H 'Content-Type: application/json' \
  87. -d "{\"database\":\"${ICEBERG_DB}\"}" \
  88. "${BACKEND_URL}/api/data/databases" || true)"
  89. if [[ "$code" != "200" && "$code" != "409" ]]; then
  90. cat "$tmp" >&2
  91. rm -f "$tmp"
  92. echo "failed to create iceberg namespace ${ICEBERG_DB}" >&2
  93. exit 1
  94. fi
  95. rm -f "$tmp"
  96. }
  97. import_csv() {
  98. local token="$1"
  99. local table="$2"
  100. local file_path="$3"
  101. curl -sS \
  102. -H "Authorization: Bearer ${token}" \
  103. -F "file=@${file_path}" \
  104. -F "mode=overwrite" \
  105. "${BACKEND_URL}/api/data/databases/${ICEBERG_DB}/tables/${table}/import/csv" >/dev/null
  106. }
  107. create_workflow_via_api() {
  108. local token="$1"
  109. local payload_file response_file code workflow_id
  110. payload_file="$(mktemp)"
  111. response_file="$(mktemp)"
  112. cat >"$payload_file" <<EOF
  113. {
  114. "workflowName": "${WORKFLOW_NAME}",
  115. "description": "Import three related Iceberg tables, clean dirty records, and generate sales KPIs",
  116. "timeoutSeconds": 9000,
  117. "failureStrategy": "STOP",
  118. "tasks": [
  119. {
  120. "id": 1,
  121. "name": "Clean Customers",
  122. "taskType": "SPARK_SQL",
  123. "clusterType": "SPARK",
  124. "script": "CREATE OR REPLACE TABLE polaris.${ICEBERG_DB}.customer_clean USING iceberg AS SELECT customer_id, trim(full_name) AS full_name, lower(trim(email)) AS email, initcap(trim(city)) AS city, upper(trim(region)) AS region, to_date(signup_date) AS signup_date, upper(trim(loyalty_tier)) AS loyalty_tier, CASE WHEN lower(trim(status)) IN ('active','1','y') THEN 'ACTIVE' ELSE 'INACTIVE' END AS status, CAST(age AS INT) AS age FROM polaris.${ICEBERG_DB}.demo_customers WHERE customer_id IS NOT NULL AND trim(email) <> '' AND email LIKE '%@%'",
  125. "executorId": 3001,
  126. "timeoutSeconds": 1200,
  127. "retryTimes": 1
  128. },
  129. {
  130. "id": 2,
  131. "name": "Clean Products",
  132. "taskType": "SPARK_SQL",
  133. "clusterType": "SPARK",
  134. "script": "CREATE OR REPLACE TABLE polaris.${ICEBERG_DB}.product_clean USING iceberg AS SELECT product_id, upper(trim(sku)) AS sku, trim(product_name) AS product_name, initcap(lower(trim(category))) AS category, CAST(price AS DECIMAL(12,2)) AS price, CAST(stock_qty AS INT) AS stock_qty, to_date(launch_date) AS launch_date, CASE WHEN lower(trim(active_flag)) IN ('y','1','true') THEN true ELSE false END AS is_active FROM polaris.${ICEBERG_DB}.demo_products WHERE product_id IS NOT NULL AND price > 0 AND stock_qty >= 0",
  135. "executorId": 3001,
  136. "timeoutSeconds": 1200,
  137. "retryTimes": 1
  138. },
  139. {
  140. "id": 3,
  141. "name": "Clean Orders",
  142. "taskType": "SPARK_SQL",
  143. "clusterType": "SPARK",
  144. "script": "CREATE OR REPLACE TABLE polaris.${ICEBERG_DB}.order_clean USING iceberg AS SELECT order_id, customer_id, product_id, to_date(order_date) AS order_date, CAST(quantity AS INT) AS quantity, CAST(unit_price AS DECIMAL(12,2)) AS unit_price, CAST(COALESCE(discount_rate, 0) AS DECIMAL(5,4)) AS discount_rate, upper(trim(status)) AS status, upper(trim(channel)) AS channel, upper(trim(region)) AS region, NULLIF(trim(note), '') AS note FROM polaris.${ICEBERG_DB}.demo_orders WHERE order_id IS NOT NULL AND customer_id IS NOT NULL AND product_id IS NOT NULL AND quantity > 0 AND unit_price > 0",
  145. "executorId": 3001,
  146. "timeoutSeconds": 1500,
  147. "retryTimes": 1
  148. },
  149. {
  150. "id": 4,
  151. "name": "Build Enriched Fact",
  152. "taskType": "SPARK_SQL",
  153. "clusterType": "SPARK",
  154. "script": "CREATE OR REPLACE TABLE polaris.${ICEBERG_DB}.order_enriched USING iceberg AS SELECT o.order_id, o.order_date, o.customer_id, c.full_name, c.city, c.region AS customer_region, p.category, p.sku, o.channel, o.status, o.quantity, o.unit_price, o.discount_rate, CAST(o.quantity * o.unit_price AS DECIMAL(14,2)) AS gross_amount, CAST(o.quantity * o.unit_price * (1 - COALESCE(o.discount_rate, 0)) AS DECIMAL(14,2)) AS net_amount FROM polaris.${ICEBERG_DB}.order_clean o JOIN polaris.${ICEBERG_DB}.customer_clean c ON o.customer_id = c.customer_id JOIN polaris.${ICEBERG_DB}.product_clean p ON o.product_id = p.product_id",
  155. "executorId": 3001,
  156. "timeoutSeconds": 1800,
  157. "retryTimes": 1
  158. },
  159. {
  160. "id": 5,
  161. "name": "Build Daily KPI",
  162. "taskType": "SPARK_SQL",
  163. "clusterType": "SPARK",
  164. "script": "CREATE OR REPLACE TABLE polaris.${ICEBERG_DB}.sales_kpi_daily USING iceberg AS SELECT order_date, customer_region, category, COUNT(*) AS order_count, COUNT(DISTINCT customer_id) AS customer_count, SUM(quantity) AS total_quantity, ROUND(SUM(net_amount), 2) AS net_revenue, ROUND(AVG(net_amount), 2) AS avg_net_order_value, ROUND(MAX(net_amount), 2) AS max_net_order_value FROM polaris.${ICEBERG_DB}.order_enriched GROUP BY order_date, customer_region, category",
  165. "executorId": 3001,
  166. "timeoutSeconds": 1200,
  167. "retryTimes": 1
  168. }
  169. ],
  170. "edges": [
  171. { "from": 1, "to": 4 },
  172. { "from": 2, "to": 4 },
  173. { "from": 3, "to": 4 },
  174. { "from": 4, "to": 5 }
  175. ]
  176. }
  177. EOF
  178. code="$(curl -sS -o "$response_file" -w '%{http_code}' \
  179. -H "Authorization: Bearer ${token}" \
  180. -H 'Content-Type: application/json' \
  181. --data-binary "@${payload_file}" \
  182. "${BACKEND_URL}/api/task-build/code/create" || true)"
  183. rm -f "$payload_file"
  184. if [[ "$code" != "200" ]]; then
  185. cat "$response_file" >&2
  186. rm -f "$response_file"
  187. echo "failed to create workflow via /api/task-build/code/create" >&2
  188. exit 1
  189. fi
  190. workflow_id="$(json_field "$response_file" workflowId)"
  191. rm -f "$response_file"
  192. if [[ -z "$workflow_id" ]]; then
  193. echo "workflow create response missing workflowId" >&2
  194. exit 1
  195. fi
  196. echo "$workflow_id"
  197. }
  198. TOKEN="$(obtain_token)"
  199. create_db "$TOKEN"
  200. import_csv "$TOKEN" "demo_customers" "${ROOT_DIR}/backend/src/main/resources/db/iceberg_demo/demo_customers.csv"
  201. import_csv "$TOKEN" "demo_products" "${ROOT_DIR}/backend/src/main/resources/db/iceberg_demo/demo_products.csv"
  202. import_csv "$TOKEN" "demo_orders" "${ROOT_DIR}/backend/src/main/resources/db/iceberg_demo/demo_orders.csv"
  203. WORKFLOW_ID="$(create_workflow_via_api "$TOKEN")"
  204. echo "Imported demo tables into Iceberg namespace ${ICEBERG_DB}."
  205. echo "Created demo workflow via /api/task-build/code/create, workflowId=${WORKFLOW_ID}."