DE Mastery GitHub

Streamhouse · STH-03

Apache Fluss — PK tables, lake tiering, streaming vs batch consistency

From your world. One copy that serves both a sub-second lookup and a historical batch scan

← All exercises

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 ENFORCED and '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 deduplicate merge engine).
    • The table defines a changelog ('changelog-producer'='input'). Then the streaming scan is well defined.
  • 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)

  1. 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).
  2. 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

solution.py
"""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).

Run it locally
uv run de start STH-03/01-tiered-read-consistency
# edit workspace/STH-03/01-tiered-read-consistency/
uv run de test