[jOOQ/jOOQ#13912] Simplify SQL Server's RETURNING emulation in the absence of triggers

This includes:

- [jOOQ/jOOQ#11248] SQL Server trigger meta data support
This commit is contained in:
Lukas Eder 2023-11-23 14:37:42 +01:00
parent 54ae05e5a7
commit 3e276baf0b
13 changed files with 282 additions and 14 deletions

View File

@ -7312,6 +7312,34 @@ public class JavaGenerator extends AbstractGenerator {
}
}
// [#1596] Updatable tables can provide fields for optimistic locking if properly configured.
// [#7904] Records being updatable isn't a strict requirement. Version and timestamp values
// can still be generated

View File

@ -89,6 +89,7 @@ import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -316,6 +317,7 @@ public abstract class AbstractDatabase implements Database {
private transient Map<SchemaDefinition, List<XMLSchemaCollectionDefinition>> xmlSchemaCollectionsBySchema;
private transient Map<SchemaDefinition, List<UDTDefinition>> udtsBySchema;
private transient Map<PackageDefinition, List<UDTDefinition>> udtsByPackage;
@ -2918,6 +2920,14 @@ public abstract class AbstractDatabase implements Database {
@ -3069,16 +3079,26 @@ public abstract class AbstractDatabase implements Database {
if (indexesByTable == null)
indexesByTable = new HashMap<>();
List<IndexDefinition> list = indexesByTable.get(table);
return getTableObjects(table, indexesByTable, this::getIndexes, IndexDefinition::getTable);
}
private final <D extends Definition> List<D> getTableObjects(
TableDefinition table,
Map<TableDefinition, List<D>> map,
Function<? super SchemaDefinition, ? extends List<D>> f,
Function<? super D, ? extends TableDefinition> t
) {
List<D> list = map.get(table);
if (list == null) {
indexesByTable.put(table, list = new ArrayList<>());
map.put(table, list = new ArrayList<>());
for (TableDefinition otherTable : getTables(table.getSchema()))
if (!indexesByTable.containsKey(otherTable))
indexesByTable.put(otherTable, new ArrayList<>());
if (!map.containsKey(otherTable))
map.put(otherTable, new ArrayList<>());
for (IndexDefinition index : getIndexes(table.getSchema()))
indexesByTable.computeIfAbsent(index.getTable(), k -> new ArrayList<>()).add(index);
for (D d : f.apply(table.getSchema()))
map.computeIfAbsent(t.apply(d), k -> new ArrayList<>()).add(d);
}
return list;

View File

@ -236,6 +236,15 @@ implements
return getDatabase().getRelations().getCheckConstraints(this);
}
@Override
public final IdentityDefinition getIdentity() {
for (ColumnDefinition column : getColumns())

View File

@ -342,6 +342,12 @@ public interface Database extends AutoCloseable {

View File

@ -40,6 +40,7 @@ package org.jooq.meta;
import java.util.List;
// ...
import org.jooq.Record;
import org.jooq.Table;
import org.jooq.TableOptions;
@ -155,6 +156,16 @@ public interface TableDefinition extends Definition {
*/
List<CheckConstraintDefinition> getCheckConstraints();
/**
* Get the <code>IDENTITY</code> column of this table, or <code>null</code>,
* if no such column exists.

View File

@ -350,6 +350,14 @@ extends

View File

@ -127,6 +127,9 @@ public class Settings
protected Boolean bindOffsetTimeType = false;
@XmlElement(defaultValue = "true")
protected Boolean fetchTriggerValuesAfterSQLServerOutput = true;
@XmlElement(defaultValue = "WHEN_NEEDED")
@XmlSchemaType(name = "string")
protected FetchTriggerValuesAfterReturning fetchTriggerValuesAfterReturning = FetchTriggerValuesAfterReturning.WHEN_NEEDED;
@XmlElement(defaultValue = "WHEN_RESULT_REQUESTED")
@XmlSchemaType(name = "string")
protected FetchIntermediateResult fetchIntermediateResult = FetchIntermediateResult.WHEN_RESULT_REQUESTED;
@ -1145,7 +1148,7 @@ public class Settings
* is only supported for single row inserts.
* <p>
* This <code>OUTPUT</code> clause does not support fetching trigger generated values. In order
* to fetch trigger generated values, {@link #fetchTriggerValuesAfterSQLServerOutput} needs to
* to fetch trigger generated values, {@link #fetchTriggerValuesAfterReturning} needs to
* be enabled as well.
* <p>
* For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.
@ -1461,12 +1464,15 @@ public class Settings
* included in the <code>OUTPUT</code> clause.
* <p>
* For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.
* <p>
* @deprecated - 3.18.0 - [#13912] [#15316] - Use {@link #fetchTriggerValuesAfterReturning} instead.
*
* @return
* possible object is
* {@link Boolean }
*
*/
@Deprecated
public Boolean isFetchTriggerValuesAfterSQLServerOutput() {
return fetchTriggerValuesAfterSQLServerOutput;
}
@ -1479,10 +1485,47 @@ public class Settings
* {@link Boolean }
*
*/
@Deprecated
public void setFetchTriggerValuesAfterSQLServerOutput(Boolean value) {
this.fetchTriggerValuesAfterSQLServerOutput = value;
}
/**
* Fetch trigger values after a <code>RETURNING</code> clause in dialects that don't have native support for this.
* <p>
* SQL Server <code>OUTPUT</code> clauses do not support fetching trigger generated values.
* Neither do SQLite <code>RETURNING</code> clauses. An additional
* <code>MERGE</code> statement can run a second query if (and only if) the primary key has been
* included in the <code>OUTPUT</code> clause.
* <p>
* Trigger meta data is only available in jOOQ's commercial editions. If setting this flag to
* <code>WHEN_NEEDED</code> in the jOOQ Open Source Edition, jOOQ will assume triggers are present.
* <p>
* For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.
*
*/
public FetchTriggerValuesAfterReturning getFetchTriggerValuesAfterReturning() {
return fetchTriggerValuesAfterReturning;
}
/**
* Fetch trigger values after a <code>RETURNING</code> clause in dialects that don't have native support for this.
* <p>
* SQL Server <code>OUTPUT</code> clauses do not support fetching trigger generated values.
* Neither do SQLite <code>RETURNING</code> clauses. An additional
* <code>MERGE</code> statement can run a second query if (and only if) the primary key has been
* included in the <code>OUTPUT</code> clause.
* <p>
* Trigger meta data is only available in jOOQ's commercial editions. If setting this flag to
* <code>WHEN_NEEDED</code> in the jOOQ Open Source Edition, jOOQ will assume triggers are present.
* <p>
* For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.
*
*/
public void setFetchTriggerValuesAfterReturning(FetchTriggerValuesAfterReturning value) {
this.fetchTriggerValuesAfterReturning = value;
}
/**
* Whether to fetch data into intermediate {@link org.jooq.Result} instances.
* <p>
@ -6373,6 +6416,25 @@ public class Settings
return this;
}
/**
* Fetch trigger values after a <code>RETURNING</code> clause in dialects that don't have native support for this.
* <p>
* SQL Server <code>OUTPUT</code> clauses do not support fetching trigger generated values.
* Neither do SQLite <code>RETURNING</code> clauses. An additional
* <code>MERGE</code> statement can run a second query if (and only if) the primary key has been
* included in the <code>OUTPUT</code> clause.
* <p>
* Trigger meta data is only available in jOOQ's commercial editions. If setting this flag to
* <code>WHEN_NEEDED</code> in the jOOQ Open Source Edition, jOOQ will assume triggers are present.
* <p>
* For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.
*
*/
public Settings withFetchTriggerValuesAfterReturning(FetchTriggerValuesAfterReturning value) {
setFetchTriggerValuesAfterReturning(value);
return this;
}
/**
* Whether to fetch data into intermediate {@link org.jooq.Result} instances.
* <p>
@ -7705,6 +7767,7 @@ public class Settings
builder.append("bindOffsetDateTimeType", bindOffsetDateTimeType);
builder.append("bindOffsetTimeType", bindOffsetTimeType);
builder.append("fetchTriggerValuesAfterSQLServerOutput", fetchTriggerValuesAfterSQLServerOutput);
builder.append("fetchTriggerValuesAfterReturning", fetchTriggerValuesAfterReturning);
builder.append("fetchIntermediateResult", fetchIntermediateResult);
builder.append("diagnosticsDuplicateStatements", diagnosticsDuplicateStatements);
builder.append("diagnosticsDuplicateStatementsUsingTransformPatterns", diagnosticsDuplicateStatementsUsingTransformPatterns);
@ -8253,6 +8316,15 @@ public class Settings
return false;
}
}
if (fetchTriggerValuesAfterReturning == null) {
if (other.fetchTriggerValuesAfterReturning!= null) {
return false;
}
} else {
if (!fetchTriggerValuesAfterReturning.equals(other.fetchTriggerValuesAfterReturning)) {
return false;
}
}
if (fetchIntermediateResult == null) {
if (other.fetchIntermediateResult!= null) {
return false;
@ -9963,6 +10035,7 @@ public class Settings
result = ((prime*result)+((bindOffsetDateTimeType == null)? 0 :bindOffsetDateTimeType.hashCode()));
result = ((prime*result)+((bindOffsetTimeType == null)? 0 :bindOffsetTimeType.hashCode()));
result = ((prime*result)+((fetchTriggerValuesAfterSQLServerOutput == null)? 0 :fetchTriggerValuesAfterSQLServerOutput.hashCode()));
result = ((prime*result)+((fetchTriggerValuesAfterReturning == null)? 0 :fetchTriggerValuesAfterReturning.hashCode()));
result = ((prime*result)+((fetchIntermediateResult == null)? 0 :fetchIntermediateResult.hashCode()));
result = ((prime*result)+((diagnosticsDuplicateStatements == null)? 0 :diagnosticsDuplicateStatements.hashCode()));
result = ((prime*result)+((diagnosticsDuplicateStatementsUsingTransformPatterns == null)? 0 :diagnosticsDuplicateStatementsUsingTransformPatterns.hashCode()));

View File

@ -45,10 +45,8 @@ import static java.lang.Boolean.FALSE;
import static org.jooq.SQLDialect.CUBRID;
// ...
import static org.jooq.SQLDialect.DERBY;
import static org.jooq.SQLDialect.DUCKDB;
// ...
import static org.jooq.SQLDialect.FIREBIRD;
// ...
import static org.jooq.SQLDialect.H2;
// ...
// ...
@ -77,7 +75,6 @@ import static org.jooq.impl.CommonTableExpressionList.markTopLevelCteAndAccept;
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.InlineDerivedTable.inlineDerivedTable;
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;
@ -98,7 +95,6 @@ import static org.jooq.impl.Names.N_DELETED;
import static org.jooq.impl.Names.N_INSERTED;
import static org.jooq.impl.Tools.EMPTY_FIELD;
import static org.jooq.impl.Tools.EMPTY_STRING;
import static org.jooq.impl.Tools.aliased;
import static org.jooq.impl.Tools.anyMatch;
import static org.jooq.impl.Tools.autoAlias;
import static org.jooq.impl.Tools.flattenCollection;
@ -107,11 +103,13 @@ import static org.jooq.impl.Tools.map;
import static org.jooq.impl.Tools.reference;
import static org.jooq.impl.Tools.removeGenerator;
import static org.jooq.impl.Tools.unalias;
import static org.jooq.impl.Tools.updateQueryImpl;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_UNALIAS_ALIASED_EXPRESSIONS;
import static org.jooq.impl.Tools.SimpleDataKey.DATA_DML_TARGET_TABLE;
import static org.jooq.impl.Tools.SimpleDataKey.DATA_DML_USING_TABLES;
import static org.jooq.impl.Tools.SimpleDataKey.DATA_RENDERING_DATA_CHANGE_DELTA_TABLE;
import static org.jooq.impl.Tools.SimpleDataKey.DATA_TOP_LEVEL_CTE;
import static org.jooq.tools.StringUtils.defaultIfNull;
import static org.jooq.util.sqlite.SQLiteDSL.rowid;
import java.sql.CallableStatement;
@ -123,6 +121,7 @@ import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@ -160,9 +159,12 @@ import org.jooq.Select;
import org.jooq.SelectFieldOrAsterisk;
import org.jooq.Table;
import org.jooq.TableField;
// ...
// ...
import org.jooq.UniqueKey;
import org.jooq.Update;
import org.jooq.conf.ExecuteWithoutWhere;
import org.jooq.conf.FetchTriggerValuesAfterReturning;
import org.jooq.conf.RenderNameCase;
import org.jooq.conf.SettingsTools;
import org.jooq.exception.DataAccessException;
@ -673,6 +675,44 @@ abstract class AbstractDMLQuery<R extends Record> extends AbstractRowCountQuery
ctx.data().remove(DATA_DML_TARGET_TABLE);
}
@Pro
private final boolean fetchTriggerValuesAfterReturning(Scope ctx) {
if (this instanceof Delete)
return false;
if (FALSE.equals(ctx.settings().isFetchTriggerValuesAfterSQLServerOutput()))
return false;
switch (defaultIfNull(ctx.settings().getFetchTriggerValuesAfterReturning(), FetchTriggerValuesAfterReturning.WHEN_NEEDED)) {
case ALWAYS:
return true;
case NEVER:
return false;
case WHEN_NEEDED:
if (ctx.configuration().commercial()) {
}
return true;
default:
throw new IllegalStateException("Unsupported value: " + ctx.settings().getFetchTriggerValuesAfterReturning());
}
}
@ -857,6 +897,10 @@ abstract class AbstractDMLQuery<R extends Record> extends AbstractRowCountQuery

View File

@ -122,6 +122,7 @@ import org.jooq.TableOptions;
import org.jooq.TableOptions.TableType;
import org.jooq.TableOuterJoinStep;
import org.jooq.TablePartitionByStep;
// ...
import org.jooq.UniqueKey;
// ...
// ...
@ -624,6 +625,20 @@ implements
return Collections.emptyList();
}
/**
* Subclasses may call this method to create {@link TableField} objects that
* are linked to this table.

View File

@ -913,6 +913,13 @@ package org.jooq.impl;

View File

@ -98,6 +98,7 @@ import static org.jooq.impl.Keywords.K_SET;
import static org.jooq.impl.Keywords.K_UPDATE;
import static org.jooq.impl.Keywords.K_WHERE;
import static org.jooq.impl.SQLDataType.INTEGER;
import static org.jooq.impl.Tools.anyMatch;
import static org.jooq.impl.Tools.containsDeclaredTable;
import static org.jooq.impl.Tools.findAny;
@ -695,6 +696,14 @@ implements
ctx.visit(mergeInto(table).using(s).on(c).whenMatchedThenUpdate().set(um));
}
final boolean updatesField(Field<?> field) {
return anyMatch(updateMap.keySet(), fr -> field.equals(fr) || fr instanceof Row && ((Row) fr).field(field) != null);
}
final boolean updatesAnyField(Collection<? extends Field<?>> fields) {
return anyMatch(fields, this::updatesField);
}
final UpdateQueryImpl<R> copy(Consumer<? super UpdateQueryImpl<R>> finisher) {
return copy(finisher, table);
}

View File

@ -137,6 +137,5 @@ package org.jooq.impl;

View File

@ -215,7 +215,7 @@ to revert to jOOQ calling {@code java.sql.Statement#getGeneratedKeys()} instead,
is only supported for single row inserts.
<p>
This <code>OUTPUT</code> clause does not support fetching trigger generated values. In order
to fetch trigger generated values, {@link #fetchTriggerValuesAfterSQLServerOutput} needs to
to fetch trigger generated values, {@link #fetchTriggerValuesAfterReturning} needs to
be enabled as well.
<p>
For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.]]></jxb:javadoc></jxb:property></appinfo></annotation>
@ -308,13 +308,37 @@ For details, see <a href="https://github.com/jOOQ/jOOQ/issues/9902">https://gith
</element>
<element name="fetchTriggerValuesAfterSQLServerOutput" type="boolean" minOccurs="0" maxOccurs="1" default="true">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Fetch trigger values after SQL Server <code>OUTPUT</code> clause.
<annotation>
<appinfo>
<jxb:property>
<jxb:javadoc><![CDATA[Fetch trigger values after SQL Server <code>OUTPUT</code> clause.
<p>
SQL Server <code>OUTPUT</code> statements do not support fetching trigger generated values.
This is a limitation of the {@link #renderOutputForSQLServerReturningClause}. An additional
<code>MERGE</code> statement can run a second query if (and only if) the primary key has been
included in the <code>OUTPUT</code> clause.
<p>
For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.
<p>
@deprecated - 3.18.0 - [#13912] [#15316] - Use {@link #fetchTriggerValuesAfterReturning} instead.]]></jxb:javadoc>
</jxb:property>
<annox:annotate target="getter">@java.lang.Deprecated</annox:annotate>
<annox:annotate target="setter">@java.lang.Deprecated</annox:annotate>
</appinfo>
</annotation>
</element>
<element name="fetchTriggerValuesAfterReturning" type="jooq-runtime:FetchTriggerValuesAfterReturning" minOccurs="0" maxOccurs="1" default="WHEN_NEEDED">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Fetch trigger values after a <code>RETURNING</code> clause in dialects that don't have native support for this.
<p>
SQL Server <code>OUTPUT</code> clauses do not support fetching trigger generated values.
Neither do SQLite <code>RETURNING</code> clauses. An additional
<code>MERGE</code> statement can run a second query if (and only if) the primary key has been
included in the <code>OUTPUT</code> clause.
<p>
Trigger meta data is only available in jOOQ's commercial editions. If setting this flag to
<code>WHEN_NEEDED</code> in the jOOQ Open Source Edition, jOOQ will assume triggers are present.
<p>
For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
@ -2412,4 +2436,19 @@ Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided]]></jxb:javad
<enumeration value="THROW"/>
</restriction>
</simpleType>
<simpleType name="FetchTriggerValuesAfterReturning">
<restriction base="string">
<!-- Never fetch trigger values after returning. -->
<enumeration value="NEVER"/>
<!-- Fetch trigger values only when triggers are known to be present.
Trigger meta data is only available in jOOQ's commercial editions -->
<enumeration value="WHEN_NEEDED"/>
<!-- Always fetch trigger values. -->
<enumeration value="ALWAYS"/>
</restriction>
</simpleType>
</schema>