Timelaketimelake
DocsBenchmarksRoadmapSpecContribute
Apache 2.0 ·
Roadmap

DoneIn progressPlanned

Tables and storage

Done:1. Iceberg tablesDone:2. CommitsDone:3. Object-store catalogIn progress:4. Time-series table contractIn progress:5. File layout and encodingsIn progress:6. Last-value indexDone:7. One-request table state and version checksDone:8. Table format versions and upgradesDone:9. SettingsPlanned:10. Supplemental timestamps

Writing

Done:11. AppendsDone:12. Late data and dedup on readDone:13. Writes from WebAssemblyPlanned:14. Hot tier and seconds-fresh readsPlanned:15. BackfillIn progress:16. Import and migrationPlanned:17. DeletesPlanned:18. GDPR erasure

Maintenance

Done:19. CompactionIn progress:20. Maintenance runs, retention and expiry

Reading

In progress:21. Timelake readerDone:22. Block cacheDone:23. Filter pushdownDone:24. Pruning with long IN listsDone:25. Newest-first scansDone:26. Scans in series orderDone:27. Materialized rollups

SQL functions

In progress:28. Function libraryDone:29. Time buckets and gap fillingDone:30. Signal aggregatesDone:31. Downsampling with m4

Integrations

Planned:32. REST catalogPlanned:33. DuckDB extensionPlanned:34. Snowflake connectionPlanned:35. Multitenancy

Services

Planned:36. Ingestion and rulesPlanned:37. Data to eventsPlanned:38. Static read modelsPlanned:39. Computed metricsPlanned:40. Agent access with permission setsPlanned:41. Device syncPlanned:42. Edge gatewayPlanned:43. Server mode

Benchmarks

In progress:44. Benchmark harness: timebenchIn progress:45. Industrial IoT benchmarkPlanned:46. Agent benchmark

Background

ArchitectureDeployment and cost modelEmbedded engine: DataFusionLandscape and positioningLicense and open corePurpose and requirementsReleases
TimelaketimelakeThe open time-series lakehouse.
v0 · Apache 2.0 · Contribute · GitHubBuilt on Apache Arrow, Apache DataFusion, Apache Parquet, and Apache Iceberg.
DocsBenchmarksRoadmapSpecContribute

Spec 12 · Writing

Late data and dedup on read

DoneUpdated 2026-10-16View on GitHub
Versions
0.5.0
Roadmap
Exact answers before compaction
Related
4. Time-series table contract, 19. Compaction, 27. Materialized rollups, 30. Signal aggregates

Summary

Late data is normal: it lands in its time partition and each commit records the ranges it touched. Copies of a row are removed by compaction and on read (the latest write wins by insertion time, then commit order); cold partitions compact sooner so dedup on read only touches recent data.

Design

Late and out-of-order data, dedup

  • Late data is normal, not an error. It is written to its correct time partition. Each commit records the time ranges it touched (per partition, optionally per series bucket) in the snapshot summary.
  • Small-file control: Late data scattered across old partitions creates small files. The compactor prioritizes partitions by small-file count × query heat.
  • Rollup invalidation: The rollup refresher diffs snapshots since its last processed snapshot and recomputes only the affected buckets (spec 27). No separate change log is needed.
  • Duplicates and corrections: IoT devices resend data. Per-table policy:
    • append (default for raw): Duplicates are removed at compaction and at read time by the Timelake reader, using dedup_key and keeping the latest _ingest_ts.
    • upsert: Same as append, but exposed as a first-class semantic for corrections.
  • Trade-off: Plain DuckDB may see duplicates in recently written, not-yet-compacted partitions. The Timelake DuckDB extension removes them on read. Open question Q3 (#106).

Built so far (2026-10-16). The key is (series…, time). The latest write wins: the higher Iceberg data sequence number (commit order), and within one append the later row. An append keeps only the last of equal keys, so every file is free of duplicates on its own. The Timelake reader removes copies on read (merge below); compaction merges only runs of files next to each other in commit order, and a merged file keeps the newest data sequence number it replaces. The first version cost 1–4.6× on not-yet-compacted IIoT data (spec 45), because there nearly every file of a day overlaps every other; see the measurements below for where it stands now.

Insertion time, _ingest_ts (built 2026-10-16, table format 0.3.0).

  • Tables created with a time column get a _ingest_ts column (timestamp with time zone, µs, not null) unless created with ingest_time = false (--no-ingest-time). The table records it in timelake.ingest-time-column.
  • The writer sets it once per append, to the time the append started, for every row. An input that already has the column (an import keeping the original arrival times) keeps its values.
  • The latest write is the highest (_ingest_ts, commit order); tables without the column use commit order alone. Writers with skewed clocks can therefore order copies by their clocks rather than by commit; commit order breaks ties.
  • Other engines can dedup with it: QUALIFY row_number() OVER (PARTITION BY <series…>, <time> ORDER BY _ingest_ts DESC) = 1. Arrival delay (_ingest_ts - time) measures store-and-forward lateness per row.
  • It costs little storage (one value per append, run-length encoded). Adding it needs table format 0.3.0: older Timelakes can't open such tables. Existing tables can't gain the column until schema evolution exists.

Dedup on read strategies (setting read-dedup-strategy, spec 9):

  • merge (built): files that may share a key are read with their commit rank, split by a hash of the key, merged in key order per slice, and the last row of each key kept. Only filters on key columns may apply before the merge, so filters on other columns can't prune those files.
  • keys-first (built 2026-10-16): pass 1 reads only the key columns (and _ingest_ts or the rank) of the files that may share keys, restricted by key filters, merges them and lists the older copies as (file rank, key). Pass 2 is the normal parallel scan with every filter pushed down, anti-joined against that list. Pass 1 reads two or three columns instead of every projected one; pass 2 prunes as freely as a scan without dedup, because the list was built from all rows.
  • Measured (2026-10-16, IIoT 50 machines × 9 days, before maintenance, laptop): keys-first was slower on every query (1.3–6×; pass 2's anti-join probe costs more than merging the value columns), so merge is the default and keys-first stays a setting.
  • What made merge cheap: splitting by the series hash inside the dedup operator (DataFusion otherwise re-sorts each slice), keeping each series whole in one slice so windows and groups by series need no new sort, filtering rows by key filters and a join's runtime filters before the merge, and comparing neighbouring keys column by column instead of row by row. Window queries (gaps, time-above-limit) then cost the same with and without dedup; scans of every row (daily-stats) still cost 2.6×, which cold-partition compaction removes (1.1×), leaving only the newest day to merge.

Cold partitions compact sooner (built 2026-10-16, setting compact-cold-after). Inline maintenance rewrites a partition into one sorted, duplicate-free file once its time range ended longer ago than the setting (default 1 hour) and it has more than one file. Backfill into a cold partition triggers the same rewrite after compact-min-files new files, not after every append. Dedup on read then only touches the current partitions, and DuckDB sees copies for a shorter time.

Open questions and issues

  • #38 Reader: deduplicate only files that may overlap, flagged at write time
  • #66 Maintenance: keep late data for a closed slot in its own tier
  • #106 Question: may plain DuckDB readers see duplicates before compaction

History

  • 2026-10-16: dedup on read (merge and keys-first), _ingest_ts, cold-partition compaction built
  • 2026-10-19: moved from SPEC.md into its own spec.
  • 2026-10-19: specs regrouped one per feature, with milestones on the roadmap.