)]}'
{
  "commit": "e6c75870a2c465aae4787605f91a9dff47b0e911",
  "tree": "f42c93fd03251555615efa7b10b4a6a18f119ec5",
  "parents": [
    "2f528d2d53a550ba5434466e32af78b8c5bfac82"
  ],
  "author": {
    "name": "Tim Saucer",
    "email": "timsaucer@gmail.com",
    "time": "Fri Aug 28 16:21:27 2026 -0400"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Fri Aug 28 16:21:27 2026 -0400"
  },
  "message": "Add FFI query planner support (#1677)\n\n* Add FFI query planner support\n\nAI Disclosure: This code was written in part by an AI agent.:\n\n* Add three-library FFI planner example\n\nAI Disclosure: This code was written in part by an AI agent.:\n\n* Update FFI query planner integration\n\nAI Disclosure: This code was written in part by an AI agent.:\n\n* add rat\n\n* fix: install FFI test wheels from nested artifact paths\n\nThe FFI test wheel artifact now bundles two projects, so upload-artifact\npreserves a `\u003cproject\u003e/dist/` prefix instead of placing the wheels at the\nartifact root. The install step globbed `wheels/*.whl`, which no longer\nmatched them, so the FFI wheels were silently skipped and the FFI unit\ntests failed with `ModuleNotFoundError: No module named\n\u0027datafusion_ffi_example\u0027`.\n\nInstall the recursive `find` results instead of re-globbing.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* refactor: address review of FFI query planner support\n\nCollapse the two duplicated planner-install blocks into a single\n`ctx_with_rebound_planner`. A derived context shares the existing\n`SessionContext` when there is no foreign planner to rebind, and forks\nonly when one is installed, since the FFI codecs capture the context\nthey are built against.\n\nDocument what that fork shares. Catalogs, tables, and the runtime\nenvironment stay shared; registered functions, configuration, and the\noptimizer rule lists are snapshotted. The caveat lands on all four\nderivation methods and on a new contributor-guide subsection, with\ntests covering both halves.\n\nExplain why `RuntimeAwareQueryPlanner` exists at all. Upstream\u0027s\n`ForeignQueryPlanner` is the consumer-side adapter that lets an\n`FFI_QueryPlanner` satisfy the `QueryPlanner` trait, which is what makes\na planner from another shared library installable in a `SessionState`.\nIts trait method receives only a `\u0026LogicalPlan` and a `\u0026dyn Session`, so\nit has nowhere to obtain a runtime handle and passes `None`.\n\nThroughout datafusion-ffi each library attaches its own runtime to the\nobjects it exports, so a producer-side wrapper can enter that runtime\nbefore running its own library\u0027s code. A provider owned by another\nlibrary keeps its owner\u0027s runtime even when it travels through our\ncatalog, because `FFI_TableProvider::new_with_ffi_codec` unwraps a\n`ForeignTableProvider` back to the original handle and discards the\nruntime passed alongside it. `session_runtime` is that same rule applied\nto the session: `FFI_SessionRef` is our object and every callback on it\nruns our code.\n\nIt matters for what those callbacks hand back. A plan produced by our\nown planner returns as `FFI_ExecutionPlan::new(plan, runtime)`, and\n`execute` enters that runtime before calling into the plan; the same\nholds for our physical optimizer rules and for tables we own rather than\nre-export. The delegation case this type exists for is exactly that\nshape. A foreign planner falling back to our planner through\n`__datafusion_query_planner__` receives a plan whose execution needs our\nruntime, and datafusion-python owns that runtime as a process global\nwhile the Python thread calling in carries no ambient one.\n\nThe same reasoning is why `__datafusion_query_planner__` re-exports\nthrough the adapter rather than unwrapping to the inner handle. A\nconsumer reaching us through `ForeignQueryPlanner` calls with `None`, so\nthe adapter is what restores our handle on the way back out. Unwrapping\nwould save a planning-time round trip and silently drop it.\n\nIn the planner example, match the two real spellings of the row-limit\nconfig key exactly instead of by suffix, and validate after both lookup\npaths so the fallback cannot accept `max_rows \u003d 0`. The key appears\ntwice because rebuilding a `ConfigOptions` across the FFI boundary\nparks every foreign extension inside a single `FFI_ExtensionOptions`,\nitself namespaced under `datafusion_ffi`.\n\nAlso declare `requires-python \u003d \"\u003e\u003d3.10\"` on the provider example to\nmatch the `abi3-py310` feature it builds against, and link both example\nREADMEs to the contributor guide rather than restating its caveats.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* refactor: drop the runtime adapter and fix exported capsule lifetimes\n\nRemove `RuntimeAwareQueryPlanner`. It existed to re-attach our Tokio\nhandle to the session we hand to a foreign planner, on the reasoning that\n`ForeignQueryPlanner` passes `session_runtime: None`. That handle turns\nout to have no reachable path: the query planner FFI exchanges serialized\nbytes rather than plan handles, a provider owned by another library keeps\nits own runtime because `FFI_TableProvider::new_with_ffi_codec` unwraps a\n`ForeignTableProvider` back to the original handle, and we execute on our\nown runtime regardless. Setting the handle to `None` left every test\npassing. Codec rebinding now downcasts upstream\u0027s `ForeignQueryPlanner`\ndirectly, which also stops `__datafusion_query_planner__` adding a second\nlayer, since `new_with_ffi_codecs` already unwraps that type. The\n`datafusion-session` dependency is no longer needed in crates/core.\n\nKeep the exporting session alive for codecs handed out in a PyCapsule.\n`FFI_TaskContextProvider` stores its provider in a `Weak`, so a capsule\nstopped working as soon as the `SessionContext` that produced it went out\nof scope. That made the natural spelling of the documented fallback\npattern fail:\n\n    fallback \u003d ctx.__datafusion_query_planner__()\n    ctx \u003d ctx.with_query_planner(MyPlanner(fallback\u003dfallback))\n\nRebinding `ctx` dropped the exporter and planning then failed with\n\"TaskContextProvider went out of scope over FFI boundary\". Both Python\ncodecs gained an opt-in `exported_session`, set only by the three capsule\ngetters. The keep-alive lives in the inner codec because the consumer\nclones the FFI handle out of the capsule and `clone` clones the inner\ncodec\u0027s `Arc`, so a capsule-scoped keep-alive would die too early. It is\ndeliberately opt-in: the same codecs are also attached to providers and\ncatalogs that end up back inside the session, where a strong reference\nwould close a `SessionContext -\u003e SessionState -\u003e query planner -\u003e FFI\ncodec` cycle. Both structs now implement `Debug` by hand, because\n`SessionContext` is not `Debug`.\n\nAdd two example tests. One drives a plan containing `RepartitionExec`,\nwhich spawns Tokio tasks as it runs, through all three libraries, so the\ncodecs are exercised on a multi-node plan rather than a bare scan. The\nother layers a planner on top of the session\u0027s existing planner using the\ncapsule captured beforehand, which is the delegation pattern upstream\nprescribes; `Session::create_physical_plan` cannot be used for this,\nbecause it dispatches through the installed planner and recurses.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* add override for datafusion version to pre-release testing of upstream fix\n\n* remove unintentionally committed files\n\n* Empty commit to trigger CI\n\n* fix: keep the example planner\u0027s exported task context alive\n\n`FFI_TaskContextProvider` downgrades the provider it is given to a\n`Weak`, so building one inline in `__datafusion_query_planner__` left the\ncapsule carrying a provider that was already dropped by the time it\nreturned. Every codec callback through that capsule would have failed\nwith \"TaskContextProvider went out of scope over FFI boundary\". The\nexample did not notice because it ships the default codecs and no custom\nextension nodes, so `try_decode` is never reached.\n\n`MyQueryPlanner` now owns the context and hands out clones of it. The\n`QueryPlanner` the capsule carries holds a reference too, so the capsule\nstays usable even when the Python object that exported it is dropped\nfirst.\n\nDocument the distinction the inline construction obscured. The\n`TaskContextProvider` supplied at export time backs the exporting\nlibrary\u0027s own codec callbacks, decoding that library\u0027s nodes in its own\nregistry. It is unrelated to the `\u0026dyn Session` that later arrives at\n`create_physical_plan`, which belongs to the host, and it could not be\nderived from that session in any case, since the codecs are built before\nany session exists.\n\nRename `PlannerConfig` to `MyPlannerConfig` to match `MyQueryPlanner`.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* test: cover which session a foreign codec decodes against\n\nThe example codecs restore objects from a process-local token registry and\nnever read the `TaskContext` their FFI decode callbacks are handed, so\nwhich session that context belongs to was untestable. The token path\nignores the registry entirely, which is why an empty\n`SessionContext::new()` has served as the exported provider without\nanyone noticing.\n\nBoth codecs now accept `require_udf_on_decode`. When set, every decode\ncall resolves that scalar function out of the task context it was given\nand fails with the session id if it is absent, which makes the answer\nobservable. Each codec registers a marker function on the context it\nexports, so a name owned by the codec\u0027s library and a name owned by the\nhost can be told apart.\n\nFour tests use it. The two library-local cases pass: a foreign codec\nresolves against the session its own library supplied. The two\nhost-registered cases are `xfail(strict\u003dTrue)`, because a function\nregistered on the host with `register_udf` is not visible to a foreign\ncodec\u0027s decode callback at all. A fifth pins the current error so the\nfailure mode stays legible. Strict xfail means the pair will announce\nitself if the upstream design changes.\n\nDocument the rule this establishes, and correct the surrounding section:\n`with_query_planner` rebuilds a foreign planner against the session that\nwill run the query, so the provider a planner library supplies is\nreplaced on that path. Codecs installed through\n`with_logical_extension_codec` and `with_physical_extension_codec` keep\nthe provider their own library exported, which is the case these tests\nexercise.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* feat: pass the session to the capsule getters that need it\n\n`FFI_QueryPlanner::new` and `FFI_{Logical,Physical}ExtensionCodec::new`\nask an extension library for a `TaskContextProvider`, and a planner for\ntwo codecs on top of that. A library has none of those. Both examples\nanswered with `Arc::new(SessionContext::new())`, an empty session that\nresolves nothing, held weakly by `FFI_TaskContextProvider` and therefore\nalso a lifetime hazard.\n\nThe table provider protocol already solved this: the host calls\n`__datafusion_table_provider__(session)` and the library takes what it\nneeds off the session. Do the same for the other three getters.\n`__datafusion_query_planner__`, `__datafusion_logical_extension_codec__`,\nand `__datafusion_physical_extension_codec__` now receive the\n`SessionContext` they are being installed on. A codec takes the task\ncontext provider from it; a planner takes both codecs and uses\n`new_with_ffi_codecs`, which needs no provider at all. Neither example\nconstructs a `SessionContext` any more.\n\nDecode callbacks consequently resolve against the session running the\nquery. The two `xfail(strict\u003dTrue)` tests from the previous commit now\npass unmodified: a scalar function registered on the host with\n`register_udf` is visible inside a decode callback executing in another\nlibrary, for both the logical and physical codec. A negative control\nkeeps the check honest, and a further test covers a function registered\nafter the codec was installed, since the provider is a live handle rather\nthan a snapshot.\n\n`PySessionContext` gains an `ancestors` list. A foreign codec is built\nagainst the session current at the time it is installed and holds it\nweakly, so installing a foreign planner afterwards — which forks — would\nstrand the codec once the Python name is rebound. The keep-alive lives on\n`PySessionContext` rather than on the codec because nothing reachable\nfrom a `SessionContext` reaches a `PySessionContext`, so it cannot close\na cycle. What it does not paper over is the fork itself: a function\nregistered after the fork is not visible to a codec bound to the session\nbefore it, which is the existing derived-context caveat seen from the\ncodec\u0027s side, and is covered by a test.\n\n`SessionContext` accepts and ignores the argument on all three getters,\nso a session satisfies the same protocol a library implements and\n`ctx.__datafusion_query_planner__()` keeps working for the delegation\npattern. Calling a stale getter that takes no session now reports an\nincompatible-library error naming the method, matching what\n`table_provider_from_pycapsule` does.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: record the FFI capsule protocol as a convention\n\nThe session-passing rule was already settled for four getters and\ndocumented in the 52.0.0 upgrade guide, but nothing pointed an agent or a\nnew contributor at it before they wrote a fifth. Write it down where it\nwill be found.\n\nAdd the 55.0.0 upgrade guide entry this branch owes. Changing\n`__datafusion_logical_extension_codec__` and\n`__datafusion_physical_extension_codec__` to take a session breaks every\nextension library implementing them, so it needs before/after Rust in the\nsame shape as the 52.0.0 entry.\n\nCorrect `user-guide/io/table_provider.md`. It still showed the pre-52.0.0\nsignature with no session and a `PyCapsule::new_bound` call, so the one\npage a reader is most likely to find contradicted the convention.\n\nAdd `.ai/skills/ffi-capsule-protocol/`. Its description is written as a\ntrigger rather than a task, because the existing skills are all things to\nrun on request and a convention read as one would be skipped. It leads\nwith enumerating the family, which is the step that makes the rest\nunnecessary.\n\nPoint `CLAUDE.md` at it, since that file loads unconditionally and a skill\nonly helps once someone goes looking. Also note that `docs/temp/` is\ngitignored build output that `grep -r` surfaces with stale copies, and\nrequire an upgrade guide section alongside the `api change` label, so a\nbreaking change forces a visit to the file that records the conventions.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: put skill frontmatter before the license header\n\nEvery `.ai/skills/*/SKILL.md` opened with the ASF header and only then the\nYAML frontmatter, which has to be the first thing in the file. The result\nwas that no skill\u0027s `description` was readable: the skill listing showed\n`\u003c!---` for all of them, so the field meant to say when a skill applies\nsaid nothing. `skills/datafusion_python/SKILL.md` already had the right\norder and was the model to follow.\n\nMove the header below the frontmatter in all four. Apache RAT still\napproves each file — it looks for the license anywhere, not at the top —\nverified with rat 0.13.\n\nThis matters most for the new `ffi-capsule-protocol` skill, whose\ndescription is written as a trigger condition rather than a task name.\nThe existing skills are all tasks to run on request, so a convention that\nhas to be read *before* writing code is easy to filter out while skimming\nfor something to invoke. Note the distinction in the skills section of\n`AGENTS.md`.\n\nThen remove what that makes redundant. `AGENTS.md` had grown a copy of the\nskill\u0027s opening grep and a summary of its central rule. Two copies of one\nconvention, with the more discoverable copy free to drift, is exactly the\nfailure this branch already fixed in `user-guide/io/table_provider.md`.\n`AGENTS.md` now says only when to look and where; the skill owns the\nprocedure. The `docs/source` versus `docs/temp` note moves the other way,\nout of the skill and into `AGENTS.md`, where it applies to everything\nrather than to this one protocol.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* Update temporary DF version with corrections in FFI\n\n* feat: rebind foreign codecs when a planner install forks the session\n\nInstalling a foreign query planner writes to `SessionState`, and\n`with_query_planner` must not modify its receiver, so it forks. A foreign\ncodec holds an `FFI_TaskContextProvider` pointing at the session it was\ninstalled on, and until now the fork could not move it: passing a new\nprovider to `FFI_LogicalExtensionCodec::new` was silently discarded\nwhenever the codec was already foreign. The fork rebound only its own\nouter wrapper, so decode callbacks in the extension library kept\nanswering from the pre-fork registry, and the pre-fork session had to be\nretained or the weakly held provider dangled.\n\napache/datafusion#24722 fixes the discard; those constructors now adopt\nthe provider on the already-foreign path. Repoint the patch at the branch\ncarrying it and rebind both codecs onto the fork.\n\nVerified the branch carries everything already pinned rather than trusting\nthe commit graph, which reports the two as diverged: across 3811 files the\nonly differences are the four constructors from the fix, and\n`datafusion/ffi/src/session/mod.rs` is byte-identical, so the\n`create_physical_plan` codec fix arrives as its branch-55 backport.\n\n`ancestors` and its helpers are deleted. They existed only to keep the\npre-fork session alive for a codec that could not be moved off it, and a\ncodec bound to the running session needs no such anchor.\n\nThree tests, replacing two that were weaker than they looked. One\nregisters a function on the fork after the codec was installed on its\nparent and resolves it, which is the direct evidence the rebind happened;\nit failed before this change. One installs a planner twice and asserts the\nfirst context still cannot resolve a function registered only on the\nsecond, covering the clone-before-adopt half — a rebind that mutated the\nshared handle would pass the first test and fail this one. The third keeps\nthe live-handle case. The test it replaces required a name registered\nnowhere, so it passed for the same reason as the negative control and\nnever exercised a fork at all.\n\nNote the version floor in `Cargo.toml` rather than raising it now: the\npatched branch still reports 55.0.0, so the requirement can only move to\n55.1.0 when the patch section is removed. Building against 55.0.0 without\nthe patch would compile and silently skip the rebind.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: restore base64 0.23.1 in Cargo.lock\n\nRegenerating the lock against the patched DataFusion fork silently\ndowngraded base64 from 0.23.1 to 0.23.0. Nothing requires the older\nversion -- neither the fork nor upstream 55.0.0 constrains it -- so this\nwas incidental churn from the lockfile refresh, not a resolution result.\n\nRestores the checksum main already had and re-points the three\ndependents (datafusion-common, datafusion-functions, parquet). No other\ndependency moves; cargo metadata --locked still resolves cleanly.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: preserve session id across a planner fork\n\nInstalling a foreign query planner forks the session state, and the fork\nwas minting a new session id. SessionStateBuilder::new_from_existing\ndrops the id and build() replaces it with a fresh UUID, while\nSessionContext had already cached the original into a field of its own\nback at new_with_state. Overwriting the state in place afterwards left\nthe two disagreeing: session_id() returned the pre-fork id, every\nTaskContext handed to a foreign codec carried a different one.\n\nNothing in DataFusion core keys on the session id beyond debug logging,\nso this broke no in-tree behavior. It matters at the FFI boundary, where\nsession id equality is the idiom for \"which session is this codec bound\nto\", and for extension libraries correlating host-side and worker-side\nstate. Upstream hit the same case in SessionContext::enable_url_table\nand preserves the id explicitly, guarded by preserve_session_context_id.\n\nPassing the id through the builder makes the fork, its state, and its\nTaskContexts agree, which is what the derived_parts doc comment and the\nFFI contributor guide already claimed.\n\nVerified by reading the id out of a decode callback via the example\ncodec\u0027s require_udf_on_decode error path -- the only way to observe the\nstate-side id from Python -- with and without the fix.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: preserve session id in add_physical_optimizer_rule\n\nSame drift just fixed in derived_parts, but on a path that never forks.\nadd_physical_optimizer_rule rebuilds SessionState through\nSessionStateBuilder::new_from_existing and writes it straight back into\nthe caller\u0027s own session, so the fresh id build() mints replaces the one\nSessionContext had already cached at construction. The session the user\nis holding then reports one id from session_id() and a different one\nfrom every TaskContext it hands out, with no derivation to explain it.\n\nReproduced against a foreign codec, reading the id back out of a decode\ncallback: identical setup differing only by an add_physical_optimizer_rule\ncall went from MATCH to DRIFT, and back to MATCH with the id threaded\nthrough the builder.\n\nThis is the last new_from_existing call site in the crate.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* test: guard the session id a codec decodes against\n\nThe two session id fixes had no regression guard. A Python-level\nassertion cannot provide one: session_id() reads a copy SessionContext\ncaches at construction, which stayed correct through both bugs. The id\nthat actually moved was the one inside the TaskContext handed to a\nforeign codec\u0027s decode callback, which nothing exposed.\n\nGive the example codecs a TaskContextProbe that records it. This\nreplaces the bare AtomicUsize the require_udf_on_decode support used, so\nthe counter and the session id are recorded together, and the id is\nrecorded on every decode rather than only when a function was requested.\n\nThree tests, all against the codec-side id rather than session_id():\na fork agrees with its codecs, add_physical_optimizer_rule does not move\nthe id, and a two-deep fork chain leaves both halves on the parent\u0027s id.\n\nConfirmed non-vacuous: with both fixes reverted all three fail and the\nother 17 tests pass; with only the derived_parts fix restored, exactly\nthe add_physical_optimizer_rule test still fails.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: stop reporting any getter TypeError as an outdated library\n\ncall_capsule_getter rewrote every TypeError from a capsule getter into\n\"Incompatible libraries ... Upgrade the library providing this object\",\nand dropped the original. Only an arity mismatch means the library is\nout of date. An extension author whose own getter raised a TypeError --\na bad cast, a wrong argument to something it called -- was told the\nerror was a version problem and lost the error that would have located\nit.\n\nThe two are distinguishable without guessing at message text: an arity\nmismatch is raised by the call machinery before the getter\u0027s frame\nexists, so no frame unwinds and no traceback is attached, while an error\nfrom the body carries one. Verified to hold for both pure-Python and\npyo3-compiled getters, which is the case that matters here since\nextension libraries are compiled.\n\nAlso chains the original as __cause__ on the paths that do report an\nupgrade, so the arity error stays readable.\n\nTests cover all three outcomes. Confirmed non-vacuous: dropping the\ntraceback check fails only the inside-the-getter test, dropping\nset_cause fails only the upgrade test.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix!: remove physical_codec_from_pycapsule\n\nThe from_pycapsule! macros call the getter with no arguments. That is\ncorrect for __datafusion_physical_optimizer_rule__ and\n__datafusion_task_context_provider__, which take no session, but\n__datafusion_physical_extension_codec__ now takes the session it is\nbeing installed on, so this helper was the one member of the family left\nspeaking the old protocol.\n\nNothing in the tree called it, but datafusion-python-util is published\nby `cargo publish --workspace`, so it was still reachable. Against an\nupdated codec it raised a bare TypeError, bypassing the ImportError that\nnames the method. Against an outdated one it succeeded and produced a\ncodec resolving names against the wrong session -- the silent failure\nthe rest of this work exists to prevent.\n\nRemoving it is a breaking change to that crate, but the crate already\nbreaks this release: ffi_logical_codec_from_pycapsule gained its session\nparameter. A compile error pointing at the replacement beats a helper\nthat quietly binds to nothing.\n\nCallers move to ffi_physical_codec_from_pycapsule, which passes the\nsession, plus (\u0026ffi).into() where an Arc\u003cdyn PhysicalExtensionCodec\u003e is\nwanted -- what crates/core already does.\n\nDocuments both helper changes in the 55.0.0 upgrade guide, which until\nnow covered only the __datafusion_*__ method signatures and not the Rust\nhelpers the same authors call.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: check the FFI major version on every importer that can\n\nOnly ffi_query_planner_from_pycapsule validated the version a capsule\nreported. The codec and table provider importers dereference a foreign\nstruct through the same `unsafe { data.as_ref() }` and were happy to\naccept one built against a different DataFusion.\n\nExtracts the planner\u0027s inline check into check_ffi_version and applies\nit to the logical codec, physical codec, and table provider importers as\nwell. The helper is pub so extension libraries writing their own\nimporters can use it.\n\nTwo things the symmetry cannot reach, both now documented where someone\nwould look:\n\nFFI_TaskContextProvider, FFI_TableProviderFactory, and\nFFI_ExtensionOptions carry no version field, so their importers cannot\ncheck. The from_pycapsule!/try_from_pycapsule! macros are #[macro_export]\nand generic over the FFI type, so requiring a version field there would\nbreak downstream users holding one of those three; they stay unchecked\nand their doc comment now says to call check_ffi_version directly.\n\nThis is a diagnostic, not a soundness guarantee, and the helper says so:\n`version` is not the first field on any of these structs, so reading it\nalready assumes the local layout. It turns the realistic failure -- a\nlibrary compiled against a different DataFusion -- into a clear error\ninstead of undefined behaviour on first use, which is what\ndatafusion_ffi::version is documented to be for.\n\nVerified all four sites are wired by inverting the comparison and\nconfirming each one fires from the test suites.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: say where to relax the FFI version check\n\nExact equality is only right while datafusion_ffi::version tracks the\ncrate\u0027s semver major, which it does today, so the number moves on every\nmajor release whether or not the ABI changed. If a version span later\nbecomes compatible, a maintainer needs to know that this one body holds\nthe whole policy -- callers pass a value and no decision -- and that\nrelaxing it at a call site would reintroduce the split the helper was\nadded to remove.\n\nAlso records the likelier resolution: if the ABI is stable but version\nstill follows the crate major, upstream\u0027s compatibility marker is wrong\nfor every consumer, so the fix belongs there rather than in a local\nrange policy.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: route every capsule getter through call_capsule_getter\n\nThe table provider and table function importers each carried their own\ncopy of the TypeError-to-ImportError mapping, predating\ncall_capsule_getter and never folded into it. Both therefore missed the\ncorrection it since received: they rewrote a TypeError raised inside a\ncorrectly-signed getter into \"upgrade your library\", and discarded the\noriginal.\n\nThree copies of one mapping, two of them stale, is the reason to have\none. Both now call the shared helper, so they pick up the traceback\ndiscrimination and the __cause__ chain, and any later correction reaches\nall three by construction.\n\nTheir messages named DataFusion 52.0.0. The shared message names the\nmethod that refused the argument instead, which points at the specific\nhook rather than a release, and the upgrade guide carries the version\ndetail. call_capsule_getter is now pub, with a doc comment saying to use\nit rather than calling getattr directly.\n\nTests cover both outcomes on both paths. Verified against the previous\nbuild that they are non-vacuous: before this change the raises-inside\ncase produced the same misleading ImportError as the old-signature case.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: explain the dropped \u0026 in the codec migration snippet\n\nThe before and after snippets pass the task context provider\ndifferently, by reference in one and by value in the other, with nothing\nsaying why. Read as a diff it looks like a typo in one of them, and a\nreader correcting it would be puzzled when both versions compile.\n\nBoth are valid: the parameter is impl Into\u003cFFI_TaskContextProvider\u003e,\nwhich is satisfied by \u0026Arc\u003cdyn TaskContextProvider\u003e and by\nFFI_TaskContextProvider itself, and the latter is what\nffi_task_context_provider_from_pycapsule returns. The argument changes\nbecause the provider now comes from the session instead of a field,\nwhich is the point of the migration.\n\nThe contributor guide shows only the post-migration form, so it needs no\nequivalent note.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: spell out the token registry lifecycle in the example codecs\n\nThe README already describes these as one-shot registries that consume\neach token during decoding, but the source comments did not, and the\nsource is what someone reuses the pattern from. The existing comment\nwarned that the registry is process-local without saying that a decode\nremoves its entry, which is the constraint most likely to bite.\n\nDocuments both consequences on the registry accessors, where the\nmechanism lives, with a pointer from each struct doc:\n\n- Decode consumes the token, so the same encoded bytes cannot be decoded\n  twice. Fine here because every plan is encoded immediately before the\n  one decode that consumes it, but it rules out replaying a stored plan,\n  retrying a decode, or fanning one plan out to several readers.\n- An encode that never reaches a decoder leaks for the life of the\n  process. Normal operation does not: encode and decode counts balance\n  exactly across repeated queries, which is what makes remove-on-decode\n  the right trade here rather than a leak on every call.\n\nComments only.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: resolve a planner fallback when it is installed, not constructed\n\nMyQueryPlanner::new imported its fallback immediately, with no session\nto pass, so the fallback\u0027s getter was called with no arguments. That\nworks for a SessionContext, whose getter takes the session optionally,\nand for a raw capsule, which has no getter at all. It fails for another\nforeign planner, which implements the same protocol this type does and\nrequires the argument -- and layering on another planner is the case a\ndistributed engine actually needs. The docstring claimed fallback \"takes\nanything exporting __datafusion_query_planner__\", which was not true.\n\nHolds the Python object instead and imports it in\n__datafusion_query_planner__, where the session is in hand and can be\nforwarded. All three fallback kinds now work.\n\nDeferring also removes a footgun rather than adding one. Passing a\nSessionContext now delegates to whichever planner it holds at install\ntime, and since with_query_planner calls the getter before installing,\nthe context still reports its previous planner, so wrapping a context in\na planner installed on that same context does not recurse.\n\nArc\u003cPy\u003cPyAny\u003e\u003e rather than Py\u003cPyAny\u003e because pyo3 0.29 gates Py: Clone\nbehind the py-clone feature, and this type derives Clone. Matches how\nPythonTableFunctionCallable holds its callable.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: accumulate planner observations instead of overwriting them\n\nforeign_session, foreign_provider, and foreign_plan were written with\nstore, so each one described only the most recent plan. Their accessors\nare named foreign_*_observed, which asks whether the thing was ever\nseen, and the tests assert them after running more than one query. The\nexisting tests passed by luck.\n\nReproduced: after scanning a foreign provider and then running\nSELECT 1, foreign_provider_observed goes from True back to False.\n\nWrites them with fetch_or so a later plan cannot retract what an earlier\none observed. plan_calls already accumulated, used_fallback only ever\nstores true so it was already cumulative, and last_max_rows is\ndeliberately last-wins as its name says. Documents that split on the\nstruct, since it is the kind of thing that gets \"tidied\" back.\n\nConfirmed non-vacuous: with store restored, exactly the new test fails\nand the other 22 pass.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* Update rev for upstream datafusion to pre-release of 55.1.0\n\n* refactor!: install the query planner in place instead of forking\n\n`with_query_planner` derived a new `SessionContext` to install a planner,\non the grounds that the receiver must not be modified. That mints a fresh\n`Arc\u003cSessionContext\u003e` allocation, and every FFI handle in play is bound to\nan allocation rather than to the logical session: `FFI_TaskContextProvider`\nholds its provider weakly, and a registered catalog provider upgrades that\nhandle on every `supports_filters_pushdown` and every `scan`.\n\nSo the natural `ctx \u003d ctx.with_query_planner(planner)` dropped the session\na foreign catalog had been registered on, and the next query failed with\n`TaskContextProvider went out of scope over FFI boundary`. Reproduced with\na `MyCatalogProvider` registered before the install and a `WHERE` clause to\nforce pushdown during logical optimization.\n\nRebinding cannot cover this. It reaches the codecs `PySessionContext` holds\nin its own fields; a codec embedded in a registered `FFI_CatalogProvider` —\nand in every `FFI_SchemaProvider` and `FFI_TableProvider` minted from it —\nhas no Python-side handle. Nor can a codec retain the session that built\nit: codecs are routinely handed to a provider that is registered straight\nback into that session, closing `SessionContext -\u003e catalog -\u003e FFI provider\n-\u003e FFI codec -\u003e SessionContext`.\n\nInstall in place instead, writing `SessionState` back through `state_ref()`\nexactly as `add_physical_optimizer_rule` already did. A session keeps one\n`Arc\u003cSessionContext\u003e` for life, so no handle is ever orphaned and the bug\ncannot occur. This deletes the fork and everything that existed to repair\nit: `ancestors`, `rebound_{logical,physical}_codec`, `exported_session` on\nboth codecs, and the `exported_ffi_*` builders.\n\nThe query planner lives in `SessionState`, so it belongs to the session\nrather than to a handle on it. `with_query_planner(planner) -\u003e\nSessionContext` therefore becomes `set_query_planner(planner) -\u003e None`,\nmatching `add_physical_optimizer_rule`.\n\nThe 55.1.0 pin may no longer be needed — its stated reason in Cargo.toml is\nthe rebinding this removes — but that is left alone pending a check of the\nrest of the PR.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* test: install codecs in the provider-survival test\n\nThe test registered a foreign catalog provider and no codecs, so it could\nnever finish: once the planner install stopped orphaning the provider, the\nquery got past filter pushdown and then failed at plan serialization with\n`LogicalExtensionCodec is not provided`. That is the same unrelated failure\n`test_query_planner_requires_provider_codec` already covers, and it would\nmask a dangling handle rather than expose one.\n\nInstall both provider codecs, and fold the codec-install-after-planner case\nin as a parameter rather than a near-duplicate test. Both orderings write\n`SessionState` — one installs the planner, the other rebuilds it against a\nnew codec — so both exercise the path that must not replace the session\u0027s\n`Arc\u003cSessionContext\u003e`.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: keep the UDF inlining setting when installing a codec\n\n`with_logical_extension_codec` and `with_physical_extension_codec` built\nthe replacement wrapper with `Python{Logical,Physical}Codec::new`, whose\nconstructor defaults `python_udf_inlining` to true. Installing a codec on\na context that had opted out therefore turned inlining back on without\nsaying so:\n\n    ctx \u003d SessionContext().with_python_udf_inlining(enabled\u003dFalse)\n    ctx \u003d ctx.with_logical_extension_codec(codec)   # inlining silently back on\n\nThat matters beyond a stale flag. Inlining is what embeds a cloudpickled\ncallable in the wire format, and it is opt-out precisely because that is\nnot portable across interpreters and not something every deployment wants\nto ship. A codec install is not a request to change it.\n\nCarry the receiver\u0027s setting across instead. Both new tests fail on the\nprior build with `DFPYUDF` reappearing in the blob, and the paired\n`..._preserves_inlining_when_enabled` case pins the default-on direction so\nthe fix cannot degenerate into hard-coding it off.\n\nThe physical case is covered in `test_plans.py` rather than alongside the\nlogical one: `Expr.to_bytes` only routes through the logical codec, so an\nassertion there would pass with the physical bug still present. It takes an\n`ExecutionPlan` to observe.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: route the last seven capsule getters through call_capsule_getter\n\n`call_capsule_getter` claimed every capsule getter went through it, so the\nmapping from a refused argument to a diagnosable error would live in one\nplace. Seven sites still called `getattr(...).call0()` or `.call1(...)`\ndirectly, so the claim was false and four of them — catalog provider,\nschema provider, catalog provider list, table provider factory — still\nhanded an out-of-date extension library a bare `TypeError`.\n\nThose four take the host\u0027s logical extension codec rather than the session,\nwhich is why they could not simply be passed through as they stood: the\ndiagnostic would have told a catalog author their method \"must accept the\nSessionContext\", pointing them at the wrong parameter. Carry the argument\nand its description together in a `CapsuleGetterArg` so one diagnostic can\nserve getters that take a session, getters that take a codec, and getters\nthat take nothing. `Option\u003c\u0026Bound\u003cPyAny\u003e\u003e` still converts into it, so the\ndocumented `*_from_pycapsule` helper signatures are unchanged.\n\nThe three zero-argument getters (scalar, aggregate, window UDF) route\nthrough as well. Nothing can be refused there, but the rule is easier to\nfollow with no exceptions to remember.\n\nAlso add `validate_pycapsule` to `table_provider_from_pycapsule` and\n`ffi_logical_codec_from_pycapsule`, the two extraction sites that lacked\nit. This is not redundant with `pointer_checked`, despite appearances:\n`pointer_checked` bottoms out in CPython\u0027s `PyCapsule_GetPointer`, whose\nerror is the fixed string `PyCapsule_GetPointer called with incorrect\nname` and names neither the expected capsule nor the one received. Say so\nin a doc comment so it does not get \"simplified\" away later.\n\nDrop the unused `datafusion-proto` dependency from the query planner\nexample while here.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: correct the pin rationale and derived-context caveats\n\nThe Cargo.toml comment justified the pre-release pin by \"the FFI codec\nrebinding in `PySessionContext::derived_parts`\", a symbol that no longer\nexists. That rebinding was not removed, it moved into\n`set_session_query_planner`, which every `with_*` method calls. It depends on\n`FFI_QueryPlanner::new_with_ffi_codecs` unwrapping a `ForeignQueryPlanner` and\nreplacing its codecs, a swap that is a silent no-op before 55.1.0\n(apache/datafusion#24722). So the pin is still required, not droppable.\n\nThe `with_*` methods rebuild the installed planner on the *shared* session, so\nthe rebind takes effect even when the returned context is discarded.\n`with_python_udf_inlining` additionally claimed \"the original session is\nunchanged\", which the rebuild contradicts; it is the context\u0027s own codec\nsettings that are unchanged.\n\nAlso:\n- Note that the arity-vs-body TypeError split in `call_capsule_getter` holds\n  only because the call originates in Rust. A Python-level shim between the\n  host and the getter would supply a traceback and silently disable it.\n- Document the new FFI major-version gate in the 55.0.0 upgrade guide. Table\n  providers previously performed no such check, so a mismatched extension\n  library that used to load now raises ImportError.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* test: pin the planner rebind and guard the planner example on log warnings\n\n`test_a_discarded_derived_context_still_rebinds_the_planner` covers the\nsurprising half of the shared-session rebuild: a codec installed through a\ncontext that is then thrown away still binds to the session\u0027s planner. A fresh\ncodec instance makes it observable, since the planner encodes the outbound\nlogical plan with whichever codec it holds. Verified non-vacuous -- a codec\nbuilt but never installed reports zero encode calls.\n\nThe query planner example had no conftest, so it ran without the autouse\nfail-on-log-warning handler the provider example uses, despite calling\n`pyo3_log::init()` for the same reason. Copied verbatim; the suite passes\nunder it with no allowlist needed.\n\nTwo table provider tests called the deprecated `register_table_provider`,\nwhich is a one-line forwarder to `register_table`, so they reached the same\ncapsule path while emitting DeprecationWarning.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* chore: drop the stub uv.lock from the query planner example\n\nIt locked nothing -- a single editable entry for the crate itself and no\ndependencies. Nothing consumed it either: CI runs both example suites with\n`uv run --no-project`, and the older datafusion-ffi-example has no lock file\nat all. The codespell skip list in pyproject.toml matches on a bare `uv.lock`\nglob, so it still covers the repository root lock.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: scope the planner codec rebind to one layer\n\nInstalling a codec rebuilds the installed planner against it, but the\nrebuild reaches exactly one `ForeignQueryPlanner`. A planner that resolved\na fallback at install time keeps that fallback\u0027s codecs, and neither side\ncan repair it: the host has no handle past the first layer, and the planner\nlibrary cannot re-derive codecs at plan time because `FFI_QueryPlanner`\nholds them by value and `Session` exposes no accessor for the host\u0027s\ncurrent ones. Tracked upstream in apache/datafusion#24762.\n\nThe examples cannot demonstrate it. Their fallback lives in the same cdylib\nas its wrapper, and `From\u003c\u0026FFI_QueryPlanner\u003e` short-circuits on a matching\n`library_marker_id`, so a same-library hop never serializes. Measured: a\nlayered planner produces the same codec traffic as a flat one.\n\nWhat is demonstrable is that the session\u0027s planner tracks whichever handle\nwrote it last, so re-installing a planner from the original handle rebinds\nthe session back to that handle\u0027s codecs rather than picking up a codec\ninstalled through a derived one. Pinned by a new test as the sequel to\n`test_a_discarded_derived_context_still_rebinds_the_planner`.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: skip the planner rebind when inlining is unchanged\n\n`with_python_udf_inlining` rebuilds the handle\u0027s codecs and rebinds the\nsession\u0027s planner to them, which is state shared with every other handle on\nthat session. Asking for the setting a context already has changes nothing,\nso it should not pay that side effect: a defensive no-op toggle on `ctx`\notherwise drags the planner back onto `ctx`\u0027s codecs and silently undoes a\ncodec installed through another handle.\n\nReturning the existing codecs is observationally equivalent to the rebuild\notherwise -- it wraps the same inner codec in a fresh `Python*Codec` -- so\nthe guard is only visible through that side effect. The new test fails\nwithout it with `assert 0 \u003e 0`.\n\nAlso pins the divergence the rebind creates. The planner carries the codecs\nof whichever handle installed it last; every other path on a context uses\nthat context\u0027s own codec field. Those can be different handles, and then\n`Expr.to_bytes(ctx)` and `ctx.sql(...)` encode with different codecs on the\nsame `ctx`. Stated as a rule in the FFI guide rather than left implicit in\nthe description of the mechanism.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* refactor: route the last three capsule getters through the helper\n\n`call_capsule_getter` documents itself as having no exceptions to remember,\nand had three. The `from_pycapsule!` and `try_from_pycapsule!` macros each\nhand-rolled the getattr/call0 prologue, and `SessionConfig::with_extension`\ncalled `__datafusion_extension_options__` directly. All three take no\nargument, so with `CapsuleGetterArg::None` the helper returns the original\nerror untouched and the substitution is behavior-identical.\n\nThis is what makes the claim true rather than aspirational: the grep in the\nFFI capsule protocol skill now turns up no bare call sites.\n\n`with_extension` also gains the `validate_pycapsule` check every other\nextractor has, so a mismatched capsule is named instead of raising CPython\u0027s\nfixed \"called with incorrect name\". It keeps its own `hasattr` precheck,\nwhose AttributeError is more useful than the helper\u0027s pass-through, and\ntakes no version check because `FFI_ExtensionOptions` carries no version\nfield.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* chore: drop the unused datafusion-proto dep from the util crate\n\nIts only use was `physical_codec_from_pycapsule`, whose `dyn\nPhysicalExtensionCodec` output type came from that crate. That helper is\ngone, replaced by `ffi_physical_codec_from_pycapsule`, which returns the FFI\ntype and leaves the conversion to the caller. Nothing in `crates/util/src`\nreferences `datafusion_proto` any more, and it is not re-exported, so\nextension libraries depending on this crate are unaffected.\n\nThe package stays in Cargo.lock; crates/core still uses it.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n---------\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "ba5255a5975a8fe7746502c517aaa441a6eb05e8",
      "old_mode": 33188,
      "old_path": ".ai/skills/audit-skill-md/SKILL.md",
      "new_id": "5f3177b3d06d5b7f6d00b5ec29a034e48f1e9521",
      "new_mode": 33188,
      "new_path": ".ai/skills/audit-skill-md/SKILL.md"
    },
    {
      "type": "modify",
      "old_id": "a3d82a6706cee60a7b2eda1ed803d2f75e854cc0",
      "old_mode": 33188,
      "old_path": ".ai/skills/check-upstream/SKILL.md",
      "new_id": "828f227d889bbaa696e2074af106adf0a7dd3a37",
      "new_mode": 33188,
      "new_path": ".ai/skills/check-upstream/SKILL.md"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "468216034236aba322c551b157c3d5071c6bbd0b",
      "new_mode": 33188,
      "new_path": ".ai/skills/ffi-capsule-protocol/SKILL.md"
    },
    {
      "type": "modify",
      "old_id": "7d490ec03fe9a52de7c8f490252db37d355743cf",
      "old_mode": 33188,
      "old_path": ".ai/skills/make-pythonic/SKILL.md",
      "new_id": "24c2bb8176a01d04e7477c22439b93658b9183fd",
      "new_mode": 33188,
      "new_path": ".ai/skills/make-pythonic/SKILL.md"
    },
    {
      "type": "modify",
      "old_id": "c35801b11f2cfe424978b3fde10ab664bfeb9532",
      "old_mode": 33188,
      "old_path": ".github/workflows/build.yml",
      "new_id": "d7af9b66372b58a0f90927c9bfd4125ecd1d6b33",
      "new_mode": 33188,
      "new_path": ".github/workflows/build.yml"
    },
    {
      "type": "modify",
      "old_id": "558e751c80147abbc717b151bdbeb83a9d6a71f6",
      "old_mode": 33188,
      "old_path": ".github/workflows/test.yml",
      "new_id": "047b35039c318214b602a0f3774fdc125fdb40aa",
      "new_mode": 33188,
      "new_path": ".github/workflows/test.yml"
    },
    {
      "type": "modify",
      "old_id": "fda08b23c1da768085a48c3c64fa901a0750f1c2",
      "old_mode": 33188,
      "old_path": "AGENTS.md",
      "new_id": "327ebd6435cd3cb4aef51af9f5f3c22a0f6917aa",
      "new_mode": 33188,
      "new_path": "AGENTS.md"
    },
    {
      "type": "modify",
      "old_id": "9e0c1862ae07700a256ae970a4da5ac7ddb787c2",
      "old_mode": 33188,
      "old_path": "Cargo.lock",
      "new_id": "c7632732a1ba35bf32eb009c85519558813dce25",
      "new_mode": 33188,
      "new_path": "Cargo.lock"
    },
    {
      "type": "modify",
      "old_id": "9896e742166e143e06d55bd8893f031901d1a349",
      "old_mode": 33188,
      "old_path": "Cargo.toml",
      "new_id": "0fabd5437b9f0f9e9444d6261442a032118c589f",
      "new_mode": 33188,
      "new_path": "Cargo.toml"
    },
    {
      "type": "modify",
      "old_id": "8ad49b098458b3c749bcec35e22487715b031848",
      "old_mode": 33188,
      "old_path": "crates/core/src/catalog.rs",
      "new_id": "dbc95661135ec6ac629342082e0685ee7cda471e",
      "new_mode": 33188,
      "new_path": "crates/core/src/catalog.rs"
    },
    {
      "type": "modify",
      "old_id": "26853e69f25333e1666fcb41d967dffb524253f5",
      "old_mode": 33188,
      "old_path": "crates/core/src/codec.rs",
      "new_id": "94942a2d299a0bcdb4baa25e6f7985b8766bc790",
      "new_mode": 33188,
      "new_path": "crates/core/src/codec.rs"
    },
    {
      "type": "modify",
      "old_id": "7bbeed2f135625265e7889ecd33a346c04fa2d62",
      "old_mode": 33188,
      "old_path": "crates/core/src/context.rs",
      "new_id": "75bfed60161d5d03e9d24bd79e08d7c1d345a80e",
      "new_mode": 33188,
      "new_path": "crates/core/src/context.rs"
    },
    {
      "type": "modify",
      "old_id": "caf7b97bcc7d6062ad7c8807d67b16ba133032df",
      "old_mode": 33188,
      "old_path": "crates/core/src/udaf.rs",
      "new_id": "6a2675193d44dd6df4852b3f79851460ad763d1d",
      "new_mode": 33188,
      "new_path": "crates/core/src/udaf.rs"
    },
    {
      "type": "modify",
      "old_id": "2006401db33e64d7124c1ee17b028b8485fbe55b",
      "old_mode": 33188,
      "old_path": "crates/core/src/udf.rs",
      "new_id": "6376c81a8d1e0dd3d225fc2ce8cbed44e97ce81c",
      "new_mode": 33188,
      "new_path": "crates/core/src/udf.rs"
    },
    {
      "type": "modify",
      "old_id": "cffa0c12a7d29c34ded39a57b474c20130fcdd3d",
      "old_mode": 33188,
      "old_path": "crates/core/src/udtf.rs",
      "new_id": "51ea8fa4fb195e1105d8cee0cb1c1d1926420c58",
      "new_mode": 33188,
      "new_path": "crates/core/src/udtf.rs"
    },
    {
      "type": "modify",
      "old_id": "ebec8f3bd4b1922096c274cd764560175e2f49c8",
      "old_mode": 33188,
      "old_path": "crates/core/src/udwf.rs",
      "new_id": "8935c9ba84a069dd86cf73fc801e466d1146a485",
      "new_mode": 33188,
      "new_path": "crates/core/src/udwf.rs"
    },
    {
      "type": "modify",
      "old_id": "c23667b0f5587f2ba7d7625ba97e9d2f014bd2c5",
      "old_mode": 33188,
      "old_path": "crates/util/Cargo.toml",
      "new_id": "00d5946a5ef2de9635006e471a67d2ce42a74be8",
      "new_mode": 33188,
      "new_path": "crates/util/Cargo.toml"
    },
    {
      "type": "modify",
      "old_id": "9327d7f2f5228621f81dffedecaba7172f0ab413",
      "old_mode": 33188,
      "old_path": "crates/util/src/lib.rs",
      "new_id": "5b31f77080093ccd115a6dca74acce705e01a458",
      "new_mode": 33188,
      "new_path": "crates/util/src/lib.rs"
    },
    {
      "type": "modify",
      "old_id": "bf65cad2a0e5f69d2f74ee2e32fc1383718b6395",
      "old_mode": 33188,
      "old_path": "docs/source/contributor-guide/ffi.md",
      "new_id": "31cd9391f145bcabbf7ad7f6240f347649fd6a17",
      "new_mode": 33188,
      "new_path": "docs/source/contributor-guide/ffi.md"
    },
    {
      "type": "modify",
      "old_id": "3c436ba1d6c47abcbacd22dfdf8e58bd3d7c3532",
      "old_mode": 33188,
      "old_path": "docs/source/user-guide/io/table_provider.md",
      "new_id": "5dc2dc086f1549b598e5a7771720203da1278a84",
      "new_mode": 33188,
      "new_path": "docs/source/user-guide/io/table_provider.md"
    },
    {
      "type": "modify",
      "old_id": "360e0533c5c046aa56b779e03de7be3d1331f3e7",
      "old_mode": 33188,
      "old_path": "docs/source/user-guide/upgrade-guides.md",
      "new_id": "29085bc3d19bb7192594d3aa39ff36163cdc65d3",
      "new_mode": 33188,
      "new_path": "docs/source/user-guide/upgrade-guides.md"
    },
    {
      "type": "modify",
      "old_id": "e0e3056d9ec4211fc70ccdc294521de80fc6599a",
      "old_mode": 33188,
      "old_path": "examples/README.md",
      "new_id": "7bbb45dcf06a5f445b5f4330a605e8acb753cf0c",
      "new_mode": 33188,
      "new_path": "examples/README.md"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "0fa10d7f32b6e6660e5ab50c0c5e3247a5bf5ce7",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/README.md"
    },
    {
      "type": "modify",
      "old_id": "7f85e94877d3b66cca361bdcb15210ff2e9737fc",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/pyproject.toml",
      "new_id": "c51fa8a8d4ffbb8db393d2c2e2a6665ae86c3f0e",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/pyproject.toml"
    },
    {
      "type": "modify",
      "old_id": "a56b5855c0114f0d5edc17ba52046b19ada99b3a",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/catalog_provider.rs",
      "new_id": "75890d083286a95a8accb902524d5ff48fcacd8a",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/catalog_provider.rs"
    },
    {
      "type": "modify",
      "old_id": "eccf7b81ac62fe0b4eb24deeb8f10e37a4f7993f",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/lib.rs",
      "new_id": "3d00fdb3ed7c9816f35d0159433f35b00cff0230",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/lib.rs"
    },
    {
      "type": "modify",
      "old_id": "8c3976d3762e67e90ee041091f9c4fd86c2d06d3",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/logical_extension_codec.rs",
      "new_id": "1fcaaef4c97c006df6e188b1ef72b29a0b959137",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/logical_extension_codec.rs"
    },
    {
      "type": "modify",
      "old_id": "35ef77f6b6a18430c6196727068ad64c65ff84b7",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/physical_extension_codec.rs",
      "new_id": "f9e96382e02f53a9c52b1f5698f3eb4ebc36112a",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/physical_extension_codec.rs"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "a21362d7f1ed87f6edbd44b5eb66a9abaade1cb0",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/required_udf.rs"
    },
    {
      "type": "modify",
      "old_id": "55543cb597863b2e5792e91762df3cc500d6fc41",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/table_function.rs",
      "new_id": "e653aeab1a1027f501c428345f3a8422fa66e74d",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/table_function.rs"
    },
    {
      "type": "modify",
      "old_id": "5756e6d0231986798b6336eb525f6d7de99dcea7",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/table_provider.rs",
      "new_id": "ef6430e2934c82bd9a80a88fa21e510ded1afd05",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/table_provider.rs"
    },
    {
      "type": "modify",
      "old_id": "71dfd73ca856d244fb68ad38488fd94015eea730",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/table_provider_factory.rs",
      "new_id": "df0845119a0dd9903ad5a5baafd6244aa6ae6430",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/table_provider_factory.rs"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "4d02c69f11538f415df9e8e30961abebf5affd77",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/Cargo.toml"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "72f96bb8e5160b1233cc4aca14bb52b40804f126",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/README.md"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "4878d8b0e106f29b3f2c5a37c8388525e5b97e87",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/build.rs"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "9e34b4cd411d6b1c12d65b0c8ee510e1b58087be",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/pyproject.toml"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "d046f67a6c378dabafc18499fa63e2c02c30f55a",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/python/tests/_test_three_library_query_planner.py"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "68f8057af46557bc41af640e96019853b3f9cf89",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/python/tests/conftest.py"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "ecfa4b9432f39bf835ff3f59220c8c10c6760cbd",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/src/config.rs"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "c505c1ce7ed2a193deb922a91e5730f827ba5dcc",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/src/lib.rs"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "67262e39c9def07d77461462a2903a08318e00f3",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/src/planner.rs"
    },
    {
      "type": "modify",
      "old_id": "94b2bb1c63acd9579e7d73109a2506dec00ea0e8",
      "old_mode": 33188,
      "old_path": "python/datafusion/context.py",
      "new_id": "2f2cc611911eb8995ecf884b5fe696ef09dfb95c",
      "new_mode": 33188,
      "new_path": "python/datafusion/context.py"
    },
    {
      "type": "modify",
      "old_id": "394c682ae87c041fb262f6ffbcc4c99e7529cdea",
      "old_mode": 33188,
      "old_path": "python/datafusion/user_defined.py",
      "new_id": "43b53e469f51ff7732e387b9263a260a136d36f8",
      "new_mode": 33188,
      "new_path": "python/datafusion/user_defined.py"
    },
    {
      "type": "modify",
      "old_id": "7d038c7a5c30a4e86a4241f2382baaea1ad49b43",
      "old_mode": 33188,
      "old_path": "python/tests/test_context.py",
      "new_id": "3c95835af342ba11707732169de273263a580cdb",
      "new_mode": 33188,
      "new_path": "python/tests/test_context.py"
    },
    {
      "type": "modify",
      "old_id": "588caa21a8179d119b350690ed1ef911f1e167a2",
      "old_mode": 33188,
      "old_path": "python/tests/test_pickle_expr.py",
      "new_id": "451f5d21595cbeb8c667c0f070df93571f4f50f6",
      "new_mode": 33188,
      "new_path": "python/tests/test_pickle_expr.py"
    },
    {
      "type": "modify",
      "old_id": "11e709f6b7065d18412de7de8a0a4289d9986128",
      "old_mode": 33188,
      "old_path": "python/tests/test_plans.py",
      "new_id": "0145d123e0146eaa2f9ce34d2f45b81153131095",
      "new_mode": 33188,
      "new_path": "python/tests/test_plans.py"
    },
    {
      "type": "modify",
      "old_id": "dcb2bacc379b42b93f2c59efc61e0d42f89f08fd",
      "old_mode": 33188,
      "old_path": "python/tests/test_udtf.py",
      "new_id": "aa0599ffa1dbc2e19d5856618c3df63d7e78fc3d",
      "new_mode": 33188,
      "new_path": "python/tests/test_udtf.py"
    }
  ]
}
