[jOOQ/jOOQ#8917] INSERT .. RETURNING emulation doesn't work on SQL

Server when returning expressions or aliases

This includes:

- [jOOQ/jOOQ#12071] Add an internal means of ad-hoc table mapping
This commit is contained in:
Lukas Eder 2021-06-29 10:29:23 +02:00
parent bceaf5df62
commit f033d6eadf
14 changed files with 141 additions and 84 deletions

View File

@ -87,14 +87,13 @@ 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.ScopeMarker.TOP_LEVEL_CTE;
import static org.jooq.impl.Tools.EMPTY_FIELD;
import static org.jooq.impl.Tools.EMPTY_STRING;
import static org.jooq.impl.Tools.anyMatch;
import static org.jooq.impl.Tools.autoAlias;
import static org.jooq.impl.Tools.findAny;
import static org.jooq.impl.Tools.flattenCollection;
import static org.jooq.impl.Tools.map;
import static org.jooq.impl.Tools.qualify;
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;
@ -120,6 +119,8 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.jooq.Asterisk;
import org.jooq.Binding;
@ -143,13 +144,18 @@ import org.jooq.Record;
import org.jooq.Result;
import org.jooq.Row;
import org.jooq.SQLDialect;
import org.jooq.SchemaMapping;
import org.jooq.Scope;
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.MappedSchema;
import org.jooq.conf.MappedTable;
import org.jooq.conf.RenderMapping;
import org.jooq.conf.RenderNameCase;
import org.jooq.conf.SettingsTools;
import org.jooq.exception.DataAccessException;
@ -590,6 +596,23 @@ abstract class AbstractDMLQuery<R extends Record> extends AbstractRowCountQuery
@ -772,6 +795,17 @@ abstract class AbstractDMLQuery<R extends Record> extends AbstractRowCountQuery

View File

@ -37,24 +37,21 @@
*/
package org.jooq.impl;
import static org.jooq.impl.DefaultExecuteContext.localConfiguration;
import static org.jooq.impl.DefaultExecuteContext.localData;
import static org.jooq.impl.DefaultExecuteContext.localScope;
import static org.jooq.impl.Tools.fieldsArray;
import static org.jooq.impl.Tools.getMappedUDTName;
import static org.jooq.impl.Tools.row0;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;
import java.util.Map;
import org.jooq.Configuration;
import org.jooq.Converter;
import org.jooq.Field;
import org.jooq.QualifiedRecord;
import org.jooq.Record;
import org.jooq.RecordQualifier;
import org.jooq.Row;
import org.jooq.UDT;
import org.jooq.Scope;
/**
* @author Lukas Eder
@ -115,20 +112,18 @@ abstract class AbstractQualifiedRecord<R extends QualifiedRecord<R>> extends Abs
// [#1693] This needs to return the fully qualified SQL type name, in
// case the connected user is not the owner of the UDT
Configuration configuration = localConfiguration();
return Tools.getMappedUDTName(configuration, this);
return getMappedUDTName(localScope(), this);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public final void readSQL(SQLInput stream, String typeName) throws SQLException {
Configuration configuration = localConfiguration();
Map<Object, Object> data = localData();
Scope scope = localScope();
Field<?>[] f = getQualifier().fields();
for (int i = 0; i < f.length; i++) {
Field field = f[i];
DefaultBindingGetSQLInputContext out = new DefaultBindingGetSQLInputContext(configuration, data, stream);
DefaultBindingGetSQLInputContext out = new DefaultBindingGetSQLInputContext(scope.configuration(), scope.data(), stream);
field.getBinding().get(out);
set(i, field, out.value());
}
@ -137,13 +132,10 @@ abstract class AbstractQualifiedRecord<R extends QualifiedRecord<R>> extends Abs
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public final void writeSQL(SQLOutput stream) throws SQLException {
Configuration configuration = localConfiguration();
Map<Object, Object> data = localData();
Scope scope = localScope();
Field<?>[] f = getQualifier().fields();
for (int i = 0; i < f.length; i++) {
Field field = f[i];
field.getBinding().set(new DefaultBindingSetSQLOutputContext(configuration, data, stream, get(i)));
}
for (int i = 0; i < f.length; i++)
f[i].getBinding().set(new DefaultBindingSetSQLOutputContext(scope.configuration(), scope.data(), stream, get(i)));
}
}

View File

@ -1175,7 +1175,7 @@ public abstract class AbstractRoutine<T> extends AbstractNamed implements Routin
List<Name> list = new ArrayList<>();
if (ctx.qualify()) {
Schema mapped = Tools.getMappedSchema(ctx.configuration(), getSchema());
Schema mapped = Tools.getMappedSchema(ctx, getSchema());
if (mapped != null && !"".equals(mapped.getName()))
list.addAll(asList(mapped.getQualifiedName().parts()));

View File

@ -39,6 +39,7 @@ package org.jooq.impl;
import static org.jooq.Clause.CATALOG;
import static org.jooq.Clause.CATALOG_REFERENCE;
import static org.jooq.impl.Tools.getMappedCatalog;
import java.util.Collections;
import java.util.List;
@ -82,7 +83,7 @@ public class CatalogImpl extends AbstractNamed implements Catalog {
@Override
public final void accept(Context<?> ctx) {
Catalog mappedCatalog = Tools.getMappedCatalog(ctx.configuration(), this);
Catalog mappedCatalog = getMappedCatalog(ctx, this);
ctx.visit(mappedCatalog != null ? mappedCatalog.getUnqualifiedName() : getUnqualifiedName());
}

View File

@ -128,6 +128,7 @@ import static org.jooq.impl.Tools.convertBytesToHex;
import static org.jooq.impl.Tools.emulateMultiset;
import static org.jooq.impl.Tools.enums;
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;
@ -511,19 +512,19 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
return theBinding;
}
static final Map<String, Class<?>> typeMap(Class<?> type, Configuration configuration) {
return typeMap(type, configuration, new HashMap<>());
static final Map<String, Class<?>> typeMap(Class<?> type, Scope scope) {
return typeMap(type, scope, new HashMap<>());
}
@SuppressWarnings("unchecked")
static final Map<String, Class<?>> typeMap(Class<?> type, Configuration configuration, Map<String, Class<?>> result) {
static final Map<String, Class<?>> typeMap(Class<?> type, Scope scope, Map<String, Class<?>> result) {
try {
if (QualifiedRecord.class.isAssignableFrom(type)) {
Class<QualifiedRecord<?>> t = (Class<QualifiedRecord<?>>) type;
result.put(getMappedUDTName(configuration, t), t);
result.put(getMappedUDTName(scope, t), t);
QualifiedRecord<?> r = t.getDeclaredConstructor().newInstance();
for (Field<?> field : r.getQualifier().fields())
typeMap(field.getType(), configuration, result);
typeMap(field.getType(), scope, result);
}
@ -3561,7 +3562,7 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
return pgNewRecord(dataType.getType(), null, ctx.resultSet().getObject(ctx.index()));
default:
return (Record) ctx.resultSet().getObject(ctx.index(), typeMap(dataType.getType(), ctx.configuration()));
return (Record) ctx.resultSet().getObject(ctx.index(), typeMap(dataType.getType(), ctx));
}
}
@ -3574,7 +3575,7 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
return pgNewRecord(dataType.getType(), null, ctx.statement().getObject(ctx.index()));
default:
return (Record) ctx.statement().getObject(ctx.index(), typeMap(dataType.getType(), ctx.configuration()));
return (Record) ctx.statement().getObject(ctx.index(), typeMap(dataType.getType(), ctx));
}
}

View File

@ -61,6 +61,8 @@ import static org.jooq.impl.Tools.EMPTY_TABLE;
import static org.jooq.impl.Tools.EMPTY_TABLE_RECORD;
import static org.jooq.impl.Tools.EMPTY_UPDATABLE_RECORD;
import static org.jooq.impl.Tools.blocking;
import static org.jooq.impl.Tools.getMappedSchema;
import static org.jooq.impl.Tools.getMappedTable;
import static org.jooq.impl.Tools.list;
import java.io.ByteArrayInputStream;
@ -350,12 +352,12 @@ public class DefaultDSLContext extends AbstractScope implements DSLContext, Seri
@Override
public Schema map(Schema schema) {
return Tools.getMappedSchema(configuration(), schema);
return getMappedSchema(this, schema);
}
@Override
public <R extends Record> Table<R> map(Table<R> table) {
return Tools.getMappedTable(configuration(), table);
return getMappedTable(this, table);
}
// -------------------------------------------------------------------------

View File

@ -42,8 +42,6 @@ import static org.jooq.impl.Tools.EMPTY_INT;
import static org.jooq.impl.Tools.EMPTY_QUERY;
import static org.jooq.impl.Tools.EMPTY_STRING;
import java.io.Closeable;
import java.io.IOException;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
@ -69,13 +67,13 @@ import org.jooq.ExecuteContext;
import org.jooq.ExecuteType;
import org.jooq.Insert;
import org.jooq.Merge;
// ...
import org.jooq.Query;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.ResultQuery;
import org.jooq.Routine;
import org.jooq.SQLDialect;
import org.jooq.Scope;
import org.jooq.Update;
import org.jooq.conf.Settings;
import org.jooq.tools.JooqLogger;
@ -83,8 +81,6 @@ import org.jooq.tools.jdbc.JDBCUtils;
import org.jetbrains.annotations.NotNull;
// ...
/**
* A default implementation for the {@link ExecuteContext}.
*
@ -171,8 +167,7 @@ class DefaultExecuteContext implements ExecuteContext {
RESOURCES.remove();
}
LOCAL_CONFIGURATION.remove();
LOCAL_DATA.remove();
LOCAL_SCOPE.remove();
LOCAL_CONNECTION.remove();
}
@ -222,29 +217,17 @@ class DefaultExecuteContext implements ExecuteContext {
// XXX: Static utility methods for handling Configuration lifecycle
// ------------------------------------------------------------------------
private static final ThreadLocal<Configuration> LOCAL_CONFIGURATION = new ThreadLocal<>();
private static final ThreadLocal<Map<Object, Object>> LOCAL_DATA = new ThreadLocal<>();
private static final ThreadLocal<Scope> LOCAL_SCOPE = new ThreadLocal<>();
/**
* Get the registered configuration.
* Get the registered scope.
* <p>
* It can be safely assumed that such a configuration is available once the
* {@link ExecuteContext} has been established, until the statement is
* closed.
*/
static final Configuration localConfiguration() {
return LOCAL_CONFIGURATION.get();
}
/**
* Get the registered data.
* <p>
* It can be safely assumed that such a configuration is available once the
* {@link ExecuteContext} has been established, until the statement is
* closed.
*/
static final Map<Object, Object> localData() {
return LOCAL_DATA.get();
static final Scope localScope() {
return LOCAL_SCOPE.get();
}
// ------------------------------------------------------------------------
@ -272,7 +255,7 @@ class DefaultExecuteContext implements ExecuteContext {
* {@link ExecuteContext} has been established, until the statement is
* closed.
*/
static final Connection localTargetConnection(Configuration configuration) {
static final Connection localTargetConnection(Scope scope) {
Connection result = localConnection();
@ -360,8 +343,7 @@ class DefaultExecuteContext implements ExecuteContext {
}
clean();
LOCAL_CONFIGURATION.set(configuration);
LOCAL_DATA.set(this.data);
LOCAL_SCOPE.set(this);
}
@Override

View File

@ -41,6 +41,7 @@ package org.jooq.impl;
import static org.jooq.conf.ParamType.INLINED;
import static org.jooq.impl.DSL.val;
import static org.jooq.impl.Keywords.K_ROW;
import static org.jooq.impl.Tools.getMappedUDTName;
import org.jooq.BindContext;
import org.jooq.Context;
@ -160,7 +161,7 @@ final class QualifiedRecordConstant<R extends QualifiedRecord<R>> extends Abstra
return "ROW";
default:
return Tools.getMappedUDTName(ctx.configuration(), value);
return getMappedUDTName(ctx, value);
}
}

View File

@ -42,6 +42,8 @@ import static org.jooq.Clause.SCHEMA;
import static org.jooq.Clause.SCHEMA_REFERENCE;
import static org.jooq.impl.CatalogImpl.DEFAULT_CATALOG;
import static org.jooq.impl.Tools.flatMap;
import static org.jooq.impl.Tools.getMappedCatalog;
import static org.jooq.impl.Tools.getMappedSchema;
import static org.jooq.tools.StringUtils.defaultIfNull;
import java.util.ArrayList;
@ -118,15 +120,13 @@ public class SchemaImpl extends AbstractNamed implements Schema {
@Override
public final void accept(Context<?> ctx) {
if (ctx.qualifyCatalog()) {
Catalog mappedCatalog = Tools.getMappedCatalog(ctx.configuration(), getCatalog());
Catalog mappedCatalog = getMappedCatalog(ctx, getCatalog());
if (mappedCatalog != null && !"".equals(mappedCatalog.getName())) {
ctx.visit(mappedCatalog);
ctx.sql('.');
}
if (mappedCatalog != null && !"".equals(mappedCatalog.getName()))
ctx.visit(mappedCatalog).sql('.');
}
Schema mappedSchema = Tools.getMappedSchema(ctx.configuration(), this);
Schema mappedSchema = getMappedSchema(ctx, this);
ctx.visit(mappedSchema != null ? mappedSchema.getUnqualifiedName() : getUnqualifiedName());
}

View File

@ -185,6 +185,7 @@ import static org.jooq.impl.Tools.EMPTY_SORTFIELD;
import static org.jooq.impl.Tools.aliased;
import static org.jooq.impl.Tools.aliasedFields;
import static org.jooq.impl.Tools.anyMatch;
import static org.jooq.impl.Tools.autoAlias;
import static org.jooq.impl.Tools.camelCase;
import static org.jooq.impl.Tools.fieldArray;
import static org.jooq.impl.Tools.findAny;
@ -666,7 +667,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
public final Table<R> asTable() {
// Its usually better to alias nested selects that are used in
// the FROM clause of a query
return new DerivedTable<>(this).as("alias_" + Tools.hash(this));
return new DerivedTable<>(this).as(autoAlias(this));
}
@Override

View File

@ -58,6 +58,7 @@ import static org.jooq.impl.Names.N_CURRVAL;
import static org.jooq.impl.Names.N_GENERATE_SERIES;
import static org.jooq.impl.Names.N_GEN_ID;
import static org.jooq.impl.Names.N_NEXTVAL;
import static org.jooq.impl.Tools.getMappedSchema;
import org.jooq.Catalog;
import org.jooq.Clause;
@ -328,7 +329,7 @@ public class SequenceImpl<T extends Number> extends AbstractTypedNamed<T> implem
Schema mappedSchema = Tools.getMappedSchema(ctx.configuration(), schema);
Schema mappedSchema = getMappedSchema(ctx, schema);
if (mappedSchema != null && !"".equals(mappedSchema.getName()) && ctx.family() != CUBRID)
ctx.visit(mappedSchema)

View File

@ -308,7 +308,7 @@ public class TableImpl<R extends Record> extends AbstractTable<R> implements Sco
// [#4834] Generate alias only if allowed to do so
if (ctx.declareAliases())
ctx.sql(' ')
.visit(Tools.getMappedTable(ctx.configuration(), this).getUnqualifiedName());
.visit(getMappedTable(ctx, this).getUnqualifiedName());
}
else
accept0(ctx);
@ -330,7 +330,7 @@ public class TableImpl<R extends Record> extends AbstractTable<R> implements Sco
)) {
Schema mappedSchema = Tools.getMappedSchema(ctx.configuration(), getSchema());
Schema mappedSchema = Tools.getMappedSchema(ctx, getSchema());
if (mappedSchema != null && !"".equals(mappedSchema.getName())) {
ctx.visit(mappedSchema);
@ -338,7 +338,7 @@ public class TableImpl<R extends Record> extends AbstractTable<R> implements Sco
}
}
ctx.visit(getMappedTable(ctx.configuration(), this).getUnqualifiedName());
ctx.visit(getMappedTable(ctx, this).getUnqualifiedName());
if (parameters != null && ctx.declareTables()) {

View File

@ -277,6 +277,7 @@ import org.jooq.Results;
import org.jooq.Row;
import org.jooq.SQLDialect;
import org.jooq.Schema;
import org.jooq.SchemaMapping;
import org.jooq.Scope;
import org.jooq.Select;
import org.jooq.SelectFieldOrAsterisk;
@ -316,6 +317,8 @@ import org.jooq.types.UInteger;
import org.jooq.types.ULong;
import org.jooq.types.UShort;
import org.jetbrains.annotations.Nullable;
/**
* General internal jOOQ utilities
*
@ -634,7 +637,13 @@ final class Tools {
* [#10540] Aliases to be applied to the current <code>SELECT</code>
* statement.
*/
DATA_SELECT_ALIASES
DATA_SELECT_ALIASES,
/**
* [#8917] An internal schema mapping that overrides any user-defined
* schema mappings.
*/
DATA_SCHEMA_MAPPING,
}
/**
@ -2995,9 +3004,12 @@ final class Tools {
/**
* Map a {@link Catalog} according to the configured {@link org.jooq.SchemaMapping}
*/
static final Catalog getMappedCatalog(Configuration configuration, Catalog catalog) {
if (configuration != null) {
org.jooq.SchemaMapping mapping = configuration.schemaMapping();
static final Catalog getMappedCatalog(Scope scope, Catalog catalog) {
if (scope != null) {
org.jooq.SchemaMapping mapping = (SchemaMapping) scope.data(DataKey.DATA_SCHEMA_MAPPING);
if (mapping == null)
mapping = scope.configuration().schemaMapping();
if (mapping != null)
return mapping.map(catalog);
@ -3009,9 +3021,12 @@ final class Tools {
/**
* Map a {@link Schema} according to the configured {@link org.jooq.SchemaMapping}
*/
static final Schema getMappedSchema(Configuration configuration, Schema schema) {
if (configuration != null) {
org.jooq.SchemaMapping mapping = configuration.schemaMapping();
static final Schema getMappedSchema(Scope scope, Schema schema) {
if (scope != null) {
org.jooq.SchemaMapping mapping = (SchemaMapping) scope.data(DataKey.DATA_SCHEMA_MAPPING);
if (mapping == null)
mapping = scope.configuration().schemaMapping();
if (mapping != null)
return mapping.map(schema);
@ -3023,9 +3038,12 @@ final class Tools {
/**
* Map a {@link Table} according to the configured {@link org.jooq.SchemaMapping}
*/
static final <R extends Record> Table<R> getMappedTable(Configuration configuration, Table<R> table) {
if (configuration != null) {
org.jooq.SchemaMapping mapping = configuration.schemaMapping();
static final <R extends Record> Table<R> getMappedTable(Scope scope, Table<R> table) {
if (scope != null) {
org.jooq.SchemaMapping mapping = (SchemaMapping) scope.data(DataKey.DATA_SCHEMA_MAPPING);
if (mapping == null)
mapping = scope.configuration().schemaMapping();
if (mapping != null)
return mapping.map(table);
@ -3039,17 +3057,17 @@ final class Tools {
* {@link org.jooq.SchemaMapping}
*/
@SuppressWarnings("unchecked")
static final String getMappedUDTName(Configuration configuration, Class<? extends QualifiedRecord<?>> type) {
return getMappedUDTName(configuration, Tools.newRecord(false, (Class<QualifiedRecord<?>>) type).operate(null));
static final String getMappedUDTName(Scope scope, Class<? extends QualifiedRecord<?>> type) {
return getMappedUDTName(scope, Tools.newRecord(false, (Class<QualifiedRecord<?>>) type).operate(null));
}
/**
* Map an {@link QualifiedRecord} according to the configured
* {@link org.jooq.SchemaMapping}
*/
static final String getMappedUDTName(Configuration configuration, QualifiedRecord<?> record) {
static final String getMappedUDTName(Scope scope, QualifiedRecord<?> record) {
RecordQualifier<?> udt = record.getQualifier();
Schema mapped = getMappedSchema(configuration, udt.getSchema());
Schema mapped = getMappedSchema(scope, udt.getSchema());
StringBuilder sb = new StringBuilder();
if (mapped != null && !"".equals(mapped.getName()))
@ -3121,7 +3139,29 @@ final class Tools {
static final DSLContext CTX = DSL.using(new DefaultConfiguration());
static final DSLContext CTX = DSL.using(new DefaultConfiguration());
/**
* A possibly inefficient but stable way to generate an alias for any
* {@link QueryPart}.
* <p>
* Stability is important to profit from execution plan caching. Equal query
* parts must produce the same alias every time.
*/
static final String autoAlias(QueryPart part) {
return "alias_" + hash(part);
}
/**
* A possibly inefficient but stable way to generate an alias for any
* {@link QueryPart}.
* <p>
* Stability is important to profit from execution plan caching. Equal query
* parts must produce the same alias every time.
*/
static final Name autoAliasName(QueryPart part) {
return DSL.name(autoAlias(part));
}
/**
* Return a non-negative hash code for a {@link QueryPart}, taking into

View File

@ -37,6 +37,8 @@
*/
package org.jooq.impl;
import static org.jooq.impl.Tools.getMappedSchema;
import java.util.stream.Stream;
import org.jooq.Binding;
@ -151,7 +153,7 @@ public class UDTImpl<R extends UDTRecord<R>> extends AbstractNamed implements UD
@Override
public final void accept(Context<?> ctx) {
Schema mappedSchema = Tools.getMappedSchema(ctx.configuration(), getSchema());
Schema mappedSchema = getMappedSchema(ctx, getSchema());
if (mappedSchema != null && !"".equals(mappedSchema.getName()))
ctx.visit(mappedSchema).sql('.');