Trigger mode versus sink
Simulate a modelNeeds Python
Problem
A window that fires early and then again (when late data arrives) can emit either the full total each time (accumulating) or only the change since the last firing (discarding). The sink must match: an overwriting sink keyed by window wants totals, an additive counter wants changes. A mismatch double-counts or loses data, with no error.
Emit a window total twice (3,100, then 3,260). Write both into (a) a table keyed by window with overwrite and (b) an additive counter, in accumulating mode, then in discarding mode (emit 3,100, then +160).
The question. Which combinations of trigger mode and sink give the right total of 3,260?
Predict
Accumulating output with the additive sink gives 6,360, and discarding mode with the additive sink gives 3,260.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
trigger-mode-versus-sink.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Trigger mode against sink. A window fires twice: an early total of 3,100, then the final 3,260. The firings are emitted in accumulating mode (each firing carries the full total) or discarding mode (each carries only what is new: 3,100, then +160), into a sink that overwrites by window key or one that adds. """ import argparse, json p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() firings = {"accumulating": [3100, 3260], "discarding": [3100, 160]} rows = [] for mode, values in firings.items(): for sink in ("overwrite by window key", "additive counter"): table = {} for v in values: if sink.startswith("overwrite"): table["w1"] = v else: table["w1"] = table.get("w1", 0) + v rows.append({"mode": mode, "sink": sink, "result": table["w1"], "correct": "yes" if table["w1"] == 3260 else "no"}) for r in rows: print(f"{r['mode']:<13} into {r['sink']:<24} -> {r['result']:>5} correct: {r['correct']}") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"combinations": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run trigger-mode-versus-sink.py uv run trigger-mode-versus-sink.py --help # every knob
Our run
Four combinations
| Mode | Sink | Result | Correct |
|---|---|---|---|
| accumulating | overwrite by window key | 3.26K | yes |
| accumulating | additive counter | 6.36K | no |
| discarding | overwrite by window key | 160 | no |
| discarding | additive counter | 3.26K | yes |
accumulating into overwrite by window key -> 3260 correct: yes
accumulating into additive counter -> 6360 correct: no
discarding into overwrite by window key -> 160 correct: no
discarding into additive counter -> 3260 correct: yesWhat we found
Accumulating into the overwriting table gave 3,260; into the additive counter, 6,360. Discarding into the additive counter gave 3,260; into the overwriting table, 160, only the last change.
Read it
Trigger mode and sink semantics are one decision, not two: totals belong in sinks that overwrite by key, deltas in sinks that add. Writing the mode into the output's contract is the only way a consumer can tell which it is reading.