feat(pg): asDomain() + PgRecordParser fixes + cross-check vs live PG + fuzz#46
Merged
Conversation
PgType.asDomain(name) renames the typename for SQL rendering and switches
the array codec to text-parsing so domain arrays decode correctly when PG
JDBC's ResultSetMetaData resolves domains to their underlying type. It
also registers the underlying typename as a query-analyzer alias so the
match succeeds without further user opt-in.
Two parser bugs surfaced and fixed along the way:
- PgType.array() hardcoded ',' as the new type's arrayDelimiter, breaking
geometric arrays like box[] (which use ';'). Now propagates the original
delimiter end-to-end through readCompositeList.
- PgRecordParser.parseUnquotedArrayElement didn't track {} brace depth or
"..." quoted-string state, breaking multi-dim arrays whose elements are
inline {...} sub-arrays containing commas (jsonb[][], text[][], etc.).
Test coverage:
- New PgDomainTest with the user-facing Name + .array().to(Bijection)
pattern, plus 27 representative scalar+array roundtrips, plus targeted
cases for ENUM, COMPOSITE, CHECK violation, NOT NULL violation,
Optional<DomainType>, domain-over-domain, and domain-as-composite-field.
- PgTypeTest matrix extended: every Element now spawns a parallel domain
variant (auto via domainEntry); ENUM and COMPOSITE entries added with
their setup DDL carried in the entry; ensureDomain runs all setup
inside the rollback-only transaction. Catch sites switched to a
causeChain helper so finally-block "transaction aborted" errors don't
mask the real root cause.
- PgRecordParserTest +21 focused tests for parseArray (scalar, quoted,
bare-nested, multi-level, custom delimiter, quoted-JSON leaves) plus
roundtrip property tests covering every value class encodeArray quotes.
Kotlin/Scala wrappers expose .asDomain(name). Docs updated with a new
PostgreSQL DOMAIN section and snippets in all three languages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Site validator allows only one //start marker per snippet file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The point of asDomain was to wrap the type once at the scalar level so
.array() and the rest of the type DSL carry the wrapper through. Doing
asDomain then transform as separate calls is two operations for what
should be one declaration. The new overload takes the constructor /
extractor inline:
PgTypes.text.asDomain("person_name", Name::new, Name::value)
Doc snippets updated: scalar uses the combined form; array example just
calls .array() on the wrapped scalar (no list-level bijection needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fuzz Two new test classes pinned to the same shared corpus (PgArrayParseCases): PgRecordParserAgreementTest — for every parseArray case, ask PG to interpret the same string under the case's named cast (text[] for the 1-D and bare-nested set, box[] for the ';' delim set) and assert PG and our parser agree on the element list / dimensions / top-level count. The shared corpus also drives the existing pure-unit assertions in PgRecordParserTest, so a new case is verified twice (parser self-consistent, parser matches PG) with one corpus entry. PgFuzzRoundtripTest — ~60 adversarial strings paired explicitly with a PgType<T>, threaded through scalar / 1-D array singleton / 1-D array multi-element / asDomain wrapper / single-field composite / two-field composite / array-of-composite / composite-with-array-field. Asserts the read-back value equals the original. Corpus covers empty/whitespace, every PG-significant punctuation, every quote/backslash combo, NULL/null literal lookalikes, control characters (CR LF TAB VT FF BEL DEL), multi-byte UTF-8, emoji (single + ZWJ family + skin-tone), RTL Arabic and Hebrew, CJK, combining marks, ZWJ + ZWSP + BOM mid-string, soft hyphen, NBSP, supplementary plane, 100x-repeated braces and \". Also: PgType.asDomain(name, f, g) overload — combined rename + transform so the value-class wrapping is in place at the scalar level before .array() runs (the original point of asDomain). Kotlin and Scala wrappers expose it. Doc snippets updated to use the combined form and demonstrate that arrays "just work" with no list-level bijection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PgType.asDomain(name)+ combinedasDomain(name, f, g)for the JDBC view of a PGCREATE DOMAIN. Renames the typename, registers the underlying type as a query-analyzer alias (PG JDBC resolves domains inResultSetMetaData), and switches the array codec to text-parsing so domain arrays decode correctly. The combined overload wraps once at the scalar level so.array()carries the wrapper through with no list-level bijection.PgType.array()+PgRecordParser:array()was hardcoding,as the resulting type'sarrayDelimiter(brokebox[]); now propagates the original delimiter end-to-end throughreadCompositeList.parseUnquotedArrayElementdidn't track{}brace depth or\"…\"quoted-string state (broke multi-dimdom_jsonb[][],text[][], etc.)..asDomain(...)(both forms). PostgreSQL docs + snippets in all three languages updated.Test plan
Library
bleep compile foundations-jdbc foundations-jdbc-hikari foundations-jdbc-spring foundations-jdbc-kotlin foundations-jdbc-scala documentation-examples-java documentation-examples-kotlin documentation-examples-scala— green.Pure-unit parser
bleep test foundations-jdbc-test -o dev.typr.foundations.PgRecordParserTest— 40 passing, with a new shared corpus covering 1-D, bare-nested (incl. multi-level + JSON-leaf + backslash-escaped quotes), and;-delim cases.Cross-check against live PG
bleep test foundations-jdbc-test -o dev.typr.foundations.PgRecordParserAgreementTest— for every entry in the sharedPgArrayParseCasescorpus, ask PG to parse the same string under the case's named cast (text[]for 1-D/bare-nested,box[]for;-delim) and assert agreement on element list, dimensions, and top-level count. Each case explicitly carries itscastSqlTypeso PG's textual array semantics aren't ambiguous.Domains
bleep test foundations-jdbc-test -o dev.typr.foundations.PgDomainTest— 12 passing. Covers the user-patternName.pgType.array().to(Bijection), ~27 representative scalar+array roundtrips, ENUM, COMPOSITE, CHECK violation, NOT NULL violation,Optional<DomainType>, domain-over-domain, and domain-as-composite-field.Adversarial roundtrip fuzz
bleep test foundations-jdbc-test -o dev.typr.foundations.PgFuzzRoundtripTest— 7 passing. ~60 adversarial strings paired explicitly to aPgType<T>, threaded through:\"NULL\"and\"{a,b};\\\"\\\\\"siblings to force per-element disambiguation)asDomainwrapper (scalar + array)PgRecordParser.parse)Corpus covers: empty/whitespace, every PG-significant punctuation (
,;:|::{}()[]), every quote/backslash combo (incl. escape lookalikes), every case ofNULL/null/nUlLlookalikes, control characters (CR LF TAB VT FF BEL DEL), multi-byte UTF-8 (®, emoji, ZWJ family, skin-tone), RTL Arabic + Hebrew, CJK, combining marks, ZWJ + ZWSP + BOM mid-string, soft hyphen, NBSP, supplementary plane (4-byte UTF-8), 100x-repeated braces, 100x-repeated\\\".Full PG matrix
bleep test foundations-jdbc-test -o dev.typr.foundations.PgTypeTest— every Element runs as both itself and as anasDomainvariant through native + streaming + JSON in-mem + JSON DB + composite + function + procedure + OUT + INOUT + analysis. ENUM + COMPOSITE entries added with their setup DDL carried in the entry.Notes
hasIdentityunconditionally — the base entry already proves the underlying's=operator works, and PG does not always define=on a domain in its own right (domain-over-enum has no operator class). Documented in the matrix..noCallable()because PG won't promotevarchar → enumat procedure-overload resolution. COMPOSITE marked.noStreaming()because COPY-text encoding splits inside composite literals at the comma. Both are pre-existing library limitations the matrix now surfaces explicitly.{{\"{\\\\\"k\\\\\": 1}\"},{\"{\\\\\"k\\\\\": 2}\"}}— the canonicaljsonb[][]shape) is parser-only against PG'stext[]cast: PG's rectangularity check tries to descend into the inner{\"k\":...}thinking it's another array dimension. Documented in the corpus and skipped on the PG cross-check.🤖 Generated with Claude Code