diff --git a/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java b/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java index c912f2faad..cb051827a4 100644 --- a/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java +++ b/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java @@ -41,13 +41,25 @@ import static org.jooq.impl.DSL.table; import static org.jooq.impl.Keywords.K_DEFAULT_VALUES; import static org.jooq.impl.Keywords.K_VALUES; +import java.util.AbstractList; +import java.util.AbstractMap; +import java.util.AbstractSet; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import org.jooq.Clause; import org.jooq.Context; +import org.jooq.Field; import org.jooq.Record; import org.jooq.Select; +import org.jooq.impl.AbstractStoreQuery.UnknownField; /** * @author Lukas Eder @@ -57,13 +69,15 @@ final class FieldMapsForInsert extends AbstractQueryPart { /** * Generated UID */ - private static final long serialVersionUID = -6227074228534414225L; + private static final long serialVersionUID = -6227074228534414225L; - final List insertMaps; + final List> empty; + final Map, List>> values; + int rows; FieldMapsForInsert() { - insertMaps = new ArrayList(); - insertMaps.add(null); + values = new LinkedHashMap, List>>(); + empty = new ArrayList>(); } // ------------------------------------------------------------------------- @@ -80,13 +94,13 @@ final class FieldMapsForInsert extends AbstractQueryPart { } // Single record inserts can use the standard syntax in any dialect - else if (insertMaps.size() == 1 || insertMaps.get(1) == null) { + else if (rows == 1) { ctx.formatSeparator() .start(INSERT_VALUES) .visit(K_VALUES) - .sql(' ') - .visit(insertMaps.get(0)) - .end(INSERT_VALUES); + .sql(' '); + toSQL92Values(ctx); + ctx.end(INSERT_VALUES); } // True SQL92 multi-record inserts aren't always supported @@ -145,31 +159,51 @@ final class FieldMapsForInsert extends AbstractQueryPart { private final Select insertSelect(Context context) { Select select = null; - for (FieldMapForInsert map : insertMaps) { - if (map != null) { - Select iteration = DSL.using(context.configuration()).select(map.values()); + for (int row = 0; row < rows; row++) { + List> fields = new ArrayList>(); - if (select == null) - select = iteration; - else - select = select.unionAll(iteration); - } + for (List> list : values.values()) + fields.add(list.get(row)); + + Select iteration = DSL.using(context.configuration()).select(fields); + + if (select == null) + select = iteration; + else + select = select.unionAll(iteration); } return select; } - private final void toSQL92Values(Context context) { - context.visit(insertMaps.get(0)); + final void toSQL92Values(Context ctx) { + boolean indent = (values.size() > 1); - int i = 0; - for (FieldMapForInsert map : insertMaps) { - if (map != null && i > 0) { - context.sql(", "); - context.visit(map); + for (int row = 0; row < rows; row++) { + if (row > 0) + ctx.sql(", "); + + ctx.sql('('); + + if (indent) + ctx.formatIndentStart(); + + String separator = ""; + for (List> list : values.values()) { + ctx.sql(separator); + + if (indent) + ctx.formatNewLine(); + + ctx.visit(list.get(row)); + separator = ", "; } - i++; + if (indent) + ctx.formatIndentEnd() + .formatNewLine(); + + ctx.sql(')'); } } @@ -182,23 +216,216 @@ final class FieldMapsForInsert extends AbstractQueryPart { // The FieldMapsForInsert API // ------------------------------------------------------------------------- + final void addFields(Collection> fields) { + if (rows == 0) + newRecord(); + + for (Field field : fields) { + Field e = DSL.val(null, field); + empty.add(e); + + if (!values.containsKey(field)) { + values.put(field, rows > 0 + ? new ArrayList>(Collections.nCopies(rows, e)) + : new ArrayList>() + ); + } + } + } + + final void set(Collection> fields) { + Iterator> it1 = fields.iterator(); + Iterator>> it2 = values.values().iterator(); + + while (it1.hasNext() && it2.hasNext()) + it2.next().set(rows - 1, it1.next()); + + if (it1.hasNext() || it2.hasNext()) + throw new IllegalArgumentException("Added record size (" + fields.size() + ") must match fields size (" + values.size() + ")"); + } + + @SuppressWarnings("unchecked") + final Field set(Field field, Field value) { + addFields(Collections.singletonList(field)); + return (Field) values.get(field).set(rows - 1, value); + } + + final void set(Map, ?> map) { + addFields(map.keySet()); + for (Entry, ?> entry : map.entrySet()) + values.get(entry.getKey()) + .set(rows - 1, Tools.field(entry.getValue(), entry.getKey())); + } + + final void newRecord() { + int i = 0; + + for (List> list : values.values()) + list.add(empty.get(i++)); + + rows++; + } + + final Collection> fields() { + return values.keySet(); + } + + final List, Field>> maps() { + return new AbstractList, Field>>() { + @Override + public Map, Field> get(int index) { + return map(index); + } + + @Override + public int size() { + return rows; + } + }; + } + + final Map, Field> map(int index) { + return new AbstractMap, Field>() { + transient Set, Field>> entrySet; + + @Override + public Set, Field>> entrySet() { + if (entrySet == null) + entrySet = new EntrySet(); + + return entrySet; + } + + @Override + public boolean containsKey(Object key) { + return values.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + for (List> list : values.values()) + if (list.get(index).equals(value)) + return true; + + return false; + } + + @Override + public Field get(Object key) { + List> list = values.get(key); + return list == null ? null : list.get(index); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Field put(Field key, Field value) { + return FieldMapsForInsert.this.set((Field) key, (Field) value); + } + + @Override + public Field remove(Object key) { + List> list = values.get(key); + values.remove(key); + return list == null ? null : list.get(index); + } + + @Override + public Set> keySet() { + return values.keySet(); + } + + final class EntrySet extends AbstractSet, Field>> { + @Override + public final int size() { + return values.size(); + } + + @Override + public final void clear() { + values.clear(); + } + + @Override + public final Iterator, Field>> iterator() { + return new Iterator, Field>>() { + Iterator, List>>> delegate = values.entrySet().iterator(); + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Entry, Field> next() { + Entry, List>> entry = delegate.next(); + return new SimpleImmutableEntry, Field>(entry.getKey(), entry.getValue().get(index)); + } + + @Override + public void remove() { + delegate.remove(); + } + }; + } + } + }; + } + + final Map, Field> lastMap() { + return map(rows - 1); + } + final boolean isExecutable() { - return !insertMaps.isEmpty() && insertMaps.get(0) != null; + return rows > 0; } - public final FieldMapForInsert getMap() { - if (insertMaps.get(index()) == null) - insertMaps.set(index(), new FieldMapForInsert()); + final void toSQLReferenceKeys(Context ctx) { - return insertMaps.get(index()); - } + // [#1506] with DEFAULT VALUES, we might not have any columns to render + if (!isExecutable()) + return; - public final void newRecord() { - if (insertMaps.get(index()) != null) - insertMaps.add(null); - } + // [#2995] Do not generate empty column lists. + if (values.size() == 0) + return; - private final int index() { - return insertMaps.size() - 1; + // [#4629] Do not generate column lists for unknown columns + unknownFields: { + for (Field field : values.keySet()) + if (!(field instanceof UnknownField)) + break unknownFields; + + return; + } + + boolean indent = (values.size() > 1); + + ctx.sql(" ("); + + if (indent) + ctx.formatIndentStart(); + + // [#989] Avoid qualifying fields in INSERT field declaration + boolean qualify = ctx.qualify(); + ctx.qualify(false); + + String separator = ""; + for (Field field : values.keySet()) { + ctx.sql(separator); + + if (indent) + ctx.formatNewLine(); + + ctx.visit(field); + separator = ", "; + } + + ctx.qualify(qualify); + + if (indent) + ctx.formatIndentEnd() + .formatNewLine(); + + ctx.sql(')'); } } diff --git a/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java index 361eaba46e..c180debcd2 100644 --- a/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java @@ -121,8 +121,8 @@ final class InsertQueryImpl extends AbstractStoreQuery impl } @Override - protected final FieldMapForInsert getValues() { - return insertMaps.getMap(); + protected final Map, Field> getValues() { + return insertMaps.lastMap(); } @Override @@ -195,13 +195,13 @@ final class InsertQueryImpl extends AbstractStoreQuery impl @Override public final void setSelect(Field[] f, Select s) { - insertMaps.getMap().putFields(Arrays.asList(f)); + insertMaps.addFields(Arrays.asList(f)); select = s; } @Override public final void addValues(Map, ?> map) { - insertMaps.getMap().set(map); + insertMaps.set(map); } @Override @@ -424,10 +424,7 @@ final class InsertQueryImpl extends AbstractStoreQuery impl .visit(table) .declareTables(declareTables); - // [#1506] with DEFAULT VALUES, we might not have any columns to render - if (insertMaps.isExecutable()) - insertMaps.insertMaps.get(0).toSQLReferenceKeys(ctx); - + insertMaps.toSQLReferenceKeys(ctx); ctx.end(INSERT_INSERT_INTO); if (select != null) { @@ -435,7 +432,7 @@ final class InsertQueryImpl extends AbstractStoreQuery impl // [#2995] Prevent the generation of wrapping parentheses around the // INSERT .. SELECT statement's SELECT because they would be // interpreted as the (missing) INSERT column list's parens. - if (insertMaps.insertMaps.get(0).size() == 0) + if (insertMaps.fields().size() == 0) ctx.data(DATA_INSERT_SELECT_WITHOUT_INSERT_COLUMN_LIST, true); @@ -530,9 +527,9 @@ final class InsertQueryImpl extends AbstractStoreQuery impl // re-used. Select rows = null; - Name[] aliases = fieldNames(insertMaps.getMap().keySet().toArray(EMPTY_FIELD)); + Name[] aliases = fieldNames(insertMaps.fields().toArray(EMPTY_FIELD)); - for (FieldMapForInsert map : insertMaps.insertMaps) { + for (Map, Field> map : insertMaps.maps()) { Select row = select(aliasedFields(map.values().toArray(EMPTY_FIELD), aliases)) .whereNotExists( @@ -549,7 +546,7 @@ final class InsertQueryImpl extends AbstractStoreQuery impl return create(configuration) .insertInto(table) - .columns(insertMaps.getMap().keySet()) + .columns(insertMaps.fields()) .select(selectFrom(table(rows).as("t"))); } else { @@ -562,7 +559,7 @@ final class InsertQueryImpl extends AbstractStoreQuery impl MergeOnConditionStep on = create(configuration).mergeInto(table) .usingDual() - .on(matchByPrimaryKey(insertMaps.getMap())); + .on(matchByPrimaryKey(insertMaps.lastMap())); // [#1295] Use UPDATE clause only when with ON DUPLICATE KEY UPDATE, // not with ON DUPLICATE KEY IGNORE @@ -572,8 +569,8 @@ final class InsertQueryImpl extends AbstractStoreQuery impl .set(updateMap); } - return notMatched.whenNotMatchedThenInsert(insertMaps.getMap().keySet()) - .values(insertMaps.getMap().values()); + return notMatched.whenNotMatchedThenInsert(insertMaps.fields()) + .values(insertMaps.lastMap().values()); } else { throw new IllegalStateException("The ON DUPLICATE KEY IGNORE/UPDATE clause cannot be emulated when inserting into non-updatable tables : " + table); @@ -585,7 +582,7 @@ final class InsertQueryImpl extends AbstractStoreQuery impl * updated primary key values. */ @SuppressWarnings("unchecked") - private final Condition matchByPrimaryKey(FieldMapForInsert map) { + private final Condition matchByPrimaryKey(Map, Field> map) { Condition result = null; for (Field f : table.getPrimaryKey().getFields()) { diff --git a/jOOQ/src/main/java/org/jooq/impl/LoaderImpl.java b/jOOQ/src/main/java/org/jooq/impl/LoaderImpl.java index cdc14da687..2f18145073 100644 --- a/jOOQ/src/main/java/org/jooq/impl/LoaderImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/LoaderImpl.java @@ -716,6 +716,7 @@ final class LoaderImpl implements Object[] row = null; BatchBindStep bind = null; InsertQuery insert = null; + boolean newRecord = false; execution: { rows: while (iterator.hasNext() && ((row = iterator.next()) != null)) { @@ -770,6 +771,11 @@ final class LoaderImpl implements if (insert == null) insert = create.insertQuery(table); + if (newRecord) { + newRecord = false; + insert.newRecord(); + } + for (int i = 0; i < row.length; i++) if (i < fields.length && fields[i] != null) addValue0(insert, fields[i], row[i]); @@ -790,7 +796,7 @@ final class LoaderImpl implements try { if (bulk != BULK_NONE) { if (bulk == BULK_ALL || processed % bulkAfter != 0) { - insert.newRecord(); + newRecord = true; continue rows; } } diff --git a/jOOQ/src/main/java/org/jooq/impl/MergeImpl.java b/jOOQ/src/main/java/org/jooq/impl/MergeImpl.java index f808185bbd..6879223266 100644 --- a/jOOQ/src/main/java/org/jooq/impl/MergeImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/MergeImpl.java @@ -240,7 +240,7 @@ implements private boolean matchedClause; private FieldMapForUpdate matchedUpdate; private boolean notMatchedClause; - private FieldMapForInsert notMatchedInsert; + private FieldMapsForInsert notMatchedInsert; // Objects for the UPSERT syntax (including H2 MERGE, HANA UPSERT, etc.) private boolean upsertStyle; @@ -721,8 +721,8 @@ implements getUpsertValues().addAll(Tools.fields(values, getUpsertFields().toArray(EMPTY_FIELD))); } else { - Field[] fields = notMatchedInsert.keySet().toArray(EMPTY_FIELD); - notMatchedInsert.putValues(Tools.fields(values, fields)); + Field[] fields = notMatchedInsert.fields().toArray(EMPTY_FIELD); + notMatchedInsert.set(Tools.fields(values, fields)); } return this; @@ -937,7 +937,7 @@ implements matchedUpdate.put(field, nullSafe(value)); } else if (notMatchedClause) { - notMatchedInsert.put(field, nullSafe(value)); + notMatchedInsert.set(field, nullSafe(value)); } else { throw new IllegalStateException("Cannot call where() on the current state of the MERGE statement"); @@ -1124,8 +1124,8 @@ implements @Override public final MergeImpl whenNotMatchedThenInsert(Collection> fields) { notMatchedClause = true; - notMatchedInsert = new FieldMapForInsert(); - notMatchedInsert.putFields(fields); + notMatchedInsert = new FieldMapsForInsert(); + notMatchedInsert.addFields(fields); matchedClause = false; return this; @@ -1524,9 +1524,9 @@ implements notMatchedInsert.toSQLReferenceKeys(ctx); ctx.formatSeparator() .start(MERGE_VALUES) - .visit(K_VALUES).sql(' ') - .visit(notMatchedInsert) - .end(MERGE_VALUES); + .visit(K_VALUES).sql(' '); + notMatchedInsert.toSQL92Values(ctx); + ctx.end(MERGE_VALUES); } ctx.start(MERGE_WHERE); diff --git a/jOOQ/src/main/java/org/jooq/tools/jdbc/MockConfiguration.java b/jOOQ/src/main/java/org/jooq/tools/jdbc/MockConfiguration.java index dfe2e45ec0..192395fd60 100644 --- a/jOOQ/src/main/java/org/jooq/tools/jdbc/MockConfiguration.java +++ b/jOOQ/src/main/java/org/jooq/tools/jdbc/MockConfiguration.java @@ -43,6 +43,7 @@ import javax.sql.DataSource; import org.jooq.Configuration; import org.jooq.ConnectionProvider; import org.jooq.ConverterProvider; +import org.jooq.DSLContext; import org.jooq.ExecuteListener; import org.jooq.ExecuteListenerProvider; import org.jooq.ExecutorProvider; @@ -85,6 +86,11 @@ public class MockConfiguration implements Configuration { this.provider = provider; } + @Override + public DSLContext dsl() { + return delegate.dsl(); + } + @Override public Map data() { return delegate.data();