PostgreSQL Array Type Support #155
Replies: 2 comments
-
Open Question Resolutions1. Text format whitespace toleranceDecision: strict parsing. PostgreSQL's text array output is well-defined ( 2. Empty array element_oid from serverDecision: use header value, fall back to mapped OID if 0. Verify empirically during integration testing with 3. Numeric array encodeDecision: implement This adds a step to the plan: implement numeric binary encode (parsing decimal string → PostgreSQL base-10000 digit format). The format is the inverse of the existing decode logic in This also requires tests: roundtrip for positive/negative integers, fractional values, zero, NaN, Infinity/-Infinity, and values with trailing zeros (dscale preservation). Integration test: INSERT a numeric value via PreparedQuery parameter, SELECT it back, verify equality. 4. Custom array types and _ParamEncoderDecision: make The change flows naturally: Updated implementation steps affected:
|
Beta Was this translation helpful? Give feedback.
-
|
Implemented in #156 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Scope
1-dimensional arrays only. Multi-dimensional arrays are deferred to a future design. This covers both decoding arrays from query results and encoding arrays as query parameters.
PgArray Type
A single
PgArraytype serves both the decode path (result fields) and the encode path (query parameters). This matches the existing pattern wherePgTimestamp,PgDate,PgTime, andPgIntervalall use one type for both paths.Fields:
element_oid: U32— the PostgreSQL OID of the element type (e.g., 23 for int4). Public field, needed for encoding and useful for users to inspect what type of array they received.elements: Array[(FieldData | None)] val— ordered element values.Nonerepresents SQL NULL. Uses the openFieldDatainterface so decoded arrays can contain any codec-produced type, including custom codec types.Methods:
create(element_oid', elements')— non-partial constructor. Validation happens at encode time (same pattern asPreparedQuery— accepts any params, validates at bind time).size(): USize— number of elements.apply(i: USize): (FieldData | None) ?— indexed access.eq(that: box->PgArray): Bool— element-wise equality using shared_FieldDataEqhelper.field_data_eq(that: FieldData box): Bool—FieldDataEquatableimplementation.string(): String iso^— PostgreSQL array literal format:{1,2,NULL,4}.Usage:
FieldDataTypes Addition
Add
PgArrayto the closed union. This is NOT a recursive union —PgArraystoresArray[(FieldData | None)] valwhereFieldDatais a separate interface fromFieldDataTypes.All existing
Codec.encode()implementations haveelse errorbranches, so they'll correctly rejectPgArraywithout modification.Element Equality Helper
Extract a shared
_FieldDataEqprimitive from the existingField.eq()logic. BothField.eq()andPgArray.eq()need element-level equality for(FieldData | None)values. Duplicating the match arms would be a maintenance hazard.Field.eq()is refactored to delegate to_FieldDataEq(value, that.value)after the name check.PgArraygets an explicit match arm in_FieldDataEq(for consistency with all other built-in types, even thoughFieldDataEquatablecatch-all would also work).Array OID Map
_ArrayOidMap— a package-private primitive with static bidirectional mapping between element OIDs and array OIDs (23 entries covering all built-in types with registered codecs).Methods:
element_oid_for(array_oid): U32 ?,array_oid_for(element_oid): U32 ?,is_array_oid(oid): Bool.Codec Architecture: Registry-Level Handling
Arrays are NOT implemented via the
Codecinterface. TheCodecinterface is designed for self-contained codecs —decode(data)takes only raw bytes. Array decoding requires the registry itself to decode elements recursively. Rather than breaking the publicCodecinterface (adding a registry parameter) or creating class-val codecs with circularity/stale-reference problems,CodecRegistry.decode()intercepts array OIDs before looking up per-OID codecs.Performance: The interception adds one
_ArrayOidMap.is_array_oid()call (a match that falls through immediately for non-array OIDs) to the decode hot path. This is ~2ns per column — negligible compared to the hash map lookup that follows.CodecRegistry Changes
_custom_array_element_oids: Map[U32, U32] valfield — maps custom array OIDs to element OIDs for text decode of user-registered typeswith_array_type(array_oid, element_oid): CodecRegistrymethod — registers a custom array type mapping, chainable withwith_codec()decode()to check_ArrayOidMapthen_custom_array_element_oidsbefore normal codec dispatch_decode_binary_array()and_decode_text_array()private methodshas_binary_codec()to return true for known array OIDs (built-in + custom)_with_codecconstructor to copy_custom_array_element_oids_decode_binary_arraycallsself.decode()for elements, which works becauseCodecRegistryisvaland these are non-mutating method callsBinary Array Decode
PostgreSQL binary array wire format:
The element OID from the binary header is used as-is (authoritative). This correctly handles domain types where the server reports the base element OID.
Security validations:
ndim < 0orndim > 1→ errorhas_nullnot 0 or 1 → errordimension_size < 0→ errordimension_size * 4 > remaining_bytes→ error (prevents allocation bombs; uses checked arithmetic viamul_partial)element_length < -1→ error (only -1 and >= 0 are valid)offset != data.size()→ error (trailing garbage)Elements decoded via recursive
self.decode(header_element_oid, 1, elem_data). Element extraction usesdata.trim()which is O(1) zero-copy onvalarrays.Text Array Decode
Iterative parser (no recursion → no stack overflow risk). Element OID comes from
_ArrayOidMap.element_oid_for()since text wire data does not carry it.Handles:
{}— empty array{1,2,3}— simple elements{1,NULL,3}— NULL elements (case-insensitive unquotedNULL){"hello, world","with \"quotes\""}— quoted elements with backslash escaping{""}— empty string element (distinct from NULL){{1,2},{3,4}}— detected and rejected (multi-dimensional)Elements decoded via
self.decode(element_oid, 0, raw).Encode Path
_ParamEncoder Changes
Add
PgArrayarm:_ArrayOidMap.array_oid_for(a.element_oid), falling back toU32(0)(server infers) for unknown element OIDs._FrontendMessage.bind() Changes
registry: CodecRegistryparameter (no default — all call sites explicitly pass the session's registry)recover valblock, pre-encode allPgArrayparameters intoArray[U8] valvia_ArrayEncoder. Store in avalarray that can enter the recover block. This avoids needing codec access inside recover and avoids double-encoding.params_data_sizecalculation loop — use pre-encoded byte array's sizeSession.pony Call Sites
All 6
_FrontendMessage.bind()call sites insession.ponyupdated to passli.codec_registry. This ensures custom array types registered viawith_array_type()work correctly._ArrayEncoder Primitive
Encodes
PgArrayto binary array wire format. Dispatches element encoding on Pony runtime types (matching the pattern_FrontendMessage.bind()already uses for scalars). String elements routed by element_oid: uuid →_UuidBinaryCodec, jsonb →_JsonbBinaryCodec, oid →_OidBinaryCodec; all others → raw UTF-8 bytes.Coupling comment on
_ArrayEncoder: element encoding must stay in sync with_FrontendMessage.bind()and_binary_codecs.pony— changes to scalar binary encoding must be mirrored here.Changes to Existing Files
field_data_types.ponyPgArrayto unionfield_data.ponyPgArrayto docstring's built-in type listfield.pony_FieldDataEq; add PgArray to docstringcodec_registry.pony_param_encoder.pony_frontend_message.ponysession.pony_test_frontend_message.pony_test_codecs.pony_test.ponypostgres.ponyexamples/README.mdCLAUDE.mdNew Files
pg_array.ponyPgArrayclass_array_oid_map.pony_ArrayOidMapprimitive_array_encoder.pony_ArrayEncoderprimitive_field_data_eq.pony_FieldDataEqprimitive_test_array.ponyexamples/array/array-example.ponyImplementation Steps
_ArrayOidMapprimitive_FieldDataEqprimitiveField.eq()to delegate to_FieldDataEqPgArraytypePgArraytoFieldDataTypes_ArrayEncoderprimitive_decode_binary_array,_decode_text_array,_custom_array_element_oids,with_array_type()toCodecRegistry; updatedecode()andhas_binary_codec()_ParamEncoder.oids_for()withPgArrayarm_FrontendMessage.bind()— addregistryparameter, pre-encoding, all three match loopsbind()call sites — 6 insession.pony, 2 in_test_frontend_message.pony, 4 in_test_codecs.pony_test_array.pony) — binary/text decode roundtrip per element type, NULL handling, empty arrays, validation errors, PgArray equality, _ParamEncoder, bind() wire format, property-based roundtrip tests_test.pony_test_array.pony) — SELECT/INSERT arrays via PreparedQuery and SimpleQuery, multiple element types, roundtripexamples/array/array-example.pony)examples/README.mdpostgres.ponypackage docstringCLAUDE.md/pony-release-notesand create release notes file — classification: added, label:changelog - addedTest Strategy
Unit Tests
Property-Based Tests (PonyCheck)
Integration Tests (requires PostgreSQL)
SELECT ARRAY[1,2,3]::int4[]via PreparedQuery (binary decode)SELECT ARRAY[1,2,3]::int4[]via SimpleQuery (text decode)SELECT $1::int4[]with PgArray parameter (encode roundtrip)Test Commands
Open Questions
Text format whitespace tolerance — PostgreSQL output is well-defined, but should the parser tolerate leading/trailing whitespace? Proposal: strict parsing matching server output. Adjust if integration tests reveal issues.
Empty array element_oid from server — Does PostgreSQL 14.5 always include a valid element_oid in the binary header for empty arrays? Proposal: use header value, fall back to mapped OID if header says 0. Verify empirically during integration testing.
Numeric array encode —
_NumericBinaryCodec.encode()currently stubs witherror. This meansnumeric[]parameters will fail at encode time. Acceptable for v1? (Numeric decode works fine.)Custom array types and _ParamEncoder —
_ParamEncoderuses_ArrayOidMap(static). For custom element OIDs, it falls back to OID 0 (server infers). Acceptable, or should_ParamEncoderbecome registry-aware?Beta Was this translation helpful? Give feedback.
All reactions