feat!: assign extension anchors per plan, not per registry - #245
Draft
nielspardon wants to merge 1 commit into
Draft
feat!: assign extension anchors per plan, not per registry#245nielspardon wants to merge 1 commit into
nielspardon wants to merge 1 commit into
Conversation
`ExtensionRegistry` handed out `function_anchor` / `extension_urn_anchor`
values at registration time and builders stamped those registry-global
numbers into plans. But anchors are plan-local in Substrait, which caused
two problems:
- Plans were not reproducible. A single-`add` plan emitted
`function_anchor: 284` against the default extension set and `4` against
a minimal one, because the value encoded how many functions the other
YAMLs defined and the order the `functions*.yaml` glob returned them
(filesystem order, not sorted).
- Extending a plan built elsewhere silently corrupted it. The merge
helpers dedupe by identity and document "assumes that there are no
collisions", with nothing enforcing it, so a foreign plan already using
a given anchor produced two URNs at one anchor and two functions at
another -- leaving `function_reference` ambiguous, with no error.
Introduce `ExtensionCollector`, which owns those anchors for the duration
of one build: function references are allocated on first use from 1, and
URN anchors are derived at emit time (nothing outside
`SimpleExtensionDeclaration` refers to one). It follows substrait-java's
`io.substrait.extension.ExtensionCollector`, including that numbering.
The collector reaches builders through a contextvar, as the builders'
other per-build state already does (`_rel_anchor_counter`,
`outer_schemas`, `anchor_scope`).
An incoming materialized plan has its declarations read back to
`(urn, name)` identities and its references re-derived rather than
trusted, so independently numbered inputs cannot disagree about what a
reference means. This is what the SQL translator needs, as it builds a set
operation's two sides as separate plans before merging them. Identities
come off the declaration rather than a catalog lookup, so a plan naming
functions absent from the registry still round-trips; declarations too
under-specified to re-derive (pyarrow emits a bare
`extension_function { name: "add" }`) are preserved verbatim.
Because the collector accumulates once per build, the per-level extension
merging in the builders is gone rather than optimized: an N-verb chain
scanned 230 declarations across 80 merge calls at N=40, and now does none.
This is the extension half of substrait-io#207; the schema re-inference half is
untouched.
`ExtensionRegistry` is now a pure catalog. `lookup_urn` and
`FunctionEntry.anchor` are deprecated (`has_urn` / `urns()` replace the
former); the urn->function mapping, signature matching and
extension-relation registration are unchanged.
BREAKING CHANGE: emitted extension anchors are now numbered per plan, so
plans compared byte-for-byte against output from an earlier release will
differ. Anchors are plan-local by spec, so plan semantics are unaffected.
`ExtensionRegistry.lookup_urn` and `FunctionEntry.anchor` now warn and no
longer return registry-global anchors.
Closes substrait-io#236
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Removes the extension-merging half of #207 as well (the schema re-inference half is
untouched).
Problem
ExtensionRegistryhands outfunction_anchor/extension_urn_anchorvalues atregistration time, and builders stamp those registry-global numbers into plans.
Anchors are plan-local in Substrait, so this caused two problems.
Plans were not reproducible. A single-
addplan emittedfunction_anchor: 284against the default extension set and
4against a minimal one — the value encodedhow many functions the other default YAMLs defined and the order the
functions*.yamlglob happened to return them (filesystem order, not sorted). Asubstrait-extensionsbump, or a different machine, shifted every anchor.Extending a plan built elsewhere silently corrupted it.
merge_extension_urns/merge_extension_declarationsdedupe by identity and their docstrings state"Assumes that there are no collisions", with nothing enforcing it. Given a plan
already using anchor 10/284 for different entities:
function_reference: 284is now ambiguous, and no error is raised.Approach
ExtensionCollectorowns those anchors for the duration of one build. Functionreferences are allocated on first use from 1; URN anchors are derived at emit time,
since nothing outside
SimpleExtensionDeclarationrefers to one. This followssubstrait-java's
io.substrait.extension.ExtensionCollector, including itsfirst-use numbering and its deferral of URN anchors.
The collector reaches builders through a contextvar, as the builders' other
per-build state already does (
_rel_anchor_counter,outer_schemas,anchor_scope). Three seams carry the change:build_scopedwrapping eachresolver, plus
_bind(plans) andresolve_expression(expressions) for adoptinginputs.
An incoming materialized plan has its declarations read back to
(urn, name)identities and its references re-derived rather than trusted, so two
independently numbered inputs cannot disagree about what a reference means. This is
load-bearing rather than defensive: the SQL translator builds a set operation's two
sides as separate plans before merging them, and both number from 1.
Two deliberate points of permissiveness:
functions absent from the registry still round-trips.
serialize_expressionsemits a bareextension_function { name: "add" }— noanchor, no URN, no
extension_urnsentry. Anchor 0 is not a reference anyexpression can name under the spec's 1-based numbering, so re-assigning it would
desynchronize the declaration from expressions that still say 0.
ExtensionRegistrybecomes a pure catalog. Its urn→function mapping, signaturematching and extension-relation registration are unchanged.
Result
Anchors are now dense and plan-local:
Byte-identical across a minimal vs. the full default registry, and across repeated
builds of the same frame.
Because the collector accumulates once per build, per-level extension merging is
gone rather than optimized. Measured against a
mainworktree over an N-verbprojectchain:main(merge calls / declarations scanned)Wall-clock build time is essentially unchanged (59 → 61 ms at N=40): schema
re-inference still dominates, which is #207's other half and out of scope here.
Examples were diffed against
main:pyarrow_exampleandduckdb_examplearebyte-identical;
builder_exampleanddataframe_examplediffer only in anchorvalues (89/284/479 → 1–4) with references rewritten consistently.
BREAKING CHANGE: extension anchors are now numbered per plan rather than per
registry, so plans compared byte-for-byte against output from an earlier release
will differ. Anchors are plan-local by spec, so plan semantics are unaffected.
ExtensionRegistry.lookup_urnandFunctionEntry.anchorare deprecated: both nowemit
DeprecationWarningand no longer return registry-global anchors. UseExtensionRegistry.has_urn()orExtensionRegistry.urns()to test URN membership,and identify a function by
(entry.urn, str(entry))instead of by anchor.Closes #236
🤖 Generated with AI