Uh oh!
There was an error while loading. Please reload this page.
GH-891: Add ExtensionTypeWriterFactory to TransferPair - #892
Conversation
This comment has been minimized.
This comment has been minimized.
7eba2c1 to
7a7e4edComparejhrotko
commented
Oct 23, 2025
Hello, @lidavidm! Could you take a look at this PR? Also, I don't have permissions to change the label |
jbonofre
commented
Oct 24, 2025
@jhrotko I will take a look on this one as soon as the CI is green (it should be good very soon). |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
laurentgo
left a comment
There was a problem hiding this comment.
I'm not really familiar with arrow vectors to be honest, but I wonder why writers aren't discovered at the same time the extension is being registered as a type? wouldn't that make things simpler from an API/usability perspective?
Uh oh!
There was an error while loading. Please reload this page.
This PR changes how we handle extension type writers in Arrow Java. Instead of using factories that get passed around everywhere, we now let the ProblemIn Arrow's type system, each The previous implementation (commits // Usage in ComplexCopierwriter.addExtensionTypeWriterFactory(extensionTypeWriterFactory);
writer.writeExtension(value);In this pattern, each extension type had a separate factory class (like Why the factory pattern wasn't working wellFor developers implementing extension types outside of arrow-java, the situation was even more painful. You had to create and manage two separate classes: one for the type itself ( The factory pattern had several issues that made it difficult to scale at this point. Specially if you wanted to use Extension Arrow-java types mixed with out of arrow-java extension types which is something that might happen more often in the future. The API also got cluttered with factory parameters. Methods like Finally, the factory pattern created tight coupling between the type definition, the writer implementation, the factory that connects them, and all the code that needs to pass factories around. This made it harder to change any one piece without affecting the others. The new approach: Let types provide their own writersI added one abstract method to publicabstractclassExtensionTypeextendsArrowType {
// NEW METHODpublicabstractFieldWritergetNewFieldWriter(ValueVectorvector);
// Other methods...
}publicclassUuidTypeextendsExtensionType {
@OverridepublicFieldWritergetNewFieldWriter(ValueVectorvector) {
returnnewUuidWriterImpl((UuidVector) vector);
}
// Other methods...
}The new approach is simpler because you only need one class per extension type now, not two. The type knows how to create its own writer. This also means the API is cleaner since there are no more factory parameters cluttering everything. For example, This approach is also consistent with how // MinorType enum (existing pattern)publicenumMinorType {
INT(newInt(...)) {
@OverridepublicFieldWritergetNewFieldWriter(ValueVectorvector) {
returnnewIntWriterImpl((IntVector) vector);
}
},
// ...
}
// ExtensionType (new pattern - same idea)publicclassUuidTypeextendsExtensionType {
@OverridepublicFieldWritergetNewFieldWriter(ValueVectorvector) {
returnnewUuidWriterImpl((UuidVector) vector);
}
}Finally, there's less coupling overall. Writers don't need to store or manage factories anymore, TransferPair implementations are simpler, and the type information just flows naturally through the ComplexCopier got simpler// OLD: Required factory parametercaseEXTENSIONTYPE:
if (extensionTypeWriterFactory == null) {
thrownewIllegalArgumentException("Must provide ExtensionTypeWriterFactory");
}
if (reader.isSet()) {
Objectvalue = reader.readObject();
if (value != null) {
writer.addExtensionTypeWriterFactory(extensionTypeWriterFactory);
writer.writeExtension(value);
}
}
...
// NEW: Type provides the writercaseEXTENSIONTYPE:
if (reader.isSet()) {
Objectvalue = reader.readObject();
if (value != null) {
writer.writeExtension(value, reader.getField().getType());
}
}
... |
jhrotko
commented
Nov 7, 2025
lidavidm
left a comment
There was a problem hiding this comment.
From a brief glance this approach seems more reasonable
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| protected ArrowType lastExtensionType; | ||
| @Override | ||
| public void writeExtension(Object value) { |
There was a problem hiding this comment.
(design) should we deprecate this method? (since we now have writeExtension(ExtensionHolder)
There was a problem hiding this comment.
I am not sure if it should be deprecated, looking at other implementations they usually offer the writeX(X arg) ex.: writeInt, and write(XHolder holder)
There was a problem hiding this comment.
I believe they do when there's no confusion about type/representation. But here we are relying on lastExtensionType to be set first via getWriter()
There was a problem hiding this comment.
what is the issue with lastExtensionType state?
There was a problem hiding this comment.
It does make for a very confusing API; there have been other issues/PRs filed about similar cases. At the very least this must detect and throw an explanatory exception for this case.
Also, it would be good to have an override that lets you supply the extension type so that there's no ambiguity or potential for hard-to-diagnose runtime issues (what if a refactoring in some other part of the code eliminates the getWriter call and now your code is suddenly throwing?). Other types (like decimal) have overrides that let you supply the type, so I think this should too.
There was a problem hiding this comment.
Yes, though now I have to question why the type has to be provided twice.
There was a problem hiding this comment.
Is there not a way to stash the extension type instance?
I believe they do when there's no confusion about type/representation. But here we are relying on lastExtensionType to be set first via getWriter()
It seems even before it implicitly was stashed somehow? Is there not a way we can explicitly stash it? (I'd also be curious why this didn't seem to apply to other parametrized types?)
There was a problem hiding this comment.
Yes, though now I have to question why the type has to be provided twice.
It's for support UnionVector/Writer - because Union could use different vectors inside and it's possible that several impl's of ExtensionType could be used at the same time. From Arrow perspective, they all will be ExtensionVector/Writer - so for the correct determination of the extensionWriter should pass the ArrowType that will be unique
There was a problem hiding this comment.
Ah...I'd kind of argue that was a weird decision by the union writer (it really shouldn't assume types correspond to type codes (also see #108)), but I guess at this point it's unavoidable, so this API will have to do.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
This file is part of the 18.3.0 release, so removing it would be a breaking change. We could have a discussion if it okay or not. If it is, maybe we can be a bit more decisive on some other methods (like PromotableWriter#writeExtension(Object)) but otherwise, file need to be kept with a @Deprecated annotation
There was a problem hiding this comment.
If we decide to move forward with this design it's going to be a breaking change because the factory pattern was completely replaced, not deprecated alongside the new pattern. This will require users to migrate. Fortunately, the migration will be easy: Extension types must implement getNewFieldWriter() method and Extension holders need to implement the type() method as well and remove all factory references. I can provide a better migration guide in the PR description
There was a problem hiding this comment.
If we're doing a major bump anyways it would be a good chance to improve things.
There was a problem hiding this comment.
Added migration steps in PR description
There was a problem hiding this comment.
@laurentgo as you previously suggested I created a thread in mailling dev: https://lists.apache.org/thread/dqfjdvh2owln3gw4tfcmp05rdmqk7hhg
lidavidm
commented
Nov 11, 2025
@jarohen does XTDB use extension types? |
Uh oh!
There was an error while loading. Please reload this page.
jarohen
commented
Nov 13, 2025
@lidavidm it does, but we have our own mechanisms for that outside of arrow-java I'm afraid. We're mostly using arrow-java for the IPC and memory management these days - we needed too many bespoke access patterns of the vectors themselves (particularly DUV) and didn't feel it reasonable to expect you folks to bend over backwards just for us 😄 That said, XT's all open source, feel free to pinch what you like, and I'm more'n happy to talk more about it (maybe a different thread though), if there's anything we can contribute back 🙂 |
lidavidm
commented
Nov 13, 2025
Thanks for the confirmation! Just wanted to evaluate how this might affect you if we went ahead, sounds like it wouldn't be a problem |
jhrotko
commented
Jan 5, 2026
@lidavidm I see that the CI failures are unrelated to the changes and other PRs are having the same issues |
lidavidm
left a comment
There was a problem hiding this comment.
@laurentgo@jbonofre do we want to make this part of the next release (and call it 19.0.0)?
jbonofre
commented
Jan 6, 2026
@lidavidm yes, I would like to include in the 19.0.0 Arrow Java release (as soon as CI will be green 😄 ). |
lidavidm
commented
Jan 7, 2026
Ok, I assume we'll wait for CI to be fixed, then we can rebase this. |
jbonofre
commented
Jan 8, 2026
CI should be OK now. Thanks @jhrotko for the rebase, I just triggered a build. |
jhrotko
commented
Jan 9, 2026
jhrotko
commented
Jan 12, 2026
Uh oh!
There was an error while loading. Please reload this page.
jhrotko
commented
Jan 15, 2026
@jbonofre@lidavidm@laurentgo@xxlaykxx thank you so much for the reviews and support! |
What's Changed
This PR simplifies extension type writer creation by moving from a factory-based pattern to a type-based pattern. Instead of passing
ExtensionTypeWriterFactoryinstances through multiple API layers, extension types now provide their own writers via a newgetNewFieldWriter()method onArrowType.ExtensionType.getNewFieldWriter(ValueVector)abstract method toArrowType.ExtensionTypeExtensionTypeWriterFactoryinterface and all implementationsComplexCopier,PromotableWriter, andTransferPairAPIsUnionWriterto support extension types (previously threwUnsupportedOperationException)UuidType,OpaqueType)The factory pattern didn't scale well. Each new extension type required creating a separate factory class and passing it through multiple API layers. This was especially painful for external developers who had to maintain two classes per extension type and manage factory parameters everywhere.
The new approach follows the same pattern as
MinorType, where each type knows how to create its own writer. This reduces boilerplate, simplifies the API, and makes it easier to implement custom extension types outside arrow-java.Breaking Changes
ExtensionTypeWriterFactoryhas been removedgetNewFieldWriter(ValueVector vector)methodtype()which returns theExtensionTypefor that HolderMigration Guide
getNewFieldWriter(ValueVector vector)methodtype()which returns theExtensionTypefor that HolderHow to use Extension Writers?
Before:
After:
Also
copyAsValuedoes not need to provide the factory anymore.Closes#891 .