Portable Customer Transformation¶
Status: Available in ETLantic 0.15 (Polars + PySpark + Pandas + SQL relational)
@Transformation.portable authoring shipped in 0.11. This guide runs a
kernel or relational plan on Polars/PySpark/Pandas without a native
@implementation(...) for the advertised claim set. Safe SQL portable
lowering for that claim set also shipped in 0.15 (etlantic-sql).
Runnable companion:
examples/portable_polars_kernel.py.
Author once¶
from etlantic import (
Data,
Input,
Output,
Parameter,
Pipeline,
PipelineRuntime,
Profile,
Load,
Extract,
Transformation,
)
from etlantic.plan import explain_plan, plan_pipeline
from etlantic.registry import PlanningContext
from etlantic.transform import functions as F
from etlantic_polars import create_plugin
class RawCustomer(Data):
customer_id: int
email: str
age: int
class Customer(Data):
customer_id: int
email: str
age: int
class NormalizeCustomers(Transformation):
customers: Input[RawCustomer]
minimum_age: Parameter[int] = 18
result: Output[Customer]
@NormalizeCustomers.portable
def normalize(customers, minimum_age):
return (
customers.filter(F.col("age") >= minimum_age)
.withColumn("email", F.lower(F.col("email")))
.select("customer_id", "email", "age")
)
class PortablePolarsPipeline(Pipeline):
raw: Extract[RawCustomer] = Extract(asset="customers")
normalized = NormalizeCustomers.step(customers=raw)
curated: Load[Customer] = Load(input=normalized.result, asset="curated")
Inspect the emitted plan (no engine required):
plan = NormalizeCustomers.to_transform_plan()
assert plan["planIdentity"] == "dtcs.transform-plan/2"
print(NormalizeCustomers.portable_fingerprint())
Run on Polars without a native callable¶
profile = Profile(
name="polars-portable",
dataframe_engine="polars",
portable_transform_policy="require", # fail closed if unsupported
)
runtime = PipelineRuntime()
runtime.register_dataframe_plugin("polars", create_plugin())
runtime.memory.seed(
"customers",
[
RawCustomer(customer_id=1, email="A@X.COM", age=30),
RawCustomer(customer_id=2, email="b@y.com", age=10),
],
)
context = PlanningContext.create(profile=profile, registry=runtime.registry)
planned = plan_pipeline(PortablePolarsPipeline, context=context)
assert planned.implementations["normalized"].kind == "portable_compiled"
report = PortablePolarsPipeline.run(
profile=profile, runtime=runtime, context=context
)
print(runtime.memory.get("curated"))
Default policy is prefer (portable when covered; diagnosed native fallback
otherwise). Use native to ignore compilers.
Expected plan evidence¶
{
"node": "normalized",
"implementation_kind": "portable_compiled",
"compiler": {"name": "etlantic-polars"},
"ir_fingerprint": "<64-char sha256>"
}
Use explain_plan(planned) or etlantic plan … --format json.
Expected outputs¶
| customer_id | age | |
|---|---|---|
| 1 | a@x.com | 30 |
Age 10 is filtered out. Email is lowercased.
Unsupported operations fail closed¶
Joins, windows, and conversion-profile functions such as dtcs:cast are
outside the 0.12 Polars kernel claim. With portable_transform_policy="require",
planning raises PipelineValidationError with PMXFORM301 (or PMXFORM302
when no compiler is discoverable).
What remains future¶
- Safe SQL portable lowering for kernel +
portable-relational/1(0.15 exit gate) - Advanced portable profile graduation (window, reshape, …) under the 0.15 continuation backlog after the SQL gate
Polars, PySpark, and Pandas relational compilers already ship in 0.13–0.14.
Keep @implementation("sql") until the portable SQL compiler ships; keep
native callables for profiles outside the advertised claim set.