)]}'
{
  "commit": "b6c6f5b94afad5a2ed37cf8bfa4cbf2ed9aa3f9c",
  "tree": "9c5ad85b68cc69e4343f57056ce59cb68cd8b526",
  "parents": [
    "e6c75870a2c465aae4787605f91a9dff47b0e911"
  ],
  "author": {
    "name": "Tim Saucer",
    "email": "timsaucer@gmail.com",
    "time": "Fri Sep 04 14:07:52 2026 -0400"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Fri Sep 04 14:07:52 2026 -0400"
  },
  "message": "Make extension codecs composable (#1678)\n\n* Make extension codecs composable\n\nInstalling a logical or physical extension codec now prepends it to a\ncodec chain instead of replacing the prior codec. The most recently\ninstalled codec is consulted first, falling through codec by codec to\nthe default codec. This lets multiple independent extension libraries\ninstall codecs on the same session, and removes the codec registration\nordering requirement between libraries.\n\nChain dispatch treats a codec error as \"not mine\". Encoding runs each\ncodec against a scratch buffer so failed attempts leave no partial\nbytes, and treats Ok-with-no-bytes (encode by name) as no opinion so\nlater codecs still get a chance. When every codec fails, the errors\nare aggregated so the owning codec\u0027s diagnostic is not masked by the\ndefault codec\u0027s generic error.\n\nAlso preserves the python_udf_inlining setting when installing a\ncodec; previously it was silently reset to enabled.\n\nDocuments the remaining planner constraint: a session holds one query\nplanner, layering is explicit via fallback capsules, and codecs must\nbe installed before exporting or chaining planners because a planner\ncapsule captures the codecs at export time.\n\nCo-Authored-By: Claude Fable 5 \u003cnoreply@anthropic.com\u003e\n\n* test: port codec Rust tests to pytest\n\nThe Rust tests added for the composable codec work never ran: CI invokes\n`cargo fmt` and `cargo clippy --all-targets` but no `cargo test`, so the\ntests compiled and were never executed. Rather than add a `cargo test`\njob — which would also require feature-gating `pyo3/extension-module`,\nsince the test binary cannot link on Linux while it is unconditional —\nmove the coverage to pytest, matching this repository\u0027s practice of\ntreating the user-facing Python surface as the first line of defense.\n\nRemove both `#[cfg(test)]` modules from crates/core/src/codec.rs and\nreplace them as follows:\n\n- Four wire-header round-trip tests and the Python-minor-mismatch test\n  were already covered by existing cases in test_pickle_expr.py.\n- `strip_errors_on_too_old_version` asserted nothing: it returns early\n  because WIRE_VERSION_MIN_SUPPORTED equals WIRE_VERSION_CURRENT.\n- The unsupported-wire-version and Python-major-mismatch cases move to\n  test_pickle_expr.py, patching the header in place inside the encoded\n  protobuf. The patches preserve length so the outer message stays\n  parseable and the bytes reach the codec.\n- The three truncated-header cases are dropped. Truncation changes the\n  payload length and breaks the protobuf framing, so they fail before\n  reaching the header check and cannot be expressed from Python.\n- The codec-chain tests move to the FFI example suite, which exercises\n  the same chain through the real FFI boundary.\n\nMyLogicalExtensionCodec gains an optional token overriding the byte\nprefix it stamps on encoded table providers. Two instances with distinct\ntokens own disjoint slices of the wire format, which is what makes chain\nordering and fall-through observable from Python.\n\nThe ported inlining test asserts encode and decode behavior rather than\nthe `python_udf_inlining()` getter the Rust test checked. This is a\nstronger assertion: the getter is preserved even when a composed\nPython-aware codec re-inlines a UDF that the outer strict codec declined\nto inline, so the original test could not have caught that path.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: record the Python-first testing preference in AGENTS.md\n\nNew coverage should land as a doctest example or a pytest case. Agents have\nbeen adding Rust tests that CI never executes: no workflow invokes\n`cargo test`, and `cargo clippy --all-targets` only compiles the test code.\nWrite down that constraint, along with the reason a `cargo test` job is not a\ntrivial addition, so the tradeoff does not have to be rediscovered.\n\nAlso point at the FFI example suites, which are easy to overlook when judging\nwhether behavior is reachable from Python.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* refactor: dispatch chained codecs by identity, not by trial\n\nDecoding walked the codec chain and took the first codec that returned\nOk. That is unsound, and upstream has already been bitten by it:\napache/datafusion#16980 records ComposedPhysicalExtensionCodec decoding\nan encoded parquet file as csv because the payload happened to parse\nwith an earlier codec, and #16986 fixed it by recording the encoder.\nProtobuf carries no type identity, so two codecs whose leading field\nnumbers and wire types line up decode each other\u0027s payloads cleanly.\nA byte-prefix convention does not help: the natural implementation is\n`Message::decode(buf)`, which has no prefix to check and cannot decline.\n\nPayloads written by a chained codec now carry an envelope naming the\ncodec that wrote them, and decoding consults exactly that codec. The\nenvelope is applied and stripped inside PythonLogicalCodec /\nPythonPhysicalCodec, so third-party codecs are unmodified and never see\nit -- they receive the bare bytes they wrote.\n\nKeyed on a stable identity rather than chain position. Position is\nsound for Ballista and datafusion-distributed because their codec lists\nare pinned -- a compile-time constant in one, a fixed entry plus\nappended user codecs rebuilt from the same startup code in the other.\nA datafusion-python chain is assembled by user Python across sessions\nthat share no struct, and a library shipping a codec cannot know its own\nindex, so position would silently name the wrong codec whenever two\nsessions install in different orders.\n\nIdentity is derived and asks nothing of existing libraries: an explicit\ncodec_id, else __datafusion_codec_id__, else the exporting class\u0027s\nmodule and qualified name. A bare PyCapsule exposes nothing stable --\nevery capsule reports the same type -- so it gets a session-local id and\na pointed error if its payloads reach an unrelated session. Installing\ntwo codecs under one id is rejected at install time rather than\nresolving to whichever entry came first.\n\nCodecs now append rather than prepend, so encoding is claimed by the\nfirst codec installed that wants the object. Installing a library can\nonly claim objects nothing else claimed; it can never take over an\nexisting library\u0027s objects, and it never renumbers ids that older\npayloads reference.\n\nTwo payloads are deliberately left unframed. The terminal codec writes\nbare, so a session with no extension codecs is byte-identical to a build\nwithout chaining. And an encode that writes nothing stays empty: an\nempty fun_definition is DataFusion\u0027s encode-by-name signal, and framing\nit would set the field, permanently skipping the registry lookup the\ndecoder does first and breaking codecs that reconstruct a function from\nits name alone (from_proto.rs, the `None \u003d\u003e ctx.udf(..).or_else(..)`\narm). That arm is also why an empty buffer still consults every codec:\nthere are no bytes to tag. It is not the hazard tagging removes -- the\nquestion asked is \"do you own the function named x\", which is\nname-scoped, and a disagreement needs two libraries claiming one name.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* test: pin the by-name decode path, and document identity dispatch\n\nAdds NameOnlyUdfCodec to the FFI example: a codec owning functions that\nare fully described by their names, so try_encode_udf writes nothing and\ntry_decode_udf rebuilds the function from the name with no registry\nentry. DataFusion supports that shape directly -- an empty\nfun_definition sends the decoder to the FunctionRegistry first and the\ncodec second (from_proto.rs, the `None \u003d\u003e ctx.udf(..).or_else(..)` arm).\n\nNothing covered that arm before, and it is the path most at risk from a\nplausible change: wrapping every chained encode in the identity envelope\nwould make an empty payload non-empty, set fun_definition, and skip the\nregistry lookup permanently. That breaks ordinary by-name round trips as\nwell as codecs like this one, and no other test would notice. The\ndecoding session here deliberately never registers the function, so only\nthe codec can supply it.\n\nExposes logical_extension_codec_ids() and physical_extension_codec_ids()\non SessionContext. The ids are the dispatch keys a payload names, so\nlisting them answers \"which library owns this plan\" and \"can this\nsession decode it\" -- and they are what a decode failure reports.\n\nDocumentation rewritten around identity rather than ordering:\n\n- ffi.md states that codecs need no changes, why rejecting foreign\n  payloads is not something a codec can reliably do, the identity\n  resolution ladder, and the two payloads left unframed with the reason\n  for each.\n- Both example READMEs drop the \"most recently installed is consulted\n  first\" language, which described dispatch that no longer exists.\n- upgrade-guides.md gains a section for the behaviour change: codecs\n  compose rather than replace, no codec-side change is required, and the\n  wire format changes only for sessions that install one.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: record codec chain dispatch in the FFI capsule protocol skill\n\nThe skill already triggers on \"any FFI_* export that asks for ... an\nextension codec\", but had nothing to say about how a chain decides which\ncodec handles a payload -- the part of this area most likely to be got\nwrong, and the part with a rationale that is not visible from the code.\n\nRule 8 records it: dispatch by identity rather than by trying codecs\nuntil one returns Ok, with the upstream incident that settles the\nquestion (apache/datafusion#16980, a Parquet payload decoded as CSV) and\nthe protobuf reason a byte-prefix convention cannot fix it.\n\nIt also records why we key on identity where Ballista,\ndatafusion-distributed, and upstream all key on chain position. Their\nlists cannot disagree -- one is a compile-time constant, the other a\npinned entry plus user codecs rebuilt from the same startup code. Ours is\nassembled by user Python, and to_bytes/from_bytes puts two independently\nbuilt sessions on either end of one payload. Copying the upstream design\nhere reintroduces the same silent mis-decode from the other direction,\nwhich is exactly the kind of thing an agent reading only upstream would\ndo.\n\nAnd it records the two payloads that are never framed, because framing\nthe empty one is a plausible tidy-up that breaks by-name decoding\nsilently. Points at NameOnlyUdfCodec as the guard.\n\nExtends the trigger to cover codec.rs dispatch, which the old wording\ndid not obviously reach, and documents the optional\n__datafusion_codec_id__ hook on the two codec Protocols -- where Rule 5\nsays such things belong.\n\nReviewed the other four skills: check-upstream, make-pythonic,\naudit-skill-md, and the user-facing datafusion_python skill are all\nunaffected, none of them touching codec dispatch.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: move codec dispatch rationale out of the skill and into the code\n\nRule 8 restated, less precisely, what the docstrings in codec.rs already\nsay: identity dispatch, why not position, the two unframed payloads,\nappend-and-first-claim-wins. A skill earns its place by carrying what\nyou cannot learn from the file you are about to edit -- Rule 1 greps a\nfamily across files, Rule 3 governs code in an extension author\u0027s own\nrepo, Rule 5 is a four-file process checklist. \"How the dispatch in\ncodec.rs works\" is what codec.rs is for, and duplicating it there just\ncreates a second copy to keep in sync.\n\nTwo pieces were worth keeping, and neither is a rule.\n\nThe apache/datafusion#16980 citation moves to `chain_decode`, next to\nthe code someone would be editing when tempted to delete the envelope\nand walk the chain instead. It is the concrete evidence that makes the\nwarning land, and it was the one thing the skill had that the code did\nnot.\n\nRule 5 gains a clause: changing what a codec puts on the wire is as\nbreaking as changing a getter signature, and easier to miss because\nnothing fails to compile. That is genuinely cross-file process, which is\nwhat the skill is for.\n\nAlso enriches ChainEntry\u0027s docstring with the datafusion-distributed\nhalf of the \"why not position\" argument, which previously named only\nBallista and upstream. Both consumers matter: the point is that their\ncodec lists structurally cannot disagree and ours can.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: give a capsule-installed codec an id no other session can mint\n\nA codec installed from a bare PyCapsule has nothing stable to derive an\nidentity from, so it was tagged `anon:{chain_length}`. That is a position,\nnot an identity: every session numbers from the same end, so two unrelated\nsessions that each install one capsule both mint `anon:0`. A payload written\nby the first was then handed to the second session\u0027s codec — the positional\ndispatch this chain design exists to avoid, reintroduced in the one case\nthat has no derivable id.\n\nThe failure was quiet. A codec offered bytes it does not recognise falls\nthrough to its own inner default codec, so the error came back as\n`LogicalExtensionCodec is not provided` and named neither codec, instead of\nthe intended \"encoded by extension codec X, which is not installed on this\nsession\" with the `codec_id\u003d` hint.\n\nMint a random id per install instead, reusing the `Uuid` idiom already in\n`context.rs`. The chain clones the id along with the codec, so a payload\nstill decodes anywhere in the installing session\u0027s lineage; everywhere else\nit now fails with the pointed error. `derive_codec_id` no longer needs the\nchain length, so that parameter goes away.\n\nAlso fixes two docs that described the pre-identity-dispatch design: the\n`codec.rs` module doc claimed codecs are consulted most-recently-installed\nfirst (it is install order, and decoding does not walk the chain at all) and\nasked downstream codecs to reject foreign payloads (an encode-side contract\nonly — a payload only ever reaches the codec whose id it carries), and a\nphysical codec test docstring said a second install prepends.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: identify a session installed as a codec by its session, not its class\n\nInstalling one context\u0027s codec stack on another session derived the\nidentity from the class, and every `SessionContext` shares one class. So\nevery session reported `datafusion.context.SessionContext`: two of them\ncould not coexist on one target, and a payload written through one\nresolved to the other on decode, failing as\n`LogicalExtensionCodec is not provided` from the stranger\u0027s own inner\ndefault codec. Same shape as the positional `anon:{n}` collision, but via\nan id that looks portable, so the error carried no hint either.\n\n`SessionContext` now declares `__datafusion_codec_id__` carrying its\nsession id. That goes through the existing resolution arm for an object\npinning its own identity, so one implementation covers both the Python\nwrapper and the internal object and `derive_codec_id` keeps its four\ndocumented arms. Handles derived from one session report the same id, so\ninstalling two of them on one target is refused — their payloads would be\nindistinguishable on decode.\n\nFraming is unchanged. A session-exported codec is opaque across the FFI\nboundary, so the outer session cannot enumerate what is inside it and the\nenvelope naming its own entry is the only handle it has; nesting costs\nabout 50 bytes per hop and only arises when composing sessions, which a\nlibrary exporting its own codec class never does. What that route needed\nwas a usable identity, not fewer frames.\n\nDocuments the lifetime coupling that comes with it: imported codecs\nresolve their task context against the source session and stop working\nwhen it is dropped, so this composes sessions rather than copying codecs\nout of one.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* test: pin the class-derived codec id, and the escape hatch from it\n\nA codec that declares no identity is named `module.QualName`, and that\nstring goes on the wire in front of every payload it writes. Nothing\nasserted it, so renaming `MyLogicalExtensionCodec` or moving its module\nwould have silently changed the wire format with a green suite.\n\nPins it twice: against the literal, so a rename has to come here and be\nacknowledged, and against `__module__`/`__qualname__`, so the literal\ncannot drift away from what the code actually derives.\n\nAlso covers the reason `__datafusion_codec_id__` exists, which had no\ntest either. A plan encoded by a codec under its old class name decodes\non a session that only knows the new one, and the payload carries the\npinned id rather than either class name. Under the class-derived default\nthose are two different ids and the plan is undecodable.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: aim the codec documentation at its readers\n\nThe codec documentation added in this PR drifted into narrating the\nimplementation. An extension author cannot act on the name of a private\nRust type, and a caller installing a library\u0027s codec does not need the\nenvelope layout or the argument for why trial decoding is unsound.\n\nRemoved from `ffi.md`: the paragraph naming `Python{Logical,Physical}Codec`\nas the thing that wraps payloads, the protobuf-ambiguity argument with the\nupstream postmortem, the note on why the bare-capsule id is random, and the\ntwo-unframed-payloads section, which opened by addressing whoever is\n\"changing this code\". What each reader can act on stays: implement your\ncodec as though yours is the only one, do not write defensive prefix checks,\nhow identity is derived, when to pass `codec_id\u003d`, that install order does\nnot affect decoding, and that a codec may encode a function by name alone.\nAll of the removed rationale already lives in `crates/core/src/codec.rs`,\nwhich the section now points to for anyone changing the framing.\n\nTrimmed `with_logical_extension_codec` to what a caller acts on, with a\n`:ref:` to the FFI guide for the rest, and added the `ValueError` on a\nduplicate id, which the longer version buried. `with_physical_extension_codec`\nnow delegates in one sentence instead of restating the same paragraphs, which\nhad already begun to drift.\n\nReworded the internal vocabulary out of the upgrade guide and\n`logical_extension_codec_ids`: \"terminal codec\", \"writes unframed\", and\n\"identity envelope\" name internals a reader cannot see. Dropped the\nexplanation of why a session id rather than a class name identifies a\ncontext installed as a codec, keeping the constraint that follows from it.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: state the one case where a codec sees bytes it may not own\n\nThe FFI guide told codec authors not to guard against another library\u0027s\npayloads, on the grounds that such a check \"simply never fires\". That was\nwrong twice over. The check executes on every decode, and its reject branch\nis reachable: an empty payload carries no identity to route on, so\n`chain_resolve_by_name` offers it to every installed codec in turn, and a\ncodec asked about a function it does not own rejects — correctly.\n\nThat exception is worth documenting on its own terms, because an author who\ntrusts the \"you only ever see your own payload\" guarantee can index into an\nempty `buf` and panic. `try_decode_udf` and its aggregate and window\nsiblings can be called with an empty buffer and another library\u0027s `name`, so\nthe guide now says to decide from `name` and not to assume `buf` is\nnon-empty. `name_only_codec.rs` in the FFI example already does exactly\nthat, so the guide and the worked example agree.\n\nThe advice against defensive checks is gone rather than reworded. An author\nwho already writes one keeps it and nothing breaks; an author who does not\nneeds nothing. What remains is the portability case: a codec that also ships\nto hosts dispatching by position or by trial may still want its own guard,\nand keeping one costs nothing here.\n\nTwo claims in codec.rs conflated a marker check with relying on decode\nfailure. A marker an encoder writes and a decoder checks is reliable — it is\nwhat the envelope itself does. What cannot be relied on is\n`MyMessage::decode(buf)` declining someone else\u0027s bytes, and what cannot be\nrequired is third-party codecs all honouring such a convention. Both are now\nstated as what they are.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: call a codec id an id, and say what one is\n\nThe codec documentation used \"identity\" as a term of art without ever\ndefining it, while the API it describes calls the same thing `codec_id`.\nA reader met an abstract noun, had to guess what it referred to, and then\nhad to map it onto the parameter they actually type.\n\nThe FFI guide now defines it once, where the surrounding text has already\ndescribed the mechanism: datafusion-python records which codec wrote each\npayload, and that record is the codec\u0027s id — a short string stored inside\nthe plan, which has to name the same codec in the process that decodes as\nit did where the plan was written. That last clause is the reason ids\nexist and is the part a reader needs.\n\nReplaced the derivation-order list with the three cases where a reader has\nto do something: two instances of one class collide and raise `ValueError`,\na bare `PyCapsule` gets an id private to its session, and a class you\nintend to rename wants `__datafusion_codec_id__`. Each names the symptom\nand the fix. The order in which an id is resolved answered a question\nnobody asks, and the sub-clause about how a `SessionContext` names itself\ndescribed an id nobody types. The session-composition paragraph loses its\nid explanation, keeping the lifetime coupling that is the real hazard.\n\nNet 34 lines to 26 in that part of the guide, with a definition it did not\nhave before. The same substitution follows through the upgrade guide, the\n`SessionContext` and protocol docstrings, and both example READMEs, since\nleaving the user-facing pages on the old vocabulary would have been worse\nthan the original.\n\nUntouched: \"shared-library identity\" earlier in the guide, which predates\nthis work and refers to distinct DataFusion library markers.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: plain language for the two codec id prefixes\n\nBoth doc comments stacked several clauses per sentence and stated the\nrejected alternatives abstractly, which a reviewer reported as hard to\nparse. Same content, plainer register: the anonymous prefix now says\nthe suffix is a fresh UUID, matching what derive_codec_id mints, and\nspells out the anon:0 collision instead of describing it as sessions\nnumbering from the same end.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* test: count decodes, not just encodes, on the composed chain\n\ntest_ffi_logical_codec_composes_with_later_install asserted the encode\ncount and then only that the round-trip produced equal batches. The\nround-trip result alone does not distinguish the codec decoding the\nbytes it wrote from the table being resolved some other way, so the\ndecode side of the chain was unpinned. Count it too.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* docs: describe the composed-codec test in install order\n\nThe docstring for test_composed_codecs_with_query_planner said the\nextra codecs decline everything and encoding falls through to the\nprovider codecs, which describes the reverse of what happens. The\nprovider codecs are installed first and chain_encode walks in install\norder, so they claim their objects before the extra codecs are\nconsulted; decoding dispatches by id and does not consult them at all.\nSay that, and say what the test asserts: the extra pair changes\nnothing, where replace semantics would have broken the round trip.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n---------\n\nCo-authored-by: Claude Fable 5 \u003cnoreply@anthropic.com\u003e",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "468216034236aba322c551b157c3d5071c6bbd0b",
      "old_mode": 33188,
      "old_path": ".ai/skills/ffi-capsule-protocol/SKILL.md",
      "new_id": "294ebfb3a23d24eceea7617c9599b5533011bc87",
      "new_mode": 33188,
      "new_path": ".ai/skills/ffi-capsule-protocol/SKILL.md"
    },
    {
      "type": "modify",
      "old_id": "327ebd6435cd3cb4aef51af9f5f3c22a0f6917aa",
      "old_mode": 33188,
      "old_path": "AGENTS.md",
      "new_id": "659094ec000123c838eca961f8fed69476c4076b",
      "new_mode": 33188,
      "new_path": "AGENTS.md"
    },
    {
      "type": "modify",
      "old_id": "94942a2d299a0bcdb4baa25e6f7985b8766bc790",
      "old_mode": 33188,
      "old_path": "crates/core/src/codec.rs",
      "new_id": "8f43bdb5edd6157a4891ce62964cdfb25a2082fd",
      "new_mode": 33188,
      "new_path": "crates/core/src/codec.rs"
    },
    {
      "type": "modify",
      "old_id": "75bfed60161d5d03e9d24bd79e08d7c1d345a80e",
      "old_mode": 33188,
      "old_path": "crates/core/src/context.rs",
      "new_id": "84182ff190ff8ea82b0f65026cfa49a9ce93bc1d",
      "new_mode": 33188,
      "new_path": "crates/core/src/context.rs"
    },
    {
      "type": "modify",
      "old_id": "31cd9391f145bcabbf7ad7f6240f347649fd6a17",
      "old_mode": 33188,
      "old_path": "docs/source/contributor-guide/ffi.md",
      "new_id": "d86858a83905c2076a71d357493f583dedcd1ab4",
      "new_mode": 33188,
      "new_path": "docs/source/contributor-guide/ffi.md"
    },
    {
      "type": "modify",
      "old_id": "29085bc3d19bb7192594d3aa39ff36163cdc65d3",
      "old_mode": 33188,
      "old_path": "docs/source/user-guide/upgrade-guides.md",
      "new_id": "257749c3a95c35795ef3400ccce1c0a85db18f5e",
      "new_mode": 33188,
      "new_path": "docs/source/user-guide/upgrade-guides.md"
    },
    {
      "type": "modify",
      "old_id": "0fa10d7f32b6e6660e5ab50c0c5e3247a5bf5ce7",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/README.md",
      "new_id": "401eb610a1a3ce26f19f4360b0b928717ef78ae8",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/README.md"
    },
    {
      "type": "modify",
      "old_id": "cd0c5a61ae585a94bbecf75d680cdcba3b7ba1ee",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/python/tests/_test_logical_extension_codec.py",
      "new_id": "ac4c69d14c6696c15bfe47fa1f4f22dfe99ecec1",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/python/tests/_test_logical_extension_codec.py"
    },
    {
      "type": "modify",
      "old_id": "28eaaf2d92f8f293f91ed2c43a5f0b6d95b48310",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/python/tests/_test_physical_extension_codec.py",
      "new_id": "c7a6ede7b15b9016829cfdcf1a13ef9e9f0392f0",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/python/tests/_test_physical_extension_codec.py"
    },
    {
      "type": "modify",
      "old_id": "3d00fdb3ed7c9816f35d0159433f35b00cff0230",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/lib.rs",
      "new_id": "92fccb1e2394034eabe8f11cf52a07a913563056",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/lib.rs"
    },
    {
      "type": "modify",
      "old_id": "1fcaaef4c97c006df6e188b1ef72b29a0b959137",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-example/src/logical_extension_codec.rs",
      "new_id": "5660489d4e69f9fc79d13c99c9139b911728f212",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/logical_extension_codec.rs"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "9b82c4bd1130027a3c123fac82c348ddbab9edc8",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-example/src/name_only_codec.rs"
    },
    {
      "type": "modify",
      "old_id": "72f96bb8e5160b1233cc4aca14bb52b40804f126",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-query-planner-example/README.md",
      "new_id": "66bc451964113aaca1bdf4d5cdb68081ebe158dc",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/README.md"
    },
    {
      "type": "modify",
      "old_id": "d046f67a6c378dabafc18499fa63e2c02c30f55a",
      "old_mode": 33188,
      "old_path": "examples/datafusion-ffi-query-planner-example/python/tests/_test_three_library_query_planner.py",
      "new_id": "c6ef2072a3797869bc85a3c47a596f991a625bf9",
      "new_mode": 33188,
      "new_path": "examples/datafusion-ffi-query-planner-example/python/tests/_test_three_library_query_planner.py"
    },
    {
      "type": "modify",
      "old_id": "2f2cc611911eb8995ecf884b5fe696ef09dfb95c",
      "old_mode": 33188,
      "old_path": "python/datafusion/context.py",
      "new_id": "644c7b445a351759dd81d9e07b9c673c3076dd36",
      "new_mode": 33188,
      "new_path": "python/datafusion/context.py"
    },
    {
      "type": "modify",
      "old_id": "43b53e469f51ff7732e387b9263a260a136d36f8",
      "old_mode": 33188,
      "old_path": "python/datafusion/user_defined.py",
      "new_id": "eafcefdaf969a37e74aa8dfdeecc45a78cd94c63",
      "new_mode": 33188,
      "new_path": "python/datafusion/user_defined.py"
    },
    {
      "type": "modify",
      "old_id": "451f5d21595cbeb8c667c0f070df93571f4f50f6",
      "old_mode": 33188,
      "old_path": "python/tests/test_pickle_expr.py",
      "new_id": "dc55a0767d31190a0cec116ed566514507c7ea4c",
      "new_mode": 33188,
      "new_path": "python/tests/test_pickle_expr.py"
    }
  ]
}
