Layer 1: Model
Pure data structures with no dependencies. Pydantic models for serialization.
Dataset Models
ftm_lakehouse.model.DatasetModel
Bases: Dataset
Source code in ftm_lakehouse/model/dataset.py
compression = None
class-attribute
instance-attribute
Compress exported artifacts (statements.csv, entities.ftm.json, diffs...)
public_url_prefix = None
class-attribute
instance-attribute
Public url prefix for resources
shards = DEFAULT_SHARDS
class-attribute
instance-attribute
Number of entity-id hash shards for the parquet store. 0 (default)
means a single shard; huge datasets should configure 8 or more at
creation for bounded per-partition working sets (e.g.
ensure_dataset("big_leak", shards=8)). Fixed once the store is
written: setting it here only changes where readers look, so changing
it after the fact means a full rewrite –
ShardOperation.
storage = None
class-attribute
instance-attribute
Set storage for external lakehouse
Statement Schema
Two schemas, one column apart. JOURNAL_SCHEMA is what every write path packs, the journal table (journal_table) stores and the api wire carries. SHARDED_SCHEMA adds the shard partition key and is what parquet holds – ParquetStore.append derives it from entity_id.
LakehouseStatement is ftmq's LakeStatement plus the two columns the lakehouse adds: deleted_at (the tombstone marker) and role (who asserted the statement). statements_to_arrow packs statements into a JOURNAL_SCHEMA table for both write paths and applies their shared fill rules.
ftm_lakehouse.model.statement.LakehouseStatement
Bases: LakeStatement
A statement carrying the two columns the lakehouse adds to the schema.
deleted_at is the tombstone marker – a storage fact about a statement
rather than statement content, and lakehouse-only (ftmq's lake store
deletes physically). role records who asserted the statement: an
identifier a submitting application supplies, alongside origin's
where. Both live here for the same reason fragment lives on
ftmq.store.lake.LakeStatement: so the write path can pass statements
around instead of (stmt, deleted_at, role) tuples.
role joins origin and fragment in dedupe_key, so two roles
asserting identical content stay two rows through
merge – full
provenance, rather than one row whose role is whoever wrote last. The
empty string collapses to None so "no role" has one representation.
There is deliberately no shard attribute – a statement is content plus
provenance, and which partition it lands in is
append's call.
Source code in ftm_lakehouse/model/statement.py
dedupe_key
property
Stable row identity: id, origin, fragment, role.
Extends ftmq.store.lake.LakeStatement.dedupe_key with the
lakehouse's fourth identity dimension, so the write buffers collapse
re-emissions exactly where merge does.
ftm_lakehouse.helpers.statements.dedupe_key keeps the same key shape
for the packed-row paths that never build a statement object.
from_db_row(row)
classmethod
Read a statement back from a SQL row, keeping role.
Source code in ftm_lakehouse/model/statement.py
from_dict(data)
classmethod
Read a statement back from a row dict, keeping role.
deleted_at is deliberately not read back: every consumer of this
(statement queries, the api NDJSON wire) reads the live view, where
a surfaced row is by definition not a tombstone.
Source code in ftm_lakehouse/model/statement.py
ftm_lakehouse.model.statement.statements_to_arrow(statements, now)
Pack a stream of statements into a JOURNAL_SCHEMA table.
ftmq's statements_to_table packs the statement
columns columnwise; this adds the two columns the lakehouse stores on top
(role and deleted_at), drops canonical_id (this store never
resolves entities), and applies the two rules both write paths share:
first_seen/last_seenfall back tonowwhen the statement carries none,- tombstones (
deleted_atset) bumplast_seento the later of the delete timestamp and the row they shadow, so they win theROW_NUMBER() OVER (... ORDER BY last_seen DESC, deleted_at DESC NULLS LAST)tiebreak inParquetStore.merge. Taking the delete timestamp alone would lose to a row dated in the future – input carrieslast_seen, so nothing bounds it by the wall clock – andmergewould drop the tombstone rather than the row, leaving the entity undeletable on every retry. On the tie this leaves, thedeleted_attiebreak decides, which is what it is there for. first_seenis clamped toclamp_first_seen– after the tombstone rule, so it settles against the finallast_seen.
All three rules are vectorized fills over the packed columns rather than
per-row branches, and every column swap below is zero-copy. The closing cast is what
makes the result align with JOURNAL_SCHEMA – including its NOT
NULL columns, so a statement missing one is rejected here rather than by a
reader later.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
statements
|
Iterable[LakehouseStatement]
|
Statements, typically a whole drained
|
required |
now
|
datetime
|
Default timestamp for missing |
required |
Returns:
| Type | Description |
|---|---|
Table
|
A table with exactly |
Source code in ftm_lakehouse/model/statement.py
ftm_lakehouse.model.statement.journal_table(metadata, name)
Physical journal table named name, mirroring JOURNAL_SCHEMA.
The journal buffers exactly the rows producers pack, so its DDL is
derived from the same pyarrow schema – a journal row needs no packing to
become a statement row, and a segment can be streamed straight into
ParquetStore.append
as Arrow, which appends the derived shard partition key.
No primary key, no unique constraint, no index: the journal is an
append-only heap – but the schema's own NOT NULL columns
(REQUIRED_COLUMNS) still hold, so a row that could not be read
back never lands. Re-emissions accumulate as extra rows and
ParquetStore.merge
collapses them, which is where dedup lives anyway – and without a key, row
identity (origin, id, fragment, role) survives the journal instead of
collapsing to (id, fragment).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metadata
|
MetaData
|
The |
required |
name
|
str
|
Table name – the live journal or one of its segments. |
required |
Returns:
| Type | Description |
|---|---|
Table
|
The SQLAlchemy |
Source code in ftm_lakehouse/model/statement.py
Job Models
ftm_lakehouse.model.JobModel
Bases: BaseModel
Status model for a (probably long running) job
Source code in ftm_lakehouse/model/job.py
ensure_run_id(value=None)
classmethod
ftm_lakehouse.model.DatasetJobModel
Bases: JobModel
Status model for a (probably long running) job bound to a dataset