Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.typr.foundations.docs.postgresql;

import dev.typr.foundations.PgType;
import dev.typr.foundations.PgTypes;
import java.util.List;

@SuppressWarnings("unused")
public class PgDomainTypeArray {
// start
// Wrap once at the scalar level — the array codec carries the wrapper through
// .array(), so no list-level bijection is needed.
public record Name(String value) {
public static final PgType<Name> pgType =
PgTypes.text.asDomain("person_name", Name::new, Name::value);

public static final PgType<List<Name>> pgArrayType = pgType.array();
}
// stop
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.typr.foundations.docs.postgresql;

import dev.typr.foundations.PgType;
import dev.typr.foundations.PgTypes;

@SuppressWarnings("unused")
public class PgDomainTypeScalar {
// start
// PG schema: CREATE DOMAIN person_name AS varchar(100);
public record Name(String value) {
public static final PgType<Name> pgType =
PgTypes.text.asDomain("person_name", Name::new, Name::value);
}
// stop
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.typr.foundationskt.docs.postgresql

import dev.typr.foundationskt.*

@Suppress("unused")
class PgDomainTypeArray {
//start
// Wrap once at the scalar level — the array codec carries the wrapper through
// .array(), so no list-level bijection is needed.
data class Name(val value: String) {
companion object {
val pgType: PgType<Name> =
PgTypes.text.asDomain("person_name", ::Name, Name::value)
val pgArrayType: PgType<List<Name>> = pgType.array()
}
}
//stop
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.typr.foundationskt.docs.postgresql

import dev.typr.foundationskt.*

@Suppress("unused")
class PgDomainTypeScalar {
//start
// PG schema: CREATE DOMAIN person_name AS varchar(100);
data class Name(val value: String) {
companion object {
val pgType: PgType<Name> =
PgTypes.text.asDomain("person_name", ::Name, Name::value)
}
}
//stop
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.typr.foundationssc.docs.postgresql
import dev.typr.foundationssc.*

@SuppressWarnings(Array("unused"))
object PgDomainTypeArray:
// start
// Wrap once at the scalar level — the array codec carries the wrapper through
// .array, so no list-level bijection is needed.
case class Name(value: String)
object Name:
val pgType: PgType[Name] =
PgTypes.text.asDomain("person_name", Name.apply, _.value)
val pgArrayType: PgType[List[Name]] = pgType.array
// stop
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.typr.foundationssc.docs.postgresql
import dev.typr.foundationssc.*

@SuppressWarnings(Array("unused"))
object PgDomainTypeScalar:
// start
// PG schema: CREATE DOMAIN person_name AS varchar(100);
case class Name(value: String)
object Name:
val pgType: PgType[Name] =
PgTypes.text.asDomain("person_name", Name.apply, _.value)
// stop
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ class PgType<T>(override val underlying: dev.typr.foundations.PgType<T>) : DbTyp
fun renamed(value: String): PgType<T> = PgType(underlying.renamed(value))
fun renamedDropPrecision(value: String): PgType<T> = PgType(underlying.renamedDropPrecision(value))

/** Reinterpret as the JDBC view of a PG DOMAIN — see [dev.typr.foundations.PgType.asDomain]. */
fun asDomain(domainName: String): PgType<T> = PgType(underlying.asDomain(domainName))

/** Combined [asDomain] + [transform] — wrap the domain in a typed value class in one call. */
fun <B> asDomain(domainName: String, f: (T) -> B, g: (B) -> T): PgType<B> =
PgType(underlying.asDomain(domainName, dev.typr.foundations.SqlFunction { f(it) }, g))

fun withRead(read: PgRead<T>): PgType<T> = PgType(underlying.withRead(read))
fun withWrite(write: PgWrite<T>): PgType<T> = PgType(underlying.withWrite(write))
fun withText(text: PgText<T>): PgType<T> = PgType(underlying.withText(text))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ class PgType[T](override val underlying: dev.typr.foundations.PgType[T]) extends
def renamed(value: String): PgType[T] = PgType(underlying.renamed(value))
def renamedDropPrecision(value: String): PgType[T] = PgType(underlying.renamedDropPrecision(value))

/** Reinterpret as the JDBC view of a PG DOMAIN — see `dev.typr.foundations.PgType.asDomain`. */
def asDomain(domainName: String): PgType[T] = PgType(underlying.asDomain(domainName))

/** Combined `asDomain` + `transform` — wrap the domain in a typed value class in one call. */
def asDomain[B](domainName: String, f: T => B, g: B => T): PgType[B] =
PgType(underlying.asDomain(domainName, v => f(v), v => g(v)))

def withRead(read: PgRead[T]): PgType[T] = PgType(underlying.withRead(read))
def withWrite(write: PgWrite[T]): PgType[T] = PgType(underlying.withWrite(write))
def withText(text: PgText[T]): PgType[T] = PgType(underlying.withText(text))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package dev.typr.foundations;

import java.util.Arrays;
import java.util.List;

/**
* Shared test inputs for {@link PgRecordParser#parseArray} — consumed by both the pure-unit
* {@link PgRecordParserTest} (parser self-consistency) and {@link PgRecordParserAgreementTest}
* (parser-vs-live-PG cross-check). Single source of truth so both paths exercise exactly the
* same strings.
*/
public final class PgArrayParseCases {

/**
* One parseArray expectation. The PG textual array form is type-dependent (delimiter, quoting,
* how PG canonicalises elements) — so each case names the {@code castSqlType} PG should
* interpret it as. The parser still operates purely textually with {@code delimiter}; PG runs
* {@code SELECT $input::castSqlType} and we cross-check.
*
* <p>{@code pgVerify=false} skips the PG cross-check for cases PG can't accept under any
* sensible array cast (e.g. jsonb-leaf bare-nested confuses PG's text[] rectangularity check).
*/
public record Case(
String input,
char delimiter,
String castSqlType,
List<String> expected,
boolean pgVerify) {}

private static Case textArr(String input, List<String> expected) {
return new Case(input, ',', "text[]", expected, true);
}

private static Case boxArr(String input, List<String> expected) {
return new Case(input, ';', "box[]", expected, true);
}

/** Parser-only — PG rejects this literal under any sensible array cast. */
private static Case parserOnly(String input, List<String> expected) {
return new Case(input, ',', "text[]", expected, false);
}

/** 1-D, comma-delimited cases — the bulk of real-world array text PG emits as text[]. */
public static final List<Case> ONE_DIM =
List.of(
textArr("{}", List.of()),
textArr("{1,2,3}", List.of("1", "2", "3")),
textArr("{a,b,c}", List.of("a", "b", "c")),
textArr("{42}", List.of("42")),
textArr("{\"hello\"}", List.of("hello")),
textArr("{NULL}", Arrays.asList((String) null)),
textArr("{a,NULL,c}", Arrays.asList("a", null, "c")),
textArr("{NULL,NULL}", Arrays.asList(null, null)),
textArr("{\"a,b\",c}", List.of("a,b", "c")),
textArr("{\"hello, world\",\"foo, bar\"}", List.of("hello, world", "foo, bar")),
textArr("{\"{not nested}\"}", List.of("{not nested}")),
textArr("{\"{a,b}\",c}", List.of("{a,b}", "c")),
textArr("{\"{\\\"k\\\": 1}\"}", List.of("{\"k\": 1}")),
textArr(
"{\"{\\\"a\\\": 1}\",\"{\\\"b\\\": 2}\"}", List.of("{\"a\": 1}", "{\"b\": 2}")),
textArr("{\"[1,10)\"}", List.of("[1,10)")),
textArr("{\"[1,10)\",\"[20,30)\"}", List.of("[1,10)", "[20,30)")),
textArr(" {1,2,3} ", List.of("1", "2", "3")));

/**
* Bare-nested multi-dim cases — at the top level we expect each {@code {…}} sub-array as a
* single element (re-parse to descend). PG sees these as rectangular N-dim arrays under the
* text[] cast.
*/
public static final List<Case> BARE_NESTED =
List.of(
textArr("{{1,2},{3,4}}", List.of("{1,2}", "{3,4}")),
textArr("{{a,b,c}}", List.of("{a,b,c}")),
textArr("{{1}}", List.of("{1}")),
textArr("{{a,b},c,d}", List.of("{a,b}", "c", "d")),
textArr("{a,{b,c},d}", List.of("a", "{b,c}", "d")),
textArr("{a,b,{c,d}}", List.of("a", "b", "{c,d}")),
textArr(
"{{{1,2},{3,4}},{{5,6},{7,8}}}",
List.of("{{1,2},{3,4}}", "{{5,6},{7,8}}")),
textArr("{{{a}}}", List.of("{{a}}")),
textArr("{{\"a,b\",c},{d}}", List.of("{\"a,b\",c}", "{d}")),
textArr("{{\"{not array}\",x},{y}}", List.of("{\"{not array}\",x}", "{y}")),
// jsonb[][] real-world shape — parser must handle it, but PG rejects this literal as
// text[] because the inner {"k":...} confuses its rectangularity check.
parserOnly(
"{{\"{\\\"k\\\": 1}\"},{\"{\\\"k\\\": 2}\"}}",
List.of("{\"{\\\"k\\\": 1}\"}", "{\"{\\\"k\\\": 2}\"}")),
textArr("{{},{}}", List.of("{}", "{}")),
textArr("{{},{a}}", List.of("{}", "{a}")),
textArr("{{a,b},NULL,{c,d}}", Arrays.asList("{a,b}", null, "{c,d}")),
textArr("{{\"a\\\"b\",c}}", List.of("{\"a\\\"b\",c}")));

/** ';' delimiter cases — geometric arrays (box, etc.) where PG's typdelim is ';'. */
public static final List<Case> SEMI_DELIM =
List.of(
boxArr("{(1,2),(3,4)}", List.of("(1,2),(3,4)")),
boxArr("{(1,2),(3,4);(5,6),(7,8)}", List.of("(1,2),(3,4)", "(5,6),(7,8)")));

private PgArrayParseCases() {}
}
Loading
Loading