diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java b/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java index 3dd289f7b7..707077e3d8 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java @@ -141,6 +141,7 @@ import org.jooq.Param; import org.jooq.QualifiedAsterisk; import org.jooq.Record; import org.jooq.Result; +import org.jooq.Row; import org.jooq.SQLDialect; import org.jooq.Scope; import org.jooq.Select; @@ -236,8 +237,10 @@ abstract class AbstractDMLQuery extends AbstractRowCountQuery returningResolvedAsterisks.addAll(Arrays.asList(((QualifiedAsterisk) f).qualifier().fields())); else if (f instanceof Asterisk) returningResolvedAsterisks.addAll(Arrays.asList(table.fields())); + else if (f instanceof Row) + returningResolvedAsterisks.add(new RowField<>((Row) f)); else - throw new AssertionError("Type not supported: " + f); + throw new UnsupportedOperationException("Type not supported: " + f); } // @Override @@ -914,7 +917,7 @@ abstract class AbstractDMLQuery extends AbstractRowCountQuery // PostgreSQL generated case-insensitive Fields (default to lower case) // and wants to query HSQLDB (default to upper case), they may choose // to overwrite casing using RenderNameCase. - ctx.statement(connection.prepareStatement(ctx.sql(), map(Tools.flattenCollection(returningResolvedAsterisks, false), + ctx.statement(connection.prepareStatement(ctx.sql(), map(flattenCollection(returningResolvedAsterisks, false, true), style == RenderNameCase.UPPER ? f -> f.getName().toUpperCase(renderLocale(configuration().settings())) : style == RenderNameCase.LOWER diff --git a/jOOQ/src/main/java/org/jooq/impl/CursorImpl.java b/jOOQ/src/main/java/org/jooq/impl/CursorImpl.java index 8094be2306..3bcbb03870 100644 --- a/jOOQ/src/main/java/org/jooq/impl/CursorImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/CursorImpl.java @@ -38,6 +38,7 @@ package org.jooq.impl; // ... +import static org.jooq.impl.RowField.NO_NATIVE_SUPPORT; import static org.jooq.impl.Tools.embeddedFields; import static org.jooq.impl.Tools.embeddedRecordType; import static org.jooq.impl.Tools.recordFactory; @@ -1517,10 +1518,9 @@ final class CursorImpl extends AbstractCursor { AbstractRow nested = null; Class recordType = null; - if (field instanceof RowField) { + if (field instanceof RowField && NO_NATIVE_SUPPORT.contains(ctx.dialect())) { nested = ((RowField) field).emulatedFields(); - // TODO: [#4695] Calculate the correct Record[B] type - recordType = RecordImplN.class; + recordType = Tools.recordType(nested.size()); } else if (field.getDataType().isEmbeddable()) { nested = Tools.row0(embeddedFields(field)); diff --git a/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java b/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java index 10aa4d336d..fc508d1019 100644 --- a/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java +++ b/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java @@ -518,7 +518,7 @@ final class FieldMapsForInsert extends AbstractQueryPart { } // [#989] Avoid qualifying fields in INSERT field declaration - ctx.sql(" (").visit(new QueryPartCollectionView<>(collect(flattenCollection(values.keySet(), true))).qualify(false)).sql(')'); + ctx.sql(" (").visit(new QueryPartCollectionView<>(collect(flattenCollection(values.keySet(), true, true))).qualify(false)).sql(')'); } final Map, List>> valuesFlattened() { diff --git a/jOOQ/src/main/java/org/jooq/impl/RowField.java b/jOOQ/src/main/java/org/jooq/impl/RowField.java index 5782a12aab..17476c0005 100644 --- a/jOOQ/src/main/java/org/jooq/impl/RowField.java +++ b/jOOQ/src/main/java/org/jooq/impl/RowField.java @@ -37,19 +37,51 @@ */ package org.jooq.impl; +import static org.jooq.Converter.fromNullable; +// ... +// ... +// ... +// ... +import static org.jooq.SQLDialect.CUBRID; +// ... +import static org.jooq.SQLDialect.DERBY; +// ... +import static org.jooq.SQLDialect.FIREBIRD; +import static org.jooq.SQLDialect.H2; +// ... +import static org.jooq.SQLDialect.HSQLDB; +import static org.jooq.SQLDialect.IGNITE; +// ... +// ... +import static org.jooq.SQLDialect.MARIADB; +// ... +import static org.jooq.SQLDialect.MYSQL; +// ... +// ... +// ... +// ... +import static org.jooq.SQLDialect.SQLITE; +// ... +// ... +// ... +// ... import static org.jooq.impl.DefaultBinding.binding; import static org.jooq.impl.DefaultBinding.DefaultRecordBinding.pgNewRecord; +import static org.jooq.impl.Keywords.K_ROW; import static org.jooq.impl.Names.N_ROW; import static org.jooq.impl.Tools.map; import static org.jooq.impl.Tools.row0; import static org.jooq.impl.Tools.BooleanDataKey.DATA_LIST_ALREADY_INDENTED; +import java.util.Set; + import org.jooq.Context; import org.jooq.DataType; import org.jooq.Field; import org.jooq.Name; import org.jooq.Record; import org.jooq.Row; +import org.jooq.SQLDialect; /** * @author Lukas Eder @@ -59,7 +91,9 @@ final class RowField extends AbstractField< /** * Generated UID */ - private static final long serialVersionUID = -2065258332642911588L; + private static final long serialVersionUID = -2065258332642911588L; + + static final Set NO_NATIVE_SUPPORT = SQLDialect.supportedBy(CUBRID, DERBY, FIREBIRD, H2, HSQLDB, IGNITE, MARIADB, MYSQL, SQLITE); private final ROW row; private final AbstractRow emulatedFields; @@ -68,22 +102,12 @@ final class RowField extends AbstractField< this(row, N_ROW); } - @SuppressWarnings({ "serial", "unchecked", "rawtypes" }) + @SuppressWarnings({ "unchecked", "rawtypes" }) RowField(final ROW row, Name as) { - super(as, (DataType) SQLDataType.RECORD, CommentImpl.NO_COMMENT, binding(new AbstractConverter( - Object.class, (Class) Tools.recordType(row.size()) - ) { - @Override - public REC from(final Object t) { - // So far, this is only supported for PostgreSQL - return (REC) (t == null ? null : pgNewRecord(Record.class, (AbstractRow) row, t)); - } - - @Override - public Object to(REC u) { - throw new UnsupportedOperationException("Converting from nested records to bind values is not yet supported"); - } - })); + super(as, (DataType) SQLDataType.RECORD, CommentImpl.NO_COMMENT, binding(fromNullable( + // So far, this is only supported for PostgreSQL + Object.class, (Class) Tools.recordType(row.size()), t -> (REC) pgNewRecord(Record.class, (AbstractRow) row, t) + ))); this.row = row; this.emulatedFields = (AbstractRow) row0(map(row.fields(), x -> x.as(as + "." + x.getName()), Field[]::new)); @@ -93,10 +117,25 @@ final class RowField extends AbstractField< return emulatedFields; } + ROW row() { + return row; + } + @Override public final void accept(Context ctx) { - if (ctx.declareFields()) - ctx.data(DATA_LIST_ALREADY_INDENTED, true, c -> c.visit(new SelectFieldList<>(emulatedFields.fields.fields))); + if (ctx.declareFields()) { + if (NO_NATIVE_SUPPORT.contains(ctx.dialect())) + ctx.data(DATA_LIST_ALREADY_INDENTED, true, c -> c.visit(new SelectFieldList<>(emulatedFields.fields.fields))); + + + + + + // [#11812] RowField is mainly used for projections, in case of which an + // explicit ROW keyword helps disambiguate (1) from ROW(1) + else + ctx.visit(K_ROW).sql(' ').visit(row); + } else ctx.visit(row); } diff --git a/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java index 19a97f1d9f..fb0639731b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java @@ -3660,8 +3660,6 @@ final class SelectQueryImpl extends AbstractResultQuery imp for (SelectFieldOrAsterisk f : list) if (f instanceof Field) result.add(getResolveProjection(c, (Field) f)); - else if (f instanceof Row) - result.add(getResolveProjection(c, new RowField((Row) f))); else if (f instanceof QualifiedAsterisk) if (((QualifiedAsteriskImpl) f).fields.isEmpty()) if (resolveSupported) @@ -3682,6 +3680,8 @@ final class SelectQueryImpl extends AbstractResultQuery imp result.addAll(resolveAsterisk(new QueryPartList<>(), ((AsteriskImpl) f).fields)); else result.add(f); + else if (f instanceof Row) + result.add(getResolveProjection(c, new RowField((Row) f))); else throw new AssertionError("Type not supported: " + f); diff --git a/jOOQ/src/main/java/org/jooq/impl/Tools.java b/jOOQ/src/main/java/org/jooq/impl/Tools.java index a26a9f0126..b861c6653e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Tools.java +++ b/jOOQ/src/main/java/org/jooq/impl/Tools.java @@ -5162,13 +5162,13 @@ final class Tools { static final SelectFieldOrAsterisk qualify(Table table, SelectFieldOrAsterisk field) { if (field instanceof Field) return qualify(table, (Field) field); - // [#11812] TODO: handle field instanceof Row else if (field instanceof Asterisk) return table.asterisk(); else if (field instanceof QualifiedAsterisk) return table.asterisk(); + // [#11812] TODO: handle field instanceof Row else - throw new IllegalArgumentException("Unsupported field : " + field); + throw new UnsupportedOperationException("Unsupported field : " + field); } static final Field qualify(Table table, Field field) { @@ -5387,7 +5387,7 @@ final class Tools { static final Row embeddedFieldsRow(Row row) { if (hasEmbeddedFields(row.fields())) - return row(map(flattenCollection(Arrays.asList(row.fields()), false), f -> f)); + return row(map(flattenCollection(Arrays.asList(row.fields()), false, false), f -> f)); else return row; } @@ -5400,7 +5400,7 @@ final class Tools { // [#8353] [#10522] [#10523] TODO: Factor out some of this logic and // reuse it for the emulation of UPDATE .. SET row = (SELECT ..) List> select = field.query.getSelect(); - List> result = collect(flattenCollection(select, false)); + List> result = collect(flattenCollection(select, false, false)); Name tableName = name("t"); Name[] fieldNames = fieldNames(result.size()); Table t = new AliasedSelect<>(field.query, true, fieldNames).as("t"); @@ -5449,12 +5449,7 @@ final class Tools { // [#11729] Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=572873 Iterator it2 = field.getDataType().isEmbeddable() - ? new FlatteningIterator(it1) { - @Override - List flatten(E e) { - return (List) Arrays.asList(embeddedFields(field)); - } - } + ? new FlatteningIterator(it1, (e, duplicates) -> (List) Arrays.asList(embeddedFields(field))) : it1; return () -> it2; @@ -5464,20 +5459,13 @@ final class Tools { * Flatten out {@link EmbeddableTableField} elements contained in an * ordinary iterable. */ - static final > Iterable flattenCollection( - final Iterable iterable, - final boolean removeDuplicates + static final Iterable> flattenCollection( + final Iterable> iterable, + final boolean removeDuplicates, + final boolean flattenRowFields ) { // [#2530] [#6124] [#10481] TODO: Refactor and optimise these flattening algorithms - // [#11729] Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=572873 - Iterator it = new FlatteningIterator(iterable.iterator()) { - - - - - @SuppressWarnings("unchecked") - @Override - Iterable flatten(E e) { + return () -> new FlatteningIterator<>(iterable.iterator(), (e, duplicates) -> { @@ -5490,11 +5478,20 @@ final class Tools { - return Tools.flatten(e); + + // TODO [#10525] Should embedded records be emulated as RowField? + if (flattenRowFields && e instanceof RowField) { + List> result = new ArrayList<>(); + + for (Field field : ((RowField) e).row().fields()) + if (duplicates.test(field)) + result.add(field); + + return result; } - }; - return () -> it; + return Tools.flatten(e); + }); } /** @@ -5502,41 +5499,31 @@ final class Tools { * set iterable, making sure no duplicate keys resulting from overlapping * embeddables will be produced. */ - static final , Field>> Iterable flattenEntrySet( - final Iterable iterable, + static final Iterable, Field>> flattenEntrySet( + final Iterable, Field>> iterable, final boolean removeDuplicates ) { // [#2530] [#6124] [#10481] TODO: Refactor and optimise these flattening algorithms // [#11729] Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=572873 - Iterator it = new FlatteningIterator(iterable.iterator()) { + return () -> new FlatteningIterator<>(iterable.iterator(), (e, duplicates) -> { + if (e.getKey() instanceof EmbeddableTableField) { + List, Field>> result = new ArrayList<>(); + Field[] keys = embeddedFields(e.getKey()); + Field[] values = embeddedFields(e.getValue()); + + for (int i = 0; i < keys.length; i++) + result.add(new SimpleImmutableEntry, Field>( + keys[i], values[i] + )); - @SuppressWarnings("unchecked") - @Override - List flatten(E e) { - if (e.getKey() instanceof EmbeddableTableField) { - List result = new ArrayList<>(); - Field[] keys = embeddedFields(e.getKey()); - Field[] values = embeddedFields(e.getValue()); - - for (int i = 0; i < keys.length; i++) - - - - result.add((E) new SimpleImmutableEntry, Field>( - keys[i], values[i] - )); - - return result; - } - - return null; + return result; } - }; - return () -> it; + return null; + }); } static final Set lazy(Set set) { @@ -5548,17 +5535,20 @@ final class Tools { * iterators with a default implementation for {@link Iterator#remove()} for * convenience in the Java 6 build. */ - static abstract class FlatteningIterator implements Iterator { - private final Iterator delegate; - private Iterator flatten; - private E next; + static final class FlatteningIterator implements Iterator { + private final Iterator delegate; + private final BiFunction, ? extends Iterable> flattener; + private final Predicate checkDuplicates; + private Iterator flatten; + private E next; + private Set duplicates; - FlatteningIterator(Iterator delegate) { + FlatteningIterator(Iterator delegate, BiFunction, ? extends Iterable> flattener) { this.delegate = delegate; + this.flattener = flattener; + this.checkDuplicates = e -> (duplicates = lazy(duplicates)).add(e); } - abstract Iterable flatten(E e); - private final void move() { if (next == null) { if (flatten != null) { @@ -5574,7 +5564,7 @@ final class Tools { if (delegate.hasNext()) { next = delegate.next(); - Iterable flattened = flatten(next); + Iterable flattened = flattener.apply(next, checkDuplicates); if (flattened == null) return;