Observation
On a filesystem destination, a table loaded from arrow data does not appear in the load package's schema_update (the per-package schema migration recorded in the trace / load info). The same load on a SQL destination (e.g. duckdb) reports it correctly. This makes the trace under-report which tables/columns were created or altered by a load.
This surfaced for us via dlthub materialized transformations (materialization emits arrow, and delta forces materialization), but it reproduces with a plain pipeline.run(<pyarrow table>) — no transformations, and independent of table_format (plain parquet and delta both hit it).
Repro
import os, shutil, dlt, pyarrow as pa
def schema_update_tables(pipeline):
for step in pipeline.last_trace.steps:
if step.step != "load":
continue
for pkg in step.step_info.load_packages:
return list(pkg.schema_update.keys())
data = pa.table({"id": [1, 2], "name": ["a", "b"]})
# filesystem — user table 'things' is MISSING
W = "/tmp/tkt_fs"; shutil.rmtree(W, ignore_errors=True); os.makedirs(W); os.chdir(W)
fs = dlt.pipeline("p", destination=dlt.destinations.filesystem(bucket_url=f"file://{W}/lake"), dataset_name="d")
fs.run(data, table_name="things")
print("filesystem:", schema_update_tables(fs))
# duckdb — user table 'things' is present
W2 = "/tmp/tkt_duck"; shutil.rmtree(W2, ignore_errors=True); os.makedirs(W2); os.chdir(W2)
dk = dlt.pipeline("p", destination="duckdb", dataset_name="d")
dk.run(data, table_name="things")
print("duckdb :", schema_update_tables(dk))
Output:
filesystem: ['_dlt_pipeline_state']
duckdb : ['_dlt_pipeline_state', '_dlt_loads', '_dlt_version', 'things']
The behavior is deterministic (not intermittent) and also affects schema evolution: a subsequent arrow load that adds a column to things produces an empty schema_update (tables=[]) on filesystem, while duckdb records the added column. Loading the same data as dict/JSON rows (instead of arrow) reports correctly on filesystem too.
Versions: dlt==1.27.0, deltalake==0.25.4, Python 3.12.11.
What is expected in the trace
Each load package carries a schema_update (serialized as tables in the load-package info / trace step info). It is meant to describe the schema migration that this load applied at the destination — the set of tables and columns that were newly created or added by this specific load. Consumers rely on it to answer "what changed at the destination in this run" (e.g. surfacing per-run schema/DDL migrations in observability tooling). For a freshly created data table with a load job, that table (with its columns) should be present in schema_update; for an evolved table, the added columns should be present — regardless of whether the data arrived as arrow or JSON, and regardless of destination type.
Why it isn't working (brief)
The load-package schema_update is derived from the diff the normalize step produces. For arrow input, the column schema is resolved at extract time (from the arrow schema), so normalize sees no change and records an empty diff. SQL destinations recompute the actually-applied update at load time in update_stored_schema (via _execute_schema_update_sql, diffing the schema against the destination's stored state), so they report the table anyway. The filesystem client's update_stored_schema instead returns the (empty) expected_update verbatim, so the table is never reported.
Observation
On a filesystem destination, a table loaded from arrow data does not appear in the load package's
schema_update(the per-package schema migration recorded in the trace / load info). The same load on a SQL destination (e.g. duckdb) reports it correctly. This makes the trace under-report which tables/columns were created or altered by a load.This surfaced for us via
dlthubmaterialized transformations (materialization emits arrow, and delta forces materialization), but it reproduces with a plainpipeline.run(<pyarrow table>)— no transformations, and independent oftable_format(plain parquet and delta both hit it).Repro
Output:
The behavior is deterministic (not intermittent) and also affects schema evolution: a subsequent arrow load that adds a column to
thingsproduces an emptyschema_update(tables=[]) on filesystem, while duckdb records the added column. Loading the same data as dict/JSON rows (instead of arrow) reports correctly on filesystem too.Versions:
dlt==1.27.0,deltalake==0.25.4, Python 3.12.11.What is expected in the trace
Each load package carries a
schema_update(serialized astablesin the load-package info / trace step info). It is meant to describe the schema migration that this load applied at the destination — the set of tables and columns that were newly created or added by this specific load. Consumers rely on it to answer "what changed at the destination in this run" (e.g. surfacing per-run schema/DDL migrations in observability tooling). For a freshly created data table with a load job, that table (with its columns) should be present inschema_update; for an evolved table, the added columns should be present — regardless of whether the data arrived as arrow or JSON, and regardless of destination type.Why it isn't working (brief)
The load-package
schema_updateis derived from the diff the normalize step produces. For arrow input, the column schema is resolved at extract time (from the arrow schema), so normalize sees no change and records an empty diff. SQL destinations recompute the actually-applied update at load time inupdate_stored_schema(via_execute_schema_update_sql, diffing the schema against the destination's stored state), so they report the table anyway. The filesystem client'supdate_stored_schemainstead returns the (empty)expected_updateverbatim, so the table is never reported.