This chapter is a practical contributor guide. It turns the
architecture from the previous chapters into checklists for common
changes.
The point is not to replace code review. The point is to help a
contributor avoid the classic Sail mistake: implementing one layer of a
feature and forgetting the other four.
19.1 Adding A Spark Function
Start by classifying the function.
Kind
Typical location
Scalar expression that maps to DataFusion
crates/sail-plan/src/function/scalar/
Scalar function requiring Spark-specific runtime behavior
crates/sail-function/src/scalar/
Aggregate function
crates/sail-function/src/aggregate/ and function
registry
Window function
crates/sail-function/src/window/ and function
registry
Table/generator function
crates/sail-plan/src/function/table/ and resolver
paths
Then follow the lifecycle:
Confirm Spark syntax and behavior.
Decide whether the function can be expressed as a DataFusion
Expr.
If yes, register it through the ScalarFunctionBuilder
closure model.
If no, implement the appropriate DataFusion UDF/UDAF/window
function.
Add type coercion and return-type logic.
Add SQL/gold tests.
Add DataFrame/PySpark tests if the API path differs.
Verify distributed behavior if aggregate state or custom UDF
encoding is involved.
Prefer expression-level registration when possible. It keeps the
function visible to DataFusion optimization.
19.2 Adding A Python UDF Path
Python UDF work crosses a wide boundary:
Spark Connect function payload
-> Sail spec
-> resolver UDF object
-> Python serialization/deserialization
-> Arrow/Python conversion
-> local or worker execution
-> codec support if distributed
Before editing, identify which UDF family you are touching:
scalar row-by-row,
Arrow scalar,
Pandas scalar,
grouped aggregate,
grouped map,
co-grouped map,
iterator UDF,
UDTF.
Then check:
Does Spark Connect carry enough metadata?
Does the spec type preserve it?
Does resolver logic construct the right UDF object?
Does sail-python-udf know how to call it?
Does the codec preserve it for workers?
Are Python exceptions mapped back to Spark-friendly errors?
19.3 Adding A Catalog Backend
Catalogs answer questions about names, databases, tables, and
views.
Add or update configuration in Sail’s config model.
Implement CatalogProvider.
Register the provider in
crates/sail-session/src/catalog.rs.
Decide whether it needs runtime-aware wrapping for I/O runtime
use.
Decide whether catalog caching applies.
Map backend errors to CatalogError.
Return TableStatus with accurate format, schema,
location, and properties.
Test create/list/get/drop behavior.
Test table scans through the DataFusion bridge.
Do not stop at metadata tests. A catalog backend is only useful if
its TableStatus leads to the right table provider.
19.4 Adding A Table Format
Table formats translate table metadata into read and write
behavior.
Implement or extend the TableFormat contract.
Register it in crates/sail-session/src/formats.rs.
Define source and sink option resolution.
Implement scan creation.
Implement write planning if supported.
Add object-store handling if needed.
Add projection/filter tests.
Add write-mode tests.
Add catalog integration tests.
For lakehouse formats, also ask:
Does this format support row-level operations?
Does it need logical expansion rules?
Does it need an extension physical planner?
Does it need metadata columns for merge/delete?
19.5 Adding A Logical Plan
Node
Use this path when Spark has a logical concept DataFusion does not
represent natively.
Add the spec representation if needed.
Add resolver logic that creates a
LogicalPlan::Extension.
Implement UserDefinedLogicalNodeCore.
Preserve schema and expressions correctly.
Implement projection-pushdown hints if relevant.
Add the physical ExecutionPlan.
Register downcast planning in
ExtensionPhysicalPlanner.
Add optimizer rules only if planning alone is insufficient.
Add codec support for cluster mode if the physical node can reach
workers.
The physical planner registration belongs in
sail-session, not sail-plan.
19.6 Adding A Physical Optimizer
Rule
Physical optimizer rules rewrite executable plans. They should be
used when:
DataFusion’s physical plan is valid but not Spark-compatible,
a Sail placeholder exec must be lowered,
a partitioning/distribution contract must be enforced,
cluster execution needs a safer physical shape.
Checklist:
Write down the exact physical invariant.
Add a focused rule implementation.
Insert it in get_physical_optimizers at the correct
point.
Add tests for before/after plan shape.
Confirm the rule does not fight DataFusion’s own rules.
Test local and cluster execution when distribution changes.
19.7 Adding A Streaming Source
Streaming sources implement StreamSource.
Define source options and schema.
Implement data_schema.
Implement scan to return an
ExecutionPlan.
Ensure execution emits flow-event batches.
Register source discovery/read resolution.
Test projection behavior.
Test query lifecycle: start, status, stop, await.
Test interactions with filters, limits, and sinks.
The source should not return ordinary user-schema batches from
physical execution. It must produce the flow-event schema expected by
streaming physical operators.
19.8 Adding Distributed Codec
Support
If a feature creates a physical object that workers need to execute,
the remote execution codec has to know about it.
Ask:
Does the object appear inside an ExecutionPlan sent to
workers?
Does DataFusion’s protobuf codec already support it?
If not, does Sail’s codec need a custom representation?
Does the worker session have all function/table-format registrations
required to decode or re-resolve it?
Are version mismatches possible?
Codec work is often the difference between “works locally” and “works
in Sail.”
19.9 Debugging A Compatibility
Bug
Use this order:
Reproduce with a minimal SQL query if possible.
Compare with Spark output.
Inspect the Spark Connect or SQL entry path.
Inspect the Sail spec.
Inspect initial and final logical plans.
Inspect the physical plan.
Run in local mode.
Run in cluster mode if the feature is distributed-sensitive.
Add the smallest regression test that would have caught it.
19.10 Takeaways
Sail features are pipelines. A complete contribution usually needs
protocol/spec, resolver, DataFusion planning, execution, tests, and
sometimes distributed codec support. The playbook is there to keep those
layers visible.