Custom Codec Registry: Interface-Based Design #147
Closed
SeanTAllen
started this conversation in
Research
Replies: 1 comment
-
|
Imeplementation plan: #148 |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Custom Codec Registry: Interface-Based Design
Discussion #146 explored three options for making the codec registry user-extensible. After analysis, none of them are the right answer. Here's the approach we're going with.
The Core Problem (quick recap)
FieldDataTypesis a closed union. Custom codecs can only return types already in the union, which makes them nearly useless. The best you can do is decode an unknown OID toStringinstead ofArray[U8] val. That's Option B from #146, and it's a mediocre API.Option A (wrapping custom types in
CustomFieldDatawithAny valinside) gives users two-level matching: first match onCustomFieldData, then cast the inner value. That's ugly and defeats the purpose of Pony's type system. It also has a concrete correctness problem:_FrontendMessage.bind()'s size-calculation pass would silently skipCustomFieldDatavalues, producing corrupted wire messages.The Actual Answer: Separate Result Types from Parameter Types
Results and parameters have different semantics. Parameters need a closed set of types because the encode path (
_FrontendMessage.bind(),_ParamEncoder) matches on Pony types to derive OIDs and produce wire bytes. Results don't have that constraint. The server tells us the OID and format code, the codec decodes the bytes, and the user gets a typed value. There's no reason the decode side needs to be locked to the same closed union as the encode side.So we use different types for each direction:
FieldDataTypes(the existing closed union). The encode path is completely unchanged.FieldDatainterface (an open type that anyvalwithstring()can satisfy).All 13 existing types already have
string()viaStringable, so they conform structurally.Field.valuebecomesFieldDatainstead ofFieldDataTypes.Codec.decode()returnsFieldData. Custom codecs return their own types directly.This is a breaking change for any code that explicitly references
FieldDataTypesas the type of a result field value. Code that just matches onfield.value(which is all the examples and every consumer I've seen) continues to work unchanged.What This Looks Like for Users
A custom codec for PostgreSQL
point(OID 600):Registration and use:
Users match on their concrete types directly:
No wrappers. No casting. Flat matching, same pattern as built-in types.
Field.eq()
Field.eq()currently has explicit arms for all 13 built-in types, withelse false. Those arms stay and work exactly as they do today.For custom types, we add an opt-in equality interface:
Custom types that want equality comparison implement
FieldDataEquatable. Theelsebranch inField.eq()checks for this interface before falling tofalse. Built-in types don't need to change. They're handled by the existing explicit arms.CodecRegistry API
_with_codecbecomes a public method (not a constructor) for fluent chaining:Sessiongets an optionalCodecRegistryparameter with a default:The existing fallback behavior is unchanged: unknown text-format OIDs decode to
String, unknown binary-format OIDs toArray[U8] val.What's Deferred
Custom parameter encoding. The encode path (
_FrontendMessage.bind(),_ParamEncoder) matches on Pony types from theFieldDataTypesunion to derive OIDs and produce wire bytes. Making that extensible would require either refactoringbind()to delegate to the codec registry, or introducing a new parameter type that carries an OID alongside the value. Both are significant changes. Users who need to send values for custom PostgreSQL types can useStringwith text format today. Custom parameter encoding gets its own design work when there's a concrete use case driving it.Codec interface split. The
Codecinterface currently has bothencode()anddecode(). Decode-only custom codecs would implementencode()aserror. Splitting into separateDecoder/Encoderinterfaces would be cleaner. It's a minor wart for now.Phase 3 Scope
FieldDatainterfaceFieldDataEquatableinterface for opt-in custom type equalityCodec.decode()return type changes fromFieldDataTypestoFieldDataCodecRegistry.decode()return type changes toFieldDataField.valuetype changes toFieldDataField.eq()updated withFieldDataEquatablecheck in else branch_with_codecrenamed towith_codecas a public method (not constructor) for fluent chainingSessionconstructor gets optionalCodecRegistryparameter, threaded through the state machine to_SessionLoggedInexamples/custom-codec/example +examples/README.mdupdatechanged)Design origin: discussion #146
Beta Was this translation helpful? Give feedback.
All reactions