Skip to content

logic

Pure transformations, with no storage or infrastructure dependencies.

Entity Aggregation

Fold a stream of statement dicts into entities, without building FtM objects – ftmq.aggregate:

from ftmq.aggregate import aggregate_statement_payloads

for entity in aggregate_statement_payloads(statement_dicts, "my_dataset"):
    print(f"{entity.id}: {entity.to_dict()['caption']}")

The input must be contiguous per entity_id, as the parquet store's reads are.

Parquet helpers

The DuckDB SQL behind ParquetStore. Reads, merges and re-shards name a partition's files directly (partition_source_sql); a read's statement view is a plain scan over merged partitions (live_rows_sql) and the dedupe query otherwise (dedupe_rows_sql). raw_view_sql is the connection-level LakeStore's statement_raw view over delta_scan; statements sql runs on ParquetStore.cursor, the whole table registered like a read.

ftm_lakehouse.logic.parquet.duckdb_config()

DuckDB config from the lakehouse settings – memory limit, spill and extension directories.

Call once per DuckDB instance: each call gets its own spill subdirectory, as instances sharing one overwrite each other's spill files. Warns once when that directory is RAM-backed. No TimeZone – at connect time it installs icu (fails offline); sessions pin UTC after connecting.

Source code in ftm_lakehouse/logic/parquet.py
def duckdb_config() -> dict[str, str]:
    """DuckDB config from the lakehouse settings – memory limit, spill and
    extension directories.

    Call once per DuckDB instance: each call gets its own spill subdirectory,
    as instances sharing one overwrite each other's spill files. Warns once
    when that directory is RAM-backed. No ``TimeZone`` – at connect time it
    installs ``icu`` (fails offline); sessions pin UTC after connecting.
    """
    settings = Settings()
    config: dict[str, str] = {"memory_limit": settings.duckdb_memory_limit}
    if settings.duckdb_temp_directory:
        # DuckDB creates the spill directory but not its parents
        os.makedirs(settings.duckdb_temp_directory, exist_ok=True)
        _warn_ram_spill(settings.duckdb_temp_directory)
        config["temp_directory"] = os.path.join(
            settings.duckdb_temp_directory, ensure_uuid()
        )
    if settings.duckdb_extension_directory:
        config["extension_directory"] = settings.duckdb_extension_directory
    return config

ftm_lakehouse.logic.parquet.raw_view_sql(dt)

SELECT body for the statement_raw view: every physical row, tombstones and duplicates included.

Source code in ftm_lakehouse/logic/parquet.py
def raw_view_sql(dt: DeltaTable) -> str:
    """SELECT body for the ``statement_raw`` view: every physical row,
    tombstones and duplicates included."""
    return f"SELECT * FROM {delta_scan_sql(dt.table_uri)}"

ftm_lakehouse.logic.parquet.live_rows_sql(source)

Live rows of a clean source – a filtered scan. canonical_id is entity_id, for ftmq's query layer.

Source code in ftm_lakehouse/logic/parquet.py
def live_rows_sql(source: str) -> str:
    """Live rows of a **clean** ``source`` – a filtered scan. ``canonical_id`` is
    ``entity_id``, for ftmq's query layer."""
    return f"SELECT *, entity_id AS canonical_id FROM {source} WHERE deleted_at IS NULL"

ftm_lakehouse.logic.parquet.dedupe_rows_sql(source)

Live rows of a source that may hold un-merged files: _dedupe_sql, canonical_id as in live_rows_sql. Row for row what the same partition reads once merged.

Source code in ftm_lakehouse/logic/parquet.py
def dedupe_rows_sql(source: str) -> str:
    """Live rows of a ``source`` that may hold un-merged files: `_dedupe_sql`,
    ``canonical_id`` as in [`live_rows_sql`][live_rows_sql]. Row for row what
    the same partition reads once merged."""
    return _dedupe_sql(source, select="*, entity_id AS canonical_id")

ftm_lakehouse.logic.parquet.delta_scan_sql(table_uri)

delta_scan('<uri>'), the uri quote-escaped (no prepared parameters).

Source code in ftm_lakehouse/logic/parquet.py
def delta_scan_sql(table_uri: str) -> str:
    """``delta_scan('<uri>')``, the uri quote-escaped (no prepared parameters)."""
    return f"delta_scan('{_string_literal(table_uri)}')"

ftm_lakehouse.logic.parquet.build_merge_sql(shard, bucket, origin, grace_cutoff, source=TABLE_RAW.name, select='*')

_dedupe_sql collapsing one partition for a merge, ordered by the file sort key. Tombstones within grace survive to keep shadowing.

Parameters:

Name Type Description Default
shard str

The partition's shard.

required
bucket str

The partition's bucket.

required
origin str

The partition's origin – validated before interpolation.

required
grace_cutoff datetime

Tombstones with deleted_at <= grace_cutoff are dropped.

required
source str

Relation holding the rows – statement_raw, or one partition's files (partition_source_sql).

name
select str

Projection of the output, e.g. without the partition columns.

'*'
Source code in ftm_lakehouse/logic/parquet.py
def build_merge_sql(
    shard: str,
    bucket: str,
    origin: str,
    grace_cutoff: datetime,
    source: str = TABLE_RAW.name,
    select: str = "*",
) -> str:
    """`_dedupe_sql` collapsing one partition for a merge, ordered by the file
    sort key. Tombstones within grace survive to keep shadowing.

    Args:
        shard: The partition's shard.
        bucket: The partition's bucket.
        origin: The partition's origin – validated before interpolation.
        grace_cutoff: Tombstones with ``deleted_at <= grace_cutoff`` are dropped.
        source: Relation holding the rows – ``statement_raw``, or one
            partition's files ([`partition_source_sql`][partition_source_sql]).
        select: Projection of the output, e.g. without the partition columns.
    """
    origin = validate_origin(origin)
    return _dedupe_sql(
        source=source,
        where=f"WHERE shard = '{shard}' AND bucket = '{bucket}' AND origin = '{origin}'",
        tombstone=(
            "(deleted_at IS NULL OR deleted_at > "
            f"TIMESTAMPTZ '{grace_cutoff.isoformat()}')"
        ),
        order_by="ORDER BY entity_id, fragment, role, prop, id, last_seen DESC",
        select=select,
    )

ftm_lakehouse.logic.parquet.partition_source_sql(files, shard, bucket, origin)

One partition's files as a SHARDED_SCHEMA relation, read without delta_scan – relation_source_sql over the files, columns they predate reading as NULL.

Source code in ftm_lakehouse/logic/parquet.py
def partition_source_sql(
    files: Iterable[str], shard: str, bucket: str, origin: str
) -> str:
    """One partition's files as a `SHARDED_SCHEMA` relation, read without
    ``delta_scan`` – [`relation_source_sql`][relation_source_sql] over the
    files, columns they predate reading as ``NULL``.
    """
    files_sql = (
        f"(SELECT * FROM {read_parquet_sql(files)} UNION ALL BY NAME {_FILE_COLUMNS})"
    )
    return relation_source_sql(files_sql, shard, bucket, origin)

ftm_lakehouse.logic.parquet.relation_source_sql(relation, shard, bucket, origin)

relation – rows of a partition's files, the partition columns absent – as a SHARDED_SCHEMA relation: the partition columns as constants, fragment coalesced to ''. origin is validated before interpolation.

Source code in ftm_lakehouse/logic/parquet.py
def relation_source_sql(relation: str, shard: str, bucket: str, origin: str) -> str:
    """``relation`` – rows of a partition's files, the partition columns absent –
    as a `SHARDED_SCHEMA` relation: the partition columns as constants,
    ``fragment`` coalesced to ``''``. ``origin`` is validated before
    interpolation."""
    origin = validate_origin(origin)
    constants = {"shard": shard, "bucket": bucket, "origin": origin}
    projection = ", ".join(
        (
            f"'{_string_literal(constants[f.name])}' AS {f.name}"
            if f.name in constants
            else (
                "COALESCE(fragment, '') AS fragment" if f.name == "fragment" else f.name
            )
        )
        for f in SHARDED_SCHEMA
    )
    return f"(SELECT {projection} FROM {relation})"

ftm_lakehouse.logic.parquet.read_parquet_sql(files)

read_parquet over files, unioned by name. No hive partitioning: it would parse a shard like 10 as an integer.

Source code in ftm_lakehouse/logic/parquet.py
def read_parquet_sql(files: Iterable[str]) -> str:
    """``read_parquet`` over ``files``, unioned by name. No hive partitioning:
    it would parse a shard like ``10`` as an integer."""
    paths = ", ".join(f"'{_string_literal(f)}'" for f in files)
    return f"read_parquet([{paths}], union_by_name = true, hive_partitioning = false)"

ftm_lakehouse.logic.parquet.worker_duckdb_config(workers)

duckdb_config for one of workers processes, one call per task.

Memory limit and threads are split between the workers.

Source code in ftm_lakehouse/logic/parquet.py
def worker_duckdb_config(workers: int) -> dict[str, str]:
    """[`duckdb_config`][duckdb_config] for one of ``workers`` processes, one call
    per task.

    Memory limit and threads are split between the workers.
    """
    config = duckdb_config()
    budget = parse_byte_size(config["memory_limit"])
    config["memory_limit"] = f"{budget // workers}B"
    config["threads"] = str(max((os.cpu_count() or 1) // workers, 1))
    return config

ftm_lakehouse.logic.parquet.build_shard_sql(shard, bucket, origin, shards, source=TABLE_RAW.name)

One partition's raw rows with shard recomputed for shards shards.

Unordered and un-deduped – the follow-up merge does both. origin is validated before interpolation.

Source code in ftm_lakehouse/logic/parquet.py
def build_shard_sql(
    shard: str, bucket: str, origin: str, shards: int, source: str = TABLE_RAW.name
) -> str:
    """One partition's raw rows with ``shard`` recomputed for ``shards`` shards.

    Unordered and un-deduped – the follow-up merge does both. ``origin`` is
    validated before interpolation.
    """
    origin = validate_origin(origin)
    return (
        f"SELECT * REPLACE ({shard_expr_sql(shards)} AS shard) "
        f"FROM {source} "
        f"WHERE shard = '{shard}' AND bucket = '{bucket}' AND origin = '{origin}'"
    )

ftm_lakehouse.logic.parquet.shard_expr_sql(shards, column='entity_id')

DuckDB expression for column's shard key – the SQL twin of entity_shard, pinned to agree by test_shard_expr_sql_parity.

Source code in ftm_lakehouse/logic/parquet.py
def shard_expr_sql(shards: int, column: str = "entity_id") -> str:
    """DuckDB expression for ``column``'s shard key – the SQL twin of
    `entity_shard`, pinned to agree by ``test_shard_expr_sql_parity``."""
    if shards <= 1:
        return "'0'"
    width = shard_hex_width(shards)
    return (
        f"printf('%0{width}x', "
        f"(('0x' || substr(sha1({column}), 1, 8))::BIGINT) % {int(shards)})"
    )

Merge helpers

The pure Arrow side of ParquetStore.merge: a partition is read as entity_id-sorted streams and cut into entity-closed chunks, each deduped by build_merge_sql on its own – every window key holds entity_id, so the chunks give the rows the whole partition would, with memory bounded by the chunk rather than the partition.

ftm_lakehouse.logic.merge.entity_chunks(streams, target)

Cut entity_id-sorted streams into entity-closed chunks of about target Arrow bytes, in ascending entity_id ranges.

A chunk holds every row of each of its entities – across all streams – and no two chunks share an entity, so a query keyed on entity_id gives the same rows per chunk as over the whole. Rows within a chunk are not sorted. Memory is the chunk plus about one batch per stream.

Each step advances the stream whose newest batch ends lowest; every row below the lowest such end is complete, as no stream has more of it to give.

Parameters:

Name Type Description Default
streams Iterable[Iterator[RecordBatch]]

Record batches sharing one schema, each stream ascending by entity_id.

required
target int

Arrow bytes to read between two chunks.

required

Yields:

Type Description
Table

pa.Table per chunk.

Source code in ftm_lakehouse/logic/merge.py
def entity_chunks(
    streams: Iterable[Iterator[pa.RecordBatch]], target: int
) -> Iterator[pa.Table]:
    """Cut ``entity_id``-sorted streams into entity-closed chunks of about
    ``target`` Arrow bytes, in ascending ``entity_id`` ranges.

    A chunk holds every row of each of its entities – across all streams – and
    no two chunks share an entity, so a query keyed on ``entity_id`` gives the
    same rows per chunk as over the whole. Rows within a chunk are not sorted.
    Memory is the chunk plus about one batch per stream.

    Each step advances the stream whose newest batch ends lowest; every row
    below the lowest such end is complete, as no stream has more of it to give.

    Args:
        streams: Record batches sharing one schema, each stream ascending by
            ``entity_id``.
        target: Arrow bytes to read between two chunks.

    Yields:
        ``pa.Table`` per chunk.
    """
    sources = [iter(stream) for stream in streams]
    ends: list[tuple[str, int]] = []  # (last entity_id read, stream) heap
    held: list[pa.RecordBatch] = []
    since = 0

    def pull(i: int) -> None:
        nonlocal since
        for batch in sources[i]:
            if batch.num_rows:
                held.append(batch)
                since += batch.nbytes
                heapq.heappush(ends, (batch.column("entity_id")[-1].as_py(), i))
                return

    for i in range(len(sources)):
        pull(i)
    while ends:
        _, i = heapq.heappop(ends)
        pull(i)
        if (ends and since < target) or not held:
            continue
        table = pa.Table.from_batches(held)
        since = 0
        if not ends:
            held = []
            yield table
            continue
        below = pc.less(table.column("entity_id"), ends[0][0])
        held = table.filter(pc.invert(below)).to_batches()
        chunk = table.filter(below)
        if chunk.num_rows:
            yield chunk

Statement Serialization

Statements are packed once, columnwise, by ftm_lakehouse.model.statement.statements_to_arrow – see Model.