Skip to content

Commit 1386646

Browse files
committed
demo: seed via CTE injection instead of CREATE TEMPORARY VIEW
The e2e run revealed that a DDL/command ExecutePlan over the grpc-web reattach path livelocks (an infinite ReattachExecute loop), hanging the demo at seeding. None of the existing passing tests issue a command - only data-returning queries - so the command path was unexercised. Define the synthetic retail dataset as CTEs injected into every query, so every statement sent to Spark is a data-returning SELECT (the working path). The table picker / DESCRIBE / queries are unchanged from the user's perspective. Co-authored-by: Isaac
1 parent 82c57e5 commit 1386646

2 files changed

Lines changed: 50 additions & 42 deletions

File tree

demo/README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ What it does:
2020
- **Runs your SQL**`spark.sql(...).toPandas()` over the blocking SAB/Atomics
2121
bridge, rendered as a result grid (first 1000 rows).
2222
- **Ships sample data** — a synthetic retail dataset (`customers`, `products`,
23-
`orders`) is seeded on load with `CREATE OR REPLACE TEMPORARY VIEW … AS SELECT
24-
… FROM range(N)`. The data is deterministic (seeded `rand`/`pmod`), so every
25-
session gets the identical dataset, with no warehouse writes or catalog config
26-
needed — it works on any Spark Connect server. Plus four example analytics
27-
queries (top products, revenue by country, monthly revenue, top customers).
23+
`orders`) defined as deterministic CTEs (seeded `rand`/`pmod`) that are injected
24+
into every query, so the "tables" need no DDL: every statement sent to Spark is
25+
a data-returning `SELECT`. Same dataset for every visitor, no warehouse writes,
26+
works on any Spark Connect server. Plus four example analytics queries (top
27+
products, revenue by country, monthly revenue, top customers).
28+
29+
> Why CTEs and not `CREATE TEMPORARY VIEW`? Command/DDL responses currently
30+
> livelock the grpc-web reattach path (an `ExecutePlan` for a command spins in
31+
> `ReattachExecute`); data-returning `SELECT`s work. The CTE injection keeps the
32+
> demo on the working path. See the note in `demo/index.html`.
2833
2934
It is the same boot path as [`jupyterlite/harness.html`](../pyspark_connect_web/jupyterlite/harness.html)
3035
(a module Web Worker running `worker_bootstrap.js` + the `bridge.js` blocking

demo/index.html

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -307,49 +307,53 @@ <h2>Starting PySpark in your browser</h2>
307307
" return v",
308308
" return str(v)",
309309
"",
310-
"def pcw_seed():",
311-
" # Per-session TEMPORARY VIEWs: deterministic (seeded rand/pmod), so every",
312-
" # visitor gets the identical dataset, with no warehouse writes or catalog",
313-
" # config needed - works on any Spark Connect server, and is self-contained.",
314-
" spark.sql('''CREATE OR REPLACE TEMPORARY VIEW products AS",
315-
" SELECT id AS product_id,",
316-
" element_at(array('Widget','Gadget','Gizmo','Doohickey','Sprocket','Cog','Lever','Valve'), cast(pmod(id,8) as int)+1) || ' #' || cast(id as string) AS product_name,",
317-
" element_at(array('Electronics','Home','Toys','Sports','Office'), cast(pmod(id,5) as int)+1) AS category,",
318-
" round(5 + rand(id)*195, 2) AS price",
319-
" FROM range(50)''')",
320-
" spark.sql('''CREATE OR REPLACE TEMPORARY VIEW customers AS",
321-
" SELECT id AS customer_id,",
322-
" 'Customer ' || cast(id as string) AS name,",
323-
" element_at(array('US','UK','DE','FR','JP','IN','BR','CA'), cast(pmod(id,8) as int)+1) AS country,",
324-
" date_add(date'2023-01-01', cast(pmod(id*7,700) as int)) AS signup_date",
325-
" FROM range(200)''')",
326-
" spark.sql('''CREATE OR REPLACE TEMPORARY VIEW orders AS",
327-
" SELECT id AS order_id,",
328-
" cast(pmod(id*31+7, 200) as bigint) AS customer_id,",
329-
" cast(pmod(id*17+3, 50) as bigint) AS product_id,",
330-
" cast(1 + pmod(id*13, 5) as int) AS quantity,",
331-
" date_add(date'2024-01-01', cast(pmod(id*3, 365) as int)) AS order_date",
332-
" FROM range(5000)''')",
333-
" _emit({'ok': True})",
310+
"# The synthetic retail dataset, defined as CTEs (deterministic seeded",
311+
"# rand/pmod). We inject these into every query so the demo's 'tables' need",
312+
"# no DDL: every statement sent to Spark is a data-returning SELECT. This is",
313+
"# deliberate - CREATE/temp-view COMMAND responses currently livelock the",
314+
"# grpc-web reattach path, while SELECT results work. Same dataset for every",
315+
"# visitor, no warehouse writes, works on any Spark Connect server.",
316+
"_CTES = '''",
317+
" products AS (",
318+
" SELECT id AS product_id,",
319+
" element_at(array('Widget','Gadget','Gizmo','Doohickey','Sprocket','Cog','Lever','Valve'), cast(pmod(id,8) as int)+1) || ' #' || cast(id as string) AS product_name,",
320+
" element_at(array('Electronics','Home','Toys','Sports','Office'), cast(pmod(id,5) as int)+1) AS category,",
321+
" round(5 + rand(id)*195, 2) AS price",
322+
" FROM range(50)),",
323+
" customers AS (",
324+
" SELECT id AS customer_id,",
325+
" 'Customer ' || cast(id as string) AS name,",
326+
" element_at(array('US','UK','DE','FR','JP','IN','BR','CA'), cast(pmod(id,8) as int)+1) AS country,",
327+
" date_add(date'2023-01-01', cast(pmod(id*7,700) as int)) AS signup_date",
328+
" FROM range(200)),",
329+
" orders AS (",
330+
" SELECT id AS order_id,",
331+
" cast(pmod(id*31+7, 200) as bigint) AS customer_id,",
332+
" cast(pmod(id*17+3, 50) as bigint) AS product_id,",
333+
" cast(1 + pmod(id*13, 5) as int) AS quantity,",
334+
" date_add(date'2024-01-01', cast(pmod(id*3, 365) as int)) AS order_date",
335+
" FROM range(5000))",
336+
"'''",
337+
"",
338+
"_TABLES = ['customers', 'orders', 'products']",
339+
"",
340+
"def _wrap(sql):",
341+
" s = sql.strip().rstrip(';').strip()",
342+
" if s[:5].lower() == 'with ':",
343+
" return 'WITH ' + _CTES + ',\\n' + s[5:]",
344+
" return 'WITH ' + _CTES + '\\n' + s",
334345
"",
335346
"def pcw_tables():",
336-
" rows = spark.sql('SHOW TABLES').collect()",
337-
" _emit([r['tableName'] for r in rows])",
347+
" _emit(_TABLES)",
338348
"",
339349
"def pcw_describe(b64):",
340350
" name = base64.b64decode(b64).decode('utf-8')",
341-
" rows = spark.sql('DESCRIBE TABLE ' + name).collect()",
342-
" cols = []",
343-
" for r in rows:",
344-
" cn = r['col_name']",
345-
" if not cn or cn.startswith('#'):",
346-
" break",
347-
" cols.append([cn, r['data_type']])",
348-
" _emit(cols)",
351+
" sdf = spark.sql(_wrap('SELECT * FROM ' + name + ' LIMIT 0'))",
352+
" _emit([[f.name, f.dataType.simpleString()] for f in sdf.schema.fields])",
349353
"",
350354
"def pcw_query(b64, limit):",
351355
" sql = base64.b64decode(b64).decode('utf-8')",
352-
" sdf = spark.sql(sql)",
356+
" sdf = spark.sql(_wrap(sql))",
353357
" cols = list(sdf.columns)",
354358
" pdf = sdf.limit(limit + 1).toPandas()",
355359
" truncated = len(pdf) > limit",
@@ -569,8 +573,7 @@ <h2>Starting PySpark in your browser</h2>
569573
await ready;
570574
bootStep("connecting to Spark (" + SPARK_REMOTE + ")…", 55);
571575
await call(PY_BOOTSTRAP);
572-
bootStep("seeding the demo retail dataset…", 80);
573-
await call("pcw_seed()");
576+
bootStep("loading tables…", 85);
574577
await renderTables();
575578
bootStep("ready", 100);
576579
$("overlay").style.display = "none";

0 commit comments

Comments
 (0)