jooq/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java

1282 lines
23 KiB
Java

/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: http://www.jooq.org/licenses
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.impl;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
// ...
// ...
// ...
// ...
// ...
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.MARIADB;
// ...
// ...
// ...
import static org.jooq.SQLDialect.MYSQL;
// ...
// ...
import static org.jooq.SQLDialect.POSTGRES;
// ...
// ...
// ...
// ...
import static org.jooq.conf.SettingsTools.renderLocale;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.select;
import static org.jooq.impl.DSL.unquotedName;
import static org.jooq.impl.Keywords.K_BEGIN;
import static org.jooq.impl.Keywords.K_BULK_COLLECT_INTO;
import static org.jooq.impl.Keywords.K_DECLARE;
import static org.jooq.impl.Keywords.K_END;
import static org.jooq.impl.Keywords.K_FOR;
import static org.jooq.impl.Keywords.K_FORALL;
import static org.jooq.impl.Keywords.K_FROM;
import static org.jooq.impl.Keywords.K_IN;
import static org.jooq.impl.Keywords.K_INTO;
import static org.jooq.impl.Keywords.K_OPEN;
import static org.jooq.impl.Keywords.K_OUTPUT;
import static org.jooq.impl.Keywords.K_RETURNING;
import static org.jooq.impl.Keywords.K_ROWCOUNT;
import static org.jooq.impl.Keywords.K_SELECT;
import static org.jooq.impl.Keywords.K_SQL;
import static org.jooq.impl.Keywords.K_TABLE;
import static org.jooq.impl.Tools.EMPTY_FIELD;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_EMULATE_BULK_INSERT_RETURNING;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_UNALIAS_ALIASED_EXPRESSIONS;
import static org.jooq.impl.Tools.DataKey.DATA_DML_TARGET_TABLE;
import static org.jooq.tools.StringUtils.defaultIfNull;
import static org.jooq.util.sqlite.SQLiteDSL.rowid;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jooq.Asterisk;
import org.jooq.Binding;
import org.jooq.CommonTableExpression;
import org.jooq.Condition;
import org.jooq.Configuration;
import org.jooq.Context;
import org.jooq.DSLContext;
import org.jooq.DataType;
import org.jooq.Delete;
import org.jooq.ExecuteContext;
import org.jooq.ExecuteListener;
import org.jooq.Field;
import org.jooq.Identity;
import org.jooq.Insert;
import org.jooq.Name;
import org.jooq.Param;
// ...
import org.jooq.QualifiedAsterisk;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.SQLDialect;
import org.jooq.Scope;
import org.jooq.Select;
import org.jooq.SelectFieldOrAsterisk;
import org.jooq.Table;
import org.jooq.UniqueKey;
import org.jooq.Update;
import org.jooq.conf.ExecuteWithoutWhere;
import org.jooq.conf.RenderNameCase;
import org.jooq.conf.SettingsTools;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.Tools.BooleanDataKey;
import org.jooq.impl.Tools.DataKey;
import org.jooq.tools.JooqLogger;
import org.jooq.tools.jdbc.JDBCUtils;
/**
* @author Lukas Eder
*/
abstract class AbstractDMLQuery<R extends Record> extends AbstractRowCountQuery {
/**
* Generated UID
*/
private static final long serialVersionUID = -7438014075226919192L;
private static final JooqLogger log = JooqLogger.getLogger(AbstractQuery.class);
private static final Set<SQLDialect> NO_SUPPORT_INSERT_ALIASED_TABLE = SQLDialect.supportedBy(DERBY, FIREBIRD, H2, MARIADB, MYSQL);
private static final Set<SQLDialect> NATIVE_SUPPORT_INSERT_RETURNING = SQLDialect.supportedBy(FIREBIRD, MARIADB, POSTGRES);
private static final Set<SQLDialect> NATIVE_SUPPORT_UPDATE_RETURNING = SQLDialect.supportedBy(FIREBIRD, POSTGRES);
private static final Set<SQLDialect> NATIVE_SUPPORT_DELETE_RETURNING = SQLDialect.supportedBy(FIREBIRD, MARIADB, POSTGRES);
private final WithImpl with;
private final Table<R> table;
final SelectFieldList<SelectFieldOrAsterisk> returning;
final List<Field<?>> returningResolvedAsterisks;
Result<Record> returnedResult;
Result<R> returned;
AbstractDMLQuery(Configuration configuration, WithImpl with, Table<R> table) {
super(configuration);
this.with = with;
this.table = table;
this.returning = new SelectFieldList<>();
this.returningResolvedAsterisks = new ArrayList<>();
}
// ------------------------------------------------------------------------
// XXX: DSL API
// ------------------------------------------------------------------------
// @Override
public final void setReturning() {
setReturning(table.fields());
}
// @Override
public final void setReturning(Identity<R, ?> identity) {
if (identity != null)
setReturning(identity.getField());
}
// @Override
public final void setReturning(SelectFieldOrAsterisk... fields) {
setReturning(Arrays.asList(fields));
}
// @Override
public final void setReturning(Collection<? extends SelectFieldOrAsterisk> fields) {
returning.clear();
returning.addAll(fields);
returningResolvedAsterisks.clear();
for (SelectFieldOrAsterisk f : fields)
if (f instanceof Field<?>)
returningResolvedAsterisks.add((Field<?>) f);
else if (f instanceof QualifiedAsterisk)
returningResolvedAsterisks.addAll(Arrays.asList(((QualifiedAsterisk) f).qualifier().fields()));
else if (f instanceof Asterisk)
returningResolvedAsterisks.addAll(Arrays.asList(table.fields()));
else
throw new AssertionError("Type not supported: " + f);
}
// @Override
public final R getReturnedRecord() {
if (getReturnedRecords().isEmpty())
return null;
return getReturnedRecords().get(0);
}
// @Override
@SuppressWarnings("unchecked")
public final Result<R> getReturnedRecords() {
if (returned == null) {
// [#3682] Plain SQL tables do not have any fields
if (table.fields().length > 0) {
// [#7479] [#7475] Warn users about potential API misuse
warnOnAPIMisuse();
returned = getResult().into(table);
}
else {
returned = (Result<R>) getResult();
}
}
return returned;
}
private final void warnOnAPIMisuse() {
for (Field<?> field : getResult().fields())
if (table.field(field) == null)
log.warn("API misuse", "Column " + field + " has been requested through the returning() clause, which is not present in table " + table + ". Use StoreQuery.getResult() or the returningResult() clause instead.");
}
final Table<R> table() {
return table;
}
final Table<?> table(Context<?> ctx) {
// [#8382] [#8384] Table might be aliased and dialect doesn't like that
if (NO_SUPPORT_INSERT_ALIASED_TABLE.contains(ctx.dialect()) && this instanceof Insert)
return defaultIfNull(Tools.aliased(table()), table());
else
return table();
}
// @Override
public final Result<?> getResult() {
if (returnedResult == null)
returnedResult = new ResultImpl<>(configuration(), returningResolvedAsterisks);
return returnedResult;
}
// ------------------------------------------------------------------------
// XXX: QueryPart API
// ------------------------------------------------------------------------
@Override
public final void accept(Context<?> ctx) {
WithImpl w = with;
ctx.data(DATA_DML_TARGET_TABLE, table);
if (w != null)
ctx.visit(w).formatSeparator();
boolean previousDeclareFields = ctx.declareFields();
{
accept0(ctx);
}
ctx.data().remove(DATA_DML_TARGET_TABLE);
}
abstract void accept0(Context<?> ctx);
/**
* [#6771] Handle the case where a statement is executed without a WHERE clause.
*/
void executeWithoutWhere(String message, ExecuteWithoutWhere executeWithoutWhere) {
switch (executeWithoutWhere) {
case IGNORE:
break;
case LOG_DEBUG:
if (log.isDebugEnabled())
log.debug(message, "A statement is executed without WHERE clause");
break;
case LOG_INFO:
if (log.isInfoEnabled())
log.info(message, "A statement is executed without WHERE clause");
break;
case LOG_WARN:
log.warn(message, "A statement is executed without WHERE clause");
break;
case THROW:
throw new DataAccessException("A statement is executed without WHERE clause");
}
}
final void toSQLReturning(Context<?> ctx) {
if (!returning.isEmpty()) {
// Other dialects don't render a RETURNING clause, but
// use JDBC's Statement.RETURN_GENERATED_KEYS mode instead
if (nativeSupportReturning(ctx)) {
boolean declareFields = ctx.declareFields();
boolean qualify = ctx.qualify();
boolean unqualify = ctx.family() == MARIADB;
if (unqualify)
ctx.qualify(false);
ctx.formatSeparator()
.visit(K_RETURNING)
.sql(' ')
.declareFields(true)
.visit(
// Firebird doesn't support asterisks at all here
// MariaDB doesn't support qualified asterisks: https://jira.mariadb.org/browse/MDEV-23178
ctx.family() == FIREBIRD || ctx.family() == MARIADB
? new SelectFieldList<>(returningResolvedAsterisks)
: returning
)
.declareFields(declareFields);
if (unqualify)
ctx.qualify(qualify);
}
}
}
private final boolean nativeSupportReturning(Scope ctx) {
return this instanceof Insert && NATIVE_SUPPORT_INSERT_RETURNING.contains(ctx.dialect())
|| this instanceof Update && NATIVE_SUPPORT_UPDATE_RETURNING.contains(ctx.dialect())
|| this instanceof Delete && NATIVE_SUPPORT_DELETE_RETURNING.contains(ctx.dialect());
}
@Override
protected final void prepare(ExecuteContext ctx) throws SQLException {
prepare0(ctx);
Tools.setFetchSize(ctx, 0);
}
private final void prepare0(ExecuteContext ctx) throws SQLException {
Connection connection = ctx.connection();
if (returning.isEmpty()) {
super.prepare(ctx);
}
else if (nativeSupportReturning(ctx)) {
super.prepare(ctx);
}
else {
switch (ctx.family()) {
// SQLite will select last_insert_rowid() after the INSER
case SQLITE:
case CUBRID:
super.prepare(ctx);
break;
// Some dialects can only return AUTO_INCREMENT values
// Other values have to be fetched in a second step
// [#1260] TODO CUBRID supports this, but there's a JDBC bug
case DERBY:
case H2:
// [#9212] Older MariaDB versions that don't support RETURNING
// yet, or UPDATE .. RETURNING
case MARIADB:
case MYSQL:
ctx.statement(connection.prepareStatement(ctx.sql(), Statement.RETURN_GENERATED_KEYS));
break;
// The default is to return all requested fields directly
case HSQLDB:
default: {
String[] names = new String[returningResolvedAsterisks.size()];
RenderNameCase style = SettingsTools.getRenderNameCase(configuration().settings());
// [#2845] Field names should be passed to JDBC in the case
// imposed by the user. For instance, if the user uses
// 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.
if (style == RenderNameCase.UPPER)
for (int i = 0; i < names.length; i++)
names[i] = returningResolvedAsterisks.get(i).getName().toUpperCase(renderLocale(configuration().settings()));
else if (style == RenderNameCase.LOWER)
for (int i = 0; i < names.length; i++)
names[i] = returningResolvedAsterisks.get(i).getName().toLowerCase(renderLocale(configuration().settings()));
else
for (int i = 0; i < names.length; i++)
names[i] = returningResolvedAsterisks.get(i).getName();
ctx.statement(connection.prepareStatement(ctx.sql(), names));
break;
}
}
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
protected final int execute(ExecuteContext ctx, ExecuteListener listener) throws SQLException {
returned = null;
returnedResult = null;
if (returning.isEmpty()) {
return super.execute(ctx, listener);
}
else {
int result = 1;
ResultSet rs;
switch (ctx.family()) {
// SQLite can select _rowid_ after the insert
case SQLITE: {
listener.executeStart(ctx);
result = ctx.statement().executeUpdate();
ctx.rows(result);
listener.executeEnd(ctx);
DSLContext create = ctx.dsl();
returnedResult =
create.select(returning)
.from(table)
.where(rowid().equal(rowid().getDataType().convert(create.lastID())))
.fetch();
returnedResult.attach(((DefaultExecuteContext) ctx).originalConfiguration());
return result;
}
case CUBRID: {
listener.executeStart(ctx);
result = ctx.statement().executeUpdate();
ctx.rows(result);
listener.executeEnd(ctx);
selectReturning(
((DefaultExecuteContext) ctx).originalConfiguration(),
ctx.configuration(),
ctx.dsl().lastID()
);
return result;
}
// Some dialects can only retrieve "identity" (AUTO_INCREMENT) values
// Additional values have to be fetched explicitly
// [#1260] TODO CUBRID supports this, but there's a JDBC bug
case DERBY:
case H2:
case MYSQL: {
return executeReturningGeneratedKeysFetchAdditionalRows(ctx, listener);
}
case MARIADB: {
if (!nativeSupportReturning(ctx))
return executeReturningGeneratedKeysFetchAdditionalRows(ctx, listener);
rs = executeReturningQuery(ctx, listener);
break;
}
// Firebird and Postgres can execute the INSERT .. RETURNING
// clause like a select clause. JDBC support is not implemented
// in the Postgres JDBC driver
case FIREBIRD:
case POSTGRES: {
rs = executeReturningQuery(ctx, listener);
break;
}
// These dialects have full JDBC support
case HSQLDB:
default: {
rs = executeReturningGeneratedKeys(ctx, listener);
break;
}
}
ExecuteContext ctx2 = new DefaultExecuteContext(((DefaultExecuteContext) ctx).originalConfiguration());
ExecuteListener listener2 = ExecuteListeners.getAndStart(ctx2);
ctx2.resultSet(rs);
returnedResult = new CursorImpl<>(ctx2, listener2, returningResolvedAsterisks.toArray(EMPTY_FIELD), null, false, true).fetch();
// [#5366] HSQLDB currently doesn't support fetching updated records in UPDATE statements.
// [#5408] Other dialects may fall through the switch above (PostgreSQL, Firebird, Oracle) and must
// execute this logic
if (!returnedResult.isEmpty() || ctx.family() != HSQLDB) {
result = returnedResult.size();
ctx.rows(result);
}
return result;
}
}
private final ResultSet executeReturningGeneratedKeys(ExecuteContext ctx, ExecuteListener listener) throws SQLException {
listener.executeStart(ctx);
int result = ctx.statement().executeUpdate();
ctx.rows(result);
listener.executeEnd(ctx);
return ctx.statement().getGeneratedKeys();
}
private final int executeReturningGeneratedKeysFetchAdditionalRows(ExecuteContext ctx, ExecuteListener listener) throws SQLException {
ResultSet rs;
listener.executeStart(ctx);
int result = ctx.statement().executeUpdate();
ctx.rows(result);
listener.executeEnd(ctx);
try {
rs = ctx.statement().getGeneratedKeys();
}
catch (SQLException e) {
throw e;
}
try {
List<Object> list = new ArrayList<>();
// Some JDBC drivers seem to illegally return null
// from getGeneratedKeys() sometimes
if (rs != null)
while (rs.next())
list.add(rs.getObject(1));
selectReturning(
((DefaultExecuteContext) ctx).originalConfiguration(),
ctx.configuration(),
list.toArray()
);
return result;
}
finally {
JDBCUtils.safeClose(rs);
}
}
private final ResultSet executeReturningQuery(ExecuteContext ctx, ExecuteListener listener) throws SQLException {
listener.executeStart(ctx);
ResultSet rs = ctx.statement().executeQuery();
listener.executeEnd(ctx);
return rs;
}
/**
* Get the returning record in those dialects that do not support fetching
* arbitrary fields from JDBC's {@link Statement#getGeneratedKeys()} method.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private final void selectReturning(
Configuration originalConfiguration,
Configuration derivedConfiguration,
Object... values
) {
if (values != null && values.length > 0) {
final Field<Object> returnIdentity = (Field<Object>) returnedIdentity();
if (returnIdentity != null) {
Object[] ids = new Object[values.length];
for (int i = 0; i < values.length; i++)
ids[i] = returnIdentity.getDataType().convert(values[i]);
// Only the IDENTITY value was requested. No need for an
// additional query
if (returningResolvedAsterisks.size() == 1 && new Fields<>(returningResolvedAsterisks).field(returnIdentity) != null) {
for (final Object id : ids) {
((Result) getResult()).add(
Tools.newRecord(
true,
AbstractRecord.class,
returningResolvedAsterisks.toArray(EMPTY_FIELD),
originalConfiguration)
.operate(new RecordOperation<AbstractRecord, RuntimeException>() {
@Override
public AbstractRecord operate(AbstractRecord record) throws RuntimeException {
record.values[0] = id;
record.originals[0] = id;
return record;
}
}));
}
}
// Other values are requested, too. Run another query
else {
returnedResult =
derivedConfiguration.dsl()
.select(returning)
.from(table)
// [#5050] [#9946] Table.getIdentity() doesn't produce aliased fields yet
.where(table.field(returnIdentity).in(ids))
.fetch();
returnedResult.attach(originalConfiguration);
}
}
}
}
private final Field<?> returnedIdentity() {
if (table.getIdentity() != null)
return table.getIdentity().getField();
else
for (Field<?> field : returningResolvedAsterisks)
if (field.getDataType().identity())
return field;
return null;
}
}