Streamhouse · STH-03
Apache Fluss — PK tables, lake tiering, streaming vs batch consistency
- 5 h
- requires STH-02
From your world. One copy that serves both a sub-second lookup and a historical batch scan
Fluss PK table + lake tiering — streaming and batch reads must match
A Fluss primary-key table tiers its data down into a lake table (Paimon). The streamhouse promise is one copy, two latencies, the same answer. Tiering has quiesced. Then a streaming read and a batch read of the tiered data must return the same set of rows. The lake tier that Fluss writes into is a Paimon PK table. So in this exercise you author that tiered table. You prove the read-consistency invariant on it directly, with no Docker.
Implement, in solution.py:
def tier_table_ddl(table: str) -> str:
"""CREATE TABLE for the tiered lake table `table` with columns
(id INT, bal INT), configured so a streaming read and a batch read return
the SAME final state (latest value per id)."""
def upsert_inserts(table: str, upserts) -> list[str]:
"""INSERT statements upserting each (id, bal) in `upserts` into `table`,
in order. Later writes for the same id supersede earlier ones."""
upserts is a list of (id, bal) tuples that you apply in order. The final state of the table is
the last bal per id.
Requirements
- The table must declare
PRIMARY KEY(id) NOT ENFORCEDand'bucket'='1'. Then the layout is deterministic and the latest write per key wins. - You must configure the table so a bounded streaming scan and a batch scan return the
identical final state. To do this:
- The table collapses multiple writes per key (the
deduplicatemerge engine). - The table defines a changelog (
'changelog-producer'='input'). Then the streaming scan is well defined.
- The table collapses multiple writes per key (the
- An append-only table (no primary key) would keep every version. A streaming scan would then show rows that a batch scan has collapsed. This is exactly the inconsistency to avoid.
What the tests do (G6 oracle)
- Read consistency (always runs, embedded Paimon — the lake-tier format): The test runs your
DDL and upserts. Then it reads the table once in batch and once as a bounded streaming
scan (with a deadline, so it never hangs). The test compares the two results to each other. It
also compares them to an independent Python oracle (last value per id) with
sth_oracle(order-insensitive multiset equality). - Native Fluss path (SKIPs if the vendored Fluss server is absent): The test starts a real Fluss local cluster (ZooKeeper + coordinator + tablet server) on free ports and then stops it. This proves that the streaming-store half of the tier exists on a bare machine with no Docker.
Everything may only skip. Then run bash labs/fetch_streamhouse.sh (and bash labs/fetch_flink.sh).
The fixtures print the exact command.
Starter code
"""STH-03/01 — build the tiered lake table so streaming and batch reads match.
TODO: the starter below creates an APPEND-ONLY table (no primary key), so it
keeps every version of a key. A streaming scan then shows duplicate rows that a
batch scan has not collapsed -> the read-consistency invariant fails. Give the
table a primary key and a merge engine so the latest write per id wins.
"""
def tier_table_ddl(table: str) -> str:
# BUG: no PRIMARY KEY and no merge engine -> append-only; every write is kept.
return (
f"CREATE TABLE {table} ("
"id INT, bal INT) "
"WITH ('bucket'='1', 'bucket-key'='id')"
)
def upsert_inserts(table, upserts):
stmts = []
for cid, bal in upserts:
bal_lit = "CAST(NULL AS INT)" if bal is None else str(int(bal))
stmts.append(f"INSERT INTO {table} VALUES ({int(cid)}, {bal_lit})")
return stmts
Hints
Hint 1
The lake tier Fluss writes into is a Paimon PK table. For a streaming read and a batch read to return the SAME final state, the table must collapse multiple writes per key to the latest value: declare PRIMARY KEY(id) NOT ENFORCED and 'bucket'='1'. An append-only (no-PK) table keeps every version, so a streaming scan shows duplicates a batch scan does not.
Hint 2
Use the deduplicate merge engine (the default for a PK table) so the latest write per key wins, and set 'changelog-producer'='input' so a streaming scan is well defined. 'bucket'='1' makes the small table deterministic.
Hint 3
Each account id is upserted several times; the final state is the last value per id. Your DDL + upserts are run twice: once read in BATCH, once read as a bounded STREAMING scan. The two results must be identical (the read-consistency invariant).
uv run de start STH-03/01-tiered-read-consistency
# edit workspace/STH-03/01-tiered-read-consistency/
uv run de test