Skip to content

feat(pg): asDomain() + PgRecordParser fixes + cross-check vs live PG + fuzz#46

Merged
oyvindberg merged 4 commits into
mainfrom
feature/asdomain
May 2, 2026
Merged

feat(pg): asDomain() + PgRecordParser fixes + cross-check vs live PG + fuzz#46
oyvindberg merged 4 commits into
mainfrom
feature/asdomain

Conversation

@oyvindberg
Copy link
Copy Markdown
Contributor

@oyvindberg oyvindberg commented May 2, 2026

Summary

  • PgType.asDomain(name) + combined asDomain(name, f, g) for the JDBC view of a PG CREATE DOMAIN. Renames the typename, registers the underlying type as a query-analyzer alias (PG JDBC resolves domains in ResultSetMetaData), 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.
  • Two parser bugs fixed in PgType.array() + PgRecordParser:
    • array() was hardcoding , as the resulting type's arrayDelimiter (broke box[]); now propagates the original delimiter end-to-end through readCompositeList.
    • parseUnquotedArrayElement didn't track {} brace depth or \"…\" quoted-string state (broke multi-dim dom_jsonb[][], text[][], etc.).
  • Kotlin and Scala wrappers expose .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 shared PgArrayParseCases corpus, 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 its castSqlType so PG's textual array semantics aren't ambiguous.

Domains

  • bleep test foundations-jdbc-test -o dev.typr.foundations.PgDomainTest — 12 passing. Covers the user-pattern Name.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 a PgType<T>, threaded through:

    • scalar
    • 1-D array singleton
    • 1-D array multi-element (mixed with \"NULL\" and \"{a,b};\\\"\\\\\" siblings to force per-element disambiguation)
    • asDomain wrapper (scalar + array)
    • single-field composite (different encode path: PgRecordParser.parse)
    • two-field composite (sibling field independently adversarial)
    • array of composite (two layers of escape rules composing)
    • composite with array field (inverse layering)

    Corpus covers: empty/whitespace, every PG-significant punctuation (,;:|::{}()[]), every quote/backslash combo (incl. escape lookalikes), every case of NULL/null/nUlL lookalikes, 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 an asDomain variant 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

  • Domain variants drop hasIdentity unconditionally — 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.
  • ENUM entries marked .noCallable() because PG won't promote varchar → enum at 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.
  • One bare-nested case ({{\"{\\\\\"k\\\\\": 1}\"},{\"{\\\\\"k\\\\\": 2}\"}} — the canonical jsonb[][] shape) is parser-only against PG's text[] 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

oyvindberg and others added 4 commits May 2, 2026 18:45
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>
@oyvindberg oyvindberg changed the title feat(pg): add asDomain() for PostgreSQL DOMAIN types feat(pg): asDomain() + PgRecordParser fixes + cross-check vs live PG + fuzz May 2, 2026
@oyvindberg oyvindberg merged commit 09ca66a into main May 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant