The mid-silver seam & supplemental loads
MidSilverNotebook sits at a precise point in every history-enabled silver
load: after Silver.his is written for this run, and before Silver.dbo is
rebuilt from it. That seam is what makes it useful for backfilling rows a
normal bronze load can't reach — rows added to Silver.his inside the hook
are picked up automatically when the rebuild runs immediately afterwards.
This page covers the seam itself, and load_data_silver.supplement_history,
the API built for feeding it.
The seam, precisely
Every history-enabled silver load (keephistory: true, full or merge)
follows the same two steps, in order:
- Write this run's changes into
Silver.his. - Rebuild
Silver.dboby reading all ofSilver.his, keeping one row per primary key: the row with the highestabs(SYSTEMSTATETIMESTAMP)(ties broken bySYSTEMSTATETIMESTAMP desc, so a positive timestamp beats a negative one at the same magnitude), and only if that timestamp is> 0. A key whose winning row is negative — i.e. deleted — is dropped fromSilver.dboentirely. See History in Fabric Lakehouse for the full mechanics of that column.
MidSilverNotebook runs between those two steps. Anything the hook appends
to Silver.his — including rows it added itself — is already there when step
2 reads "all of Silver.his", so it flows into Silver.dbo on the same run,
governed by the same abs(SYSTEMSTATETIMESTAMP) desc / > 0 window as every
other row. There is no separate hand-off; the hook doesn't write to
Silver.dbo itself, it just has to land its rows in Silver.his before the
rebuild reads it.
This is what distinguishes the mid-silver slot from its neighbours:
PreSilverNotebookruns beforeSilver.hisis touched at all — it can't hand the load extra rows to merge in, only prepare things ahead of it.PostSilverNotebookruns afterSilver.dbohas already been rebuilt — anything it writes toSilver.hiswaits for the next load to appear inSilver.dbo.MidSilverNotebookis the only slot that sits inside the seam between the two.
The supplemental load API
def supplement_history(
data_frame: DataFrame,
table_config: TableConfig,
config_manager: ConfigManager = None,
) -> str | None
load_data_silver.supplement_history is the entry point a mid-silver
notebook uses to hand EasyFabric rows that are already in silver shape and
destined straight for Silver.his, without routing them through bronze.
table_config is always the target table's own config — the one whose
Silver.his receives the rows; there is no separate config for the
supplemental source.
Anti-join contract and its grain. Before appending, supplement_history
anti-joins data_frame against the existing Silver.his on the pair
(SYSTEMPRIMARYKEY, SYSTEMSTATETIMESTAMP) — the same primary-key
grain the rest of his history is written at, not just the primary key
alone. A row is skipped only when both the key and the exact timestamp are
already present; the same key at a different timestamp is a new version and
gets appended. That grain is what makes repeat calls with the same
data_frame idempotent — re-running the notebook against an unchanged
archive appends nothing the second time.
Schema requirements.
data_framemust already be in silver shape.silverexpression/ converted-column transforms are not applied — those describe how the target's own bronze columns become silver columns, and this DataFrame never touches bronze.- It must contain the target's primary-key columns
(
table_config.get_keycolumn_array()— there is no per-call override). SYSTEMSTATETIMESTAMPis mandatory. Supplemental rows carry their own historical effective time — the archive's own recorded date — and the framework will not invent one for them, because a supplemental source can itself hold history. Adata_framewithout the column is a hard failure.SYSTEMSOURCETAGis optional; it defaults to"Not applicable"when absent.data_frameneed not carry everySilver.hiscolumn, the same as a bronze source file. Key columns are mandatory; any other columnSilver.hishas thatdata_framelacks is logged as a column-mismatch warning and filled with NULL — unless it is non-nullable inSilver.his, which is a hard failure, since there is no value to fill it with. Extra columnsdata_framecarries that aren't part ofSilver.hisare dropped — only the target's own schema is written.
What the framework validates, all raising before anything is written:
- The target has
keephistory: true—supplement_historywrites intoSilver.his, and a table without history has none. - The target has primary-key columns configured.
data_framecontains every one of those primary-key columns.data_framehas no duplicate rows for the same key at the sameabs(SYSTEMSTATETIMESTAMP)— that combination must be unique within the batch you supply, the same constraintSilver.hisitself upholds.data_framesuppliesSYSTEMSTATETIMESTAMP.data_frameisn't missing any non-nullable column ofSilver.his.
An empty data_frame is not an error — the call logs that there's nothing to
append and returns without writing.
What appears in the run log: a start line naming the target table, a row
count line for what was appended (or that everything was already present and
skipped, keeping the call visibly idempotent), and — on failure — an error
line naming supplement_history as the failing loader and the target table.
Only Silver.his is written. supplement_history itself never touches
Silver.dbo — that's why calling it from MidSilverNotebook matters: it's
what puts the new rows in Silver.his far enough ahead of the seam's rebuild
step to be picked up automatically, on the same run.
A reference notebook
The motivating case: rows recovered from another system, sitting in an
archive table such as Bronze.his.<table>_archive, that never went through
this table's own bronze ingest and need to land in Silver.his anyway.
Configured as the table's MidSilverNotebook:
from easyfabric import load_data_silver, load_meta_data
obj = load_meta_data.get_object_by_file(object_yaml_file)
df_archive = spark.table("Bronze.his.customers_archive").select(
"CustomerID", "Name", "Email", "SignupDate", "SYSTEMSTATETIMESTAMP"
)
load_data_silver.supplement_history(df_archive, obj)
No manual "which rows are missing" step is needed — supplement_history's
own anti-join already limits the append to rows not already in Silver.his,
so the notebook only has to shape the archive into silver columns plus the
target's primary key and the archive's own SYSTEMSTATETIMESTAMP, which it
must supply. Because the hook runs inside the seam, those rows are
in Silver.dbo by the time this same load finishes — no separate rebuild
step to remember.
When not to use it
supplement_history bypasses bronze entirely: the rows it appends never
land in Bronze.his, are never covered by bronze's own schema validation or
business-key checks, and never go through silverexpression type
conversion. That's the right trade-off for a one-off backfill from a source
that no longer exists to be re-ingested from. It's the wrong tool if the
rows belong in the table's regular delivery — if the source can still
produce them, fix the bronze ingest instead (extend the source query, add a
connection, adjust a filter) so they flow through the normal
Bronze.dbo → Bronze.his → Silver.his → Silver.dbo pipelineAn automated "conveyor belt" that moves data from one place to another or performs a task automatically. with the
rest of the table's data: validated, converted, and reconciled the same way
on every run, instead of depending on a notebook someone has to remember to
re-run.