From cea2f300aafeebac7b9a4573d8d4a0663e171048 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Tue, 13 Jul 2021 12:26:23 +0200 Subject: [PATCH] [jOOQ/jOOQ#12155] Nested multiset record mapping fails at the 3rd level of nesting --- .../java/org/jooq/impl/ConvertedDataType.java | 5 + .../main/java/org/jooq/impl/JSONReader.java | 118 ++++++++++++------ .../src/main/java/org/jooq/impl/RowField.java | 4 +- 3 files changed, 85 insertions(+), 42 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java b/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java index 79bd3634e9..c7ca9484b1 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java @@ -46,6 +46,7 @@ import org.jooq.DataType; import org.jooq.Field; import org.jooq.Nullability; import org.jooq.Record; +import org.jooq.Result; import org.jooq.Row; import org.jooq.SQLDialect; @@ -214,6 +215,10 @@ final class ConvertedDataType extends AbstractDataTypeX { if (getConverter().toType().isInstance(object)) return (U) object; + // [#12155] Avoid double conversion passes between Result and custom List + else if (delegate.isMultiset() && !(object instanceof Result)) + return (U) object; + // [#3200] Try to convert arbitrary objects to T else return ((Converter) getConverter()).from(delegate.convert(object)); diff --git a/jOOQ/src/main/java/org/jooq/impl/JSONReader.java b/jOOQ/src/main/java/org/jooq/impl/JSONReader.java index 4423aa734a..566819f02b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/JSONReader.java +++ b/jOOQ/src/main/java/org/jooq/impl/JSONReader.java @@ -60,6 +60,7 @@ import javax.xml.bind.DatatypeConverter; import org.jooq.DSLContext; import org.jooq.Field; +import org.jooq.Fields; import org.jooq.Record; import org.jooq.Result; import org.jooq.tools.json.ContainerFactory; @@ -71,7 +72,7 @@ import org.jooq.tools.json.JSONParser; * @author Johannes Bühler * @author Lukas Eder */ -@SuppressWarnings({ "unchecked" }) +@SuppressWarnings({ "unchecked", "rawtypes" }) final class JSONReader { private final DSLContext ctx; @@ -92,7 +93,6 @@ final class JSONReader { return read(reader, false); } - @SuppressWarnings("rawtypes") final Result read(final Reader reader, boolean multiset) { try { Object root = new JSONParser().parse(reader, new ContainerFactory() { @@ -107,39 +107,53 @@ final class JSONReader { } }); - AbstractRow actualRow = row; - List> header = new ArrayList<>(); + return read(ctx, row, recordType, multiset, root); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } - List records; - Result result = null; + private static final Result read( + DSLContext ctx, + AbstractRow actualRow, + Class recordType, + boolean multiset, + Object root + ) { + List> header = new ArrayList<>(); - if (root instanceof Map) { - Map o1 = (Map) root; - List> fields = (List>) o1.get("fields"); + List records; + Result result = null; - if (fields != null) { - for (Map field : fields) { - String catalog = field.get("catalog"); - String schema = field.get("schema"); - String table = field.get("table"); - String name = field.get("name"); - String type = field.get("type"); + if (root instanceof Map) { + Map o1 = (Map) root; + List> fields = (List>) o1.get("fields"); - header.add(field(name(catalog, schema, table, name), getDataType(ctx.dialect(), defaultIfBlank(type, "VARCHAR")))); - } + if (fields != null) { + for (Map field : fields) { + String catalog = field.get("catalog"); + String schema = field.get("schema"); + String table = field.get("table"); + String name = field.get("name"); + String type = field.get("type"); + + header.add(field(name(catalog, schema, table, name), getDataType(ctx.dialect(), defaultIfBlank(type, "VARCHAR")))); } - - records = (List) o1.get("records"); } - else - records = (List) root; - if (actualRow == null && !header.isEmpty()) - actualRow = (AbstractRow) Tools.row0(header); + records = (List) o1.get("records"); + } + else + records = (List) root; - if (actualRow != null) - result = new ResultImpl<>(ctx.configuration(), actualRow); + if (actualRow == null && !header.isEmpty()) + actualRow = (AbstractRow) Tools.row0(header); + if (actualRow != null) + result = new ResultImpl<>(ctx.configuration(), actualRow); + + if (records != null) { for (Object o3 : records) { if (o3 instanceof Map) { Map record = (Map) o3; @@ -152,11 +166,20 @@ final class JSONReader { result = new ResultImpl<>(ctx.configuration(), actualRow = (AbstractRow) Tools.row0(header)); } - result.add(newRecord(true, recordType, actualRow, ctx.configuration()).operate(r -> { + List list = multiset + ? patchRecord( + ctx, + multiset, + actualRow, - // This sort is required if we use the JSONFormat.RecordFormat.OBJECT encoding (e.g. in SQL Server) + // This sort is required if we use the JSONFormat.RecordFormat.OBJECT encoding (e.g. in SQL Server) + record.entrySet().stream().sorted(comparing(Entry::getKey)).map(Entry::getValue).collect(toList()) + ) + : null; + + result.add(newRecord(true, recordType, actualRow, ctx.configuration()).operate(r -> { if (multiset) - r.from(record.entrySet().stream().sorted(comparing(Entry::getKey)).map(Entry::getValue).collect(toList())); + r.from(list); else r.fromMap(record); @@ -164,7 +187,7 @@ final class JSONReader { })); } else { - List record = (List) o3; + List record = (List) o3; if (result == null) { if (header.isEmpty()) @@ -173,24 +196,39 @@ final class JSONReader { result = new ResultImpl<>(ctx.configuration(), actualRow = (AbstractRow) Tools.row0(header)); } - // [#8829] LoaderImpl expects binary data to be encoded in base64, - // not according to org.jooq.tools.Convert - for (int i = 0; i < result.fields().length; i++) - if (result.field(i).getType() == byte[].class && record.get(i) instanceof String) - record.set(i, DatatypeConverter.parseBase64Binary((String) record.get(i))); - + patchRecord(ctx, multiset, actualRow, record); result.add(newRecord(true, recordType, actualRow, ctx.configuration()).operate(r -> { r.from(record); return r; })); } } + } - return result; - } - catch (Exception e) { - throw new RuntimeException(e); + return result; + } + + private static final List patchRecord(DSLContext ctx, boolean multiset, Fields result, List record) { + for (int i = 0; i < result.fields().length; i++) { + Field field = result.field(i); + + // [#8829] LoaderImpl expects binary data to be encoded in base64, + // not according to org.jooq.tools.Convert + if (field.getType() == byte[].class && record.get(i) instanceof String) + record.set(i, DatatypeConverter.parseBase64Binary((String) record.get(i))); + + // [#12155] Recurse for nested data types + else if (multiset && field.getDataType().isMultiset()) + record.set(i, read( + ctx, + (AbstractRow) field.getDataType().getRow(), + (Class) field.getDataType().getRecordType(), + multiset, + record.get(i) + )); } + + return record; } } diff --git a/jOOQ/src/main/java/org/jooq/impl/RowField.java b/jOOQ/src/main/java/org/jooq/impl/RowField.java index fafa5c242f..6429f9bd77 100644 --- a/jOOQ/src/main/java/org/jooq/impl/RowField.java +++ b/jOOQ/src/main/java/org/jooq/impl/RowField.java @@ -174,7 +174,7 @@ final class RowField extends AbstractField< default: - ctx.visit(alias(ctx, returningClob(ctx, jsonArray(row.fields())))); + ctx.visit(alias(ctx, returningClob(ctx, jsonArray(row.fields()).nullOnNull()))); break; } @@ -199,7 +199,7 @@ final class RowField extends AbstractField< default: - ctx.visit(alias(ctx, returningClob(ctx, jsonbArray(row.fields())))); + ctx.visit(alias(ctx, returningClob(ctx, jsonbArray(row.fields()).nullOnNull()))); break; }