Skip to content

Null-encoding converter doesn't work with PostgreSQL nested records #17703

Closed
@KuceraMartin

Description

@KuceraMartin

Expected behavior

If I call withNullability(Nullability.NULL) on the ForcedType, I would expect the conversion to be invoked every time – even if the value is null.

In the problem reproduction below, I would expect the output to be

(unimportant JOOQ prints...)
name: Alan Turing	age: Some(41)
name: Ada Lovelace	age: None

When running the reproduction, I also tried putting a breakpoint into OptionIntConverter.from to see where it gets called from. Here is the call stack that I got:

OptionIntConverter.from(Integer): Option (/Users/martin/development/jooq-option-problem/jooq-generated/src/main/scala/converters/OptionIntConverter.scala:8)
OptionIntConverter.from(Object): Object (/Users/martin/development/jooq-option-problem/jooq-generated/src/main/scala/converters/OptionIntConverter.scala:5)
ContextConverter$1.from(Object,ConverterContext): Object (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/ContextConverter.java:147)
DefaultBinding$DefaultRecordBinding.pgFromString(BindingScope,Field,String): Object (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/DefaultBinding.java:4286)
DefaultBinding$DefaultRecordBinding.pgSetValue(BindingScope,Record,Field,String): void (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/DefaultBinding.java:4411)
DefaultBinding$DefaultRecordBinding.lambda$pgNewRecord$13(BindingScope,List,Record): Record (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/DefaultBinding.java:4403)
DefaultBinding$DefaultRecordBinding$$Lambda/0x00000008002c04e0.apply(Object): Object (Unknown Source:-1)
RecordDelegate.operate(ThrowingFunction): Record (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/RecordDelegate.java:144)
DefaultBinding$DefaultRecordBinding.pgNewRecord(BindingScope,Class,AbstractRow,Object): Record (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/DefaultBinding.java:4399)
DefaultBinding$DefaultRecordBinding.get0(BindingGetResultSetContext): Record (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/DefaultBinding.java:4172)
DefaultBinding$DefaultRecordBinding.get0(BindingGetResultSetContext): Object (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/DefaultBinding.java:4084)
DefaultBinding$InternalBinding.get(BindingGetResultSetContext): void (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/DefaultBinding.java:1190)
CursorImpl$CursorRecordInitialiser.setValue(AbstractRecord,Field,int): void (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/CursorImpl.java:1582)
CursorImpl$CursorRecordInitialiser.apply(AbstractRecord): AbstractRecord (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/CursorImpl.java:1518)
CursorImpl$CursorRecordInitialiser.apply(Object): Object (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/CursorImpl.java:1433)
RecordDelegate.operate(ThrowingFunction): Record (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/RecordDelegate.java:144)
CursorImpl$CursorIterator.fetchNext(): Record (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/CursorImpl.java:1390)
CursorImpl$CursorIterator.hasNext(): boolean (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/CursorImpl.java:1366)
CursorImpl.fetchNext(int): Result (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/CursorImpl.java:174)
AbstractCursor.fetch(int): Result (file:///Users/martin/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/jooq/jooq/3.19.15/jooq-3.19.15-sources.jar!/org/jooq/impl/AbstractCursor.java:177)

What I found interesting is the frame DefaultBinding:4286:

else if (type == Integer.class)
    return converter.from((T) Integer.valueOf(string), ctx.converterContext());

If we scroll up through the different cases of the else-ifs, we see that on the very top there is:

if (string == null)
    return null;

In other words, if the value is null, it will indeed return null directly, whereas in other cases, the converter is invoked.

Actual behavior

The conversion is only invoked if the value is not null. If the value is null, the conversion is skipped entirely, and the value remains null.

In the problem reproduction below, the actual output is:

(unimportant JOOQ prints...)
name: Alan Turing	age: Some(41)
name: Ada Lovelace	age: null

Steps to reproduce the problem

  1. clone from https://github.com/KuceraMartin/jooq-option-problem
  2. run sbt generateJooq
  3. run sbt app/run

jOOQ Version

3.19.15

Database product and version

PostgreSQL 17

Java Version

No response

JDBC / R2DBC driver name and version (include name if unofficial driver)

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions