Skip to Content
Under the Hood

Under the Hood

KoldStore keeps PostgreSQL authoritative for the hot tier and stores flushed history as open Parquet plus catalog metadata. This page explains the moving parts so API behavior makes sense.

End-to-end path

TEXT
heap commit  → logical slot (PK-only pgoutput)  → database worker  → koldstore.<table>__cl  (latest-state mirror)  → flush (policy or force)  → Parquet segments + manifest  → SELECT via KoldMergeScan (newest visible wins)

There is no trigger/strict capture path today. Foreground DML writes the heap only; the serialized WAL applier allocates authoritative seq values.

Hot tier

  • Ordinary heap table with a primary key
  • Application columns only (clean schema — no KoldStore columns on the user table)
  • Native indexes cover hot rows only after flush
  • Transactions, locking, MVCC, RLS, and permissions stay PostgreSQL-native

Change-log mirror (koldstore.<table>__cl)

One row per primary key (latest state), not an append-only event log:

ColumnMeaning
PK columnsSame as the heap
seqSnowflake effect id — ordering / flush cutoffs / change cursor
op1 = INSERT, 2 = UPDATE, 3 = DELETE
commit_lsnDiagnostics only — not the public resume cursor

Tombstones (op = 3) remain until flush prunes them. seq is not a commit LSN and is not a commit-order guarantee beyond “allocated by the serialized applier.”

WAL capture consistency

ModeBehavior
NormalWorker polls (~100 ms) and applies available WAL
Strong fencekoldstore.wait_for_async_mirror() drains to a boundary
FlushCalls the same fence automatically

Lag is expected. Call the fence before comparing heap state to mirror/seq cursors when you need a strong boundary.

Other capture rules:

  • Primary-key UPDATEs are rejected (BEFORE UPDATE OF pk guard)
  • TRUNCATE on async managed tables is rejected
  • Publication koldstore_async_mirror + one deterministic logical slot per database are auto-managed

Cold tier

  • Parquet segments under a registered storage backend
  • Manifest metadata in PostgreSQL catalogs (koldstore.manifest, koldstore.cold_segments, …)
  • Object storage is not WAL-protected — back up PostgreSQL and the cold prefix together

Default path templates:

  • Shared: {namespace}/{tableName}/
  • User-scoped (intended): {namespace}/{tableName}/{scopeId}/

Scoped cold folders and full per-user_id physical layout are still hardening; session koldstore.user_id is required for user-typed tables today.

Flush policy (intuition)

Example: hot_row_limit = 10000, min_flush_rows = 1000, max_rows_per_file = 1000

Mirror rowsResult
10,505No flush (505 excess < min)
11,000Flush 1,000 → 1 file
11,250Flush 1,000, keep 10,250 hot

Age policy uses koldstore_move_after (for example '90 days') measured from mirror seq. It is mutually exclusive with hot_row_limit. koldstore_move_when is reserved and fails closed.

KoldMergeScan

TEXT
KoldMergeScan├── native hot child (Index / Bitmap / Seq Scan)├── KoldParquetScan└── MirrorOverlay (inserts / updates / tombstones)

Contracts that matter for operators:

  • Cold-capable predicates → KoldMergeScan with hot child + planned cold access
  • Hot-only / cold-proven-empty PK lookups may keep a native plan with no wrapper (intentional performance path)
  • koldstore.enable_merge_scan = offERROR at execution (never silent hot-only)
  • Unavailable cold storage → query fails (no partial hot-only results)

Catalog touchpoints

Useful relations/functions while debugging:

  • koldstore.schemas — managed table options
  • koldstore.jobs — migrate / flush progress
  • koldstore.describe_table(...) — hot/mirror/cold counts, sizes, pending jobs
  • koldstore.async_mirror_status() — slot lag, retained bytes, health

Next

Last updated on