[jOOQ/jOOQ#3884] Support ambiguous column names in MULTISET (MSSQL)

This commit is contained in:
Lukas Eder 2021-07-02 14:11:52 +02:00
parent 5d60eb36f1
commit 04a5a0ffe7
7 changed files with 69 additions and 45 deletions

View File

@ -133,8 +133,6 @@ import static org.jooq.impl.Tools.findAny;
import static org.jooq.impl.Tools.getMappedUDTName;
import static org.jooq.impl.Tools.map;
import static org.jooq.impl.Tools.needsBackslashEscaping;
import static org.jooq.impl.Tools.newRecord;
import static org.jooq.impl.Tools.row0;
import static org.jooq.impl.Tools.uncoerce;
import static org.jooq.tools.StringUtils.leftPad;
import static org.jooq.tools.jdbc.JDBCUtils.safeFree;
@ -146,6 +144,7 @@ import static org.jooq.util.postgres.PostgresUtils.toPGArrayString;
import static org.jooq.util.postgres.PostgresUtils.toPGInterval;
import java.io.Serializable;
import java.io.StringReader;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -214,7 +213,6 @@ import org.jooq.RowId;
import org.jooq.SQLDialect;
import org.jooq.Schema;
import org.jooq.Scope;
import org.jooq.Select;
import org.jooq.TableRecord;
import org.jooq.UDTRecord;
import org.jooq.XML;
@ -241,8 +239,6 @@ import org.jooq.types.YearToMonth;
import org.jooq.types.YearToSecond;
import org.jooq.util.postgres.PostgresUtils;
import org.jetbrains.annotations.NotNull;
// ...
// ...
@ -3787,12 +3783,12 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
Field<?> field = uncoerce(ctx.field());
if (field.getDataType() instanceof MultisetDataType)
return extracted(ctx, (MultisetDataType<?>) field.getDataType());
return readMultiset(ctx, (MultisetDataType<?>) field.getDataType());
else
return ctx.configuration().dsl().fetch(convert(ctx.resultSet().getObject(ctx.index()), ResultSet.class));
}
private final <R extends Record> Result<R> extracted(BindingGetResultSetContext<U> ctx, MultisetDataType<R> type) throws SQLException {
private final <R extends Record> Result<R> readMultiset(BindingGetResultSetContext<U> ctx, MultisetDataType<R> type) throws SQLException {
NestedCollectionEmulation emulation = emulateMultiset(ctx.configuration());
switch (emulation) {
@ -3801,7 +3797,7 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
case JSON:
case JSONB:
return new JSONReader<>(ctx.dsl(), type.row, type.recordType).read(ctx.resultSet().getString(ctx.index()));
return new JSONReader<>(ctx.dsl(), type.row, type.recordType).read(new StringReader(ctx.resultSet().getString(ctx.index())), true);
case XML:
return new XMLHandler<>(ctx.dsl(), type.row, type.recordType).read(ctx.resultSet().getString(ctx.index()));

View File

@ -38,6 +38,8 @@
package org.jooq.impl;
import static java.util.Arrays.asList;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import static org.jooq.impl.DSL.field;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DefaultDataType.getDataType;
@ -52,6 +54,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.bind.DatatypeConverter;
@ -85,8 +88,12 @@ final class JSONReader<R extends Record> {
return read(new StringReader(string));
}
@SuppressWarnings("rawtypes")
final Result<R> read(final Reader reader) {
return read(reader, false);
}
@SuppressWarnings("rawtypes")
final Result<R> read(final Reader reader, boolean multiset) {
try {
Object root = new JSONParser().parse(reader, new ContainerFactory() {
@Override
@ -146,7 +153,13 @@ final class JSONReader<R extends Record> {
}
result.add(newRecord(true, recordType, actualRow, ctx.configuration()).operate(r -> {
r.fromMap(record);
// This sort is required if we use the JSONFormat.RecordFormat.OBJECT encoding (e.g. in SQL Server)
if (multiset)
r.from(record.entrySet().stream().sorted(comparing(Entry::getKey)).map(Entry::getValue).collect(toList()));
else
r.fromMap(record);
return r;
}));
}

View File

@ -63,10 +63,13 @@ import static org.jooq.impl.SQLDataType.CLOB;
import static org.jooq.impl.SQLDataType.VARCHAR;
import static org.jooq.impl.Tools.emulateMultiset;
import static org.jooq.impl.Tools.fieldName;
import static org.jooq.impl.Tools.fieldNameString;
import static org.jooq.impl.Tools.fieldNameStrings;
import static org.jooq.impl.Tools.map;
import static org.jooq.impl.Tools.visitSubquery;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_MULTISET_CONDITION;
import java.util.List;
import java.util.Set;
import org.jooq.AggregateFilterStep;
@ -78,7 +81,6 @@ import org.jooq.JSONArrayAggOrderByStep;
import org.jooq.JSONArrayAggReturningStep;
import org.jooq.JSONArrayReturningStep;
import org.jooq.JSONB;
import org.jooq.JSONObjectReturningStep;
import org.jooq.Record;
import org.jooq.Record1;
import org.jooq.Result;
@ -101,7 +103,8 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
@SuppressWarnings("unchecked")
Multiset(Select<R> select) {
super(N_MULTISET, new MultisetDataType<>((AbstractRow<R>) select.fieldsRow(), select.getRecordType()));
// [#12100] Can't use select.fieldsRow() here.
super(N_MULTISET, new MultisetDataType<>((AbstractRow<R>) DSL.row(select.getSelect()), select.getRecordType()));
this.select = select;
}
@ -131,7 +134,7 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
private final void accept0(Context<?> ctx, boolean multisetCondition) {
switch (emulateMultiset(ctx.configuration())) {
case JSON: {
Table<?> t = select.asTable("t");
Table<?> t = select.asTable("t", fieldNameStrings(select.getSelect().size()));
switch (ctx.family()) {
@ -147,7 +150,7 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
JSONArrayAggOrderByStep<JSON> order;
JSONArrayAggReturningStep<JSON> returning;
returning = order = jsonArrayaggEmulation(ctx, t, multisetCondition);
returning = order = jsonArrayaggEmulation(ctx, select, false);
// TODO: Re-apply derived table's ORDER BY clause as aggregate ORDER BY
if (multisetCondition)
@ -174,7 +177,7 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
}
case JSONB: {
Table<?> t = select.asTable("t");
Table<?> t = select.asTable("t", fieldNameStrings(select.getSelect().size()));
switch (ctx.family()) {
@ -190,7 +193,7 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
JSONArrayAggOrderByStep<JSONB> order;
JSONArrayAggReturningStep<JSONB> returning;
returning = order = jsonbArrayaggEmulation(ctx, t, multisetCondition);
returning = order = jsonbArrayaggEmulation(ctx, select, false);
// TODO: Re-apply derived table's ORDER BY clause as aggregate ORDER BY
if (multisetCondition)
@ -213,7 +216,8 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
}
case XML: {
Table<?> t = select.asTable("t");
List<Field<?>> fields = select.getSelect();
Table<?> t = select.asTable("t", fieldNameStrings(fields.size()));
switch (ctx.family()) {
@ -230,11 +234,15 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
default: {
XMLAggOrderByStep<XML> order;
AggregateFilterStep<XML> filter;
filter = order = xmlaggEmulation(t, multisetCondition);
filter = order = xmlaggEmulation(select, multisetCondition, false);
// TODO: Re-apply derived table's ORDER BY clause as aggregate ORDER BY
if (multisetCondition)
@ -260,18 +268,6 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
}
}
static final <J> Field<J> returningClob(Scope ctx, JSONObjectReturningStep<J> jsonObject) {
switch (ctx.family()) {
default:
return jsonObject;
}
}
static final <J> Field<J> returningClob(Scope ctx, JSONArrayReturningStep<J> jsonArray) {
switch (ctx.family()) {
@ -296,24 +292,35 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> {
}
}
static final JSONArrayAggOrderByStep<JSON> jsonArrayaggEmulation(Scope ctx, Fields fields, boolean multisetCondition) {
// The emulations use the less intuitive JSONFormat.RecordFormat.ARRAY encoding:
// - It uses less bandwidth
// - It is column name agnostic (supporting ambiguous column names)
// - The JSON never leaks outside of the emulation into user code
static final JSONArrayAggOrderByStep<JSON> jsonArrayaggEmulation(Scope ctx, Fields fields, boolean agg) {
return jsonArrayAgg(
returningClob(ctx, jsonObject(
map(fields.fields(), (f, i) -> jsonEntry(multisetCondition ? Tools.fieldNameString(i) : f.getName(), f))
returningClob(ctx, jsonArray(
map(fields.fields(), (f, i) -> agg ? f : DSL.field(fieldName(i), SQLDataType.JSON))
))
);
}
static final JSONArrayAggOrderByStep<JSONB> jsonbArrayaggEmulation(Scope ctx, Fields fields, boolean multisetCondition) {
static final JSONArrayAggOrderByStep<JSONB> jsonbArrayaggEmulation(Scope ctx, Fields fields, boolean agg) {
return jsonbArrayAgg(
returningClob(ctx, jsonbObject(
map(fields.fields(), (f, i) -> jsonEntry(multisetCondition ? Tools.fieldNameString(i) : f.getName(), f))
returningClob(ctx, jsonbArray(
map(fields.fields(), (f, i) -> agg ? f : DSL.field(fieldName(i), SQLDataType.JSONB))
))
);
}
static final XMLAggOrderByStep<XML> xmlaggEmulation(Fields fields, boolean multisetCondition) {
return xmlagg(xmlelement(N_RECORD, map(fields.fields(), (f, i) -> xmlelement(multisetCondition ? fieldName(i) : f.getUnqualifiedName(), f))));
static final XMLAggOrderByStep<XML> xmlaggEmulation(Fields fields, boolean multisetCondition, boolean agg) {
return xmlagg(
xmlelement(N_RECORD,
map(fields.fields(), (f, i) -> xmlelement(
multisetCondition ? fieldNameString(i) : f.getName(),
agg ? f : DSL.field(fieldName(i))
))
)
);
}
}

View File

@ -113,7 +113,7 @@ final class MultisetAgg<R extends Record> extends DefaultAggregateFunction<Resul
private final void accept0(Context<?> ctx, boolean multisetCondition) {
switch (emulateMultiset(ctx.configuration())) {
case JSON: {
JSONArrayAggOrderByStep<JSON> order = jsonArrayaggEmulation(ctx, row, multisetCondition);
JSONArrayAggOrderByStep<JSON> order = jsonArrayaggEmulation(ctx, row, true);
Field<?> f = multisetCondition
? fo((AbstractAggregateFunction<?>) returningClob(ctx, order.orderBy(row.fields())))
@ -128,7 +128,7 @@ final class MultisetAgg<R extends Record> extends DefaultAggregateFunction<Resul
}
case JSONB: {
JSONArrayAggOrderByStep<JSONB> order = jsonbArrayaggEmulation(ctx, row, multisetCondition);
JSONArrayAggOrderByStep<JSONB> order = jsonbArrayaggEmulation(ctx, row, true);
Field<?> f = multisetCondition
? fo((AbstractAggregateFunction<?>) returningClob(ctx, order.orderBy(row.fields())))
@ -139,7 +139,7 @@ final class MultisetAgg<R extends Record> extends DefaultAggregateFunction<Resul
}
case XML: {
XMLAggOrderByStep<XML> order = xmlaggEmulation(row, multisetCondition);
XMLAggOrderByStep<XML> order = xmlaggEmulation(row, multisetCondition, true);
Field<XML> f = xmlelement(N_RESULT,
multisetCondition

View File

@ -37,10 +37,14 @@
*/
package org.jooq.impl;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import static org.jooq.impl.Tools.CTX;
import static org.jooq.impl.Tools.newRecord;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.jooq.CharacterSet;
import org.jooq.Collation;
@ -124,6 +128,7 @@ final class MultisetDataType<R extends Record> extends DefaultDataType<Result<R>
return row;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Result<R> convert(Object object) {
@ -137,7 +142,11 @@ final class MultisetDataType<R extends Record> extends DefaultDataType<Result<R>
// [#12014] TODO: Fix this and remove workaround
if (record instanceof Record)
((AbstractRecord) r).from((Record) record);
((AbstractRecord) r).fromArray(((Record) record).intoArray());
// This sort is required if we use the JSONFormat.RecordFormat.OBJECT encoding (e.g. in SQL Server)
else if (record instanceof Map)
r.from(((Map<String, ?>) record).entrySet().stream().sorted(comparing(Entry::getKey)).map(Entry::getValue).collect(toList()));
else
r.from(record);

View File

@ -5890,7 +5890,6 @@ final class Tools {
default:
return NestedCollectionEmulation.NATIVE;
}

View File

@ -261,7 +261,7 @@ final class XMLHandler<R extends Record> extends DefaultHandler {
if (fields.size() <= 1)
return false;
else
return !anyMatch(fields, f -> !"value".equals(f.getName()));
return !anyMatch(fields, f -> !"value".equalsIgnoreCase(f.getName()));
}
@Override