[jOOQ/jOOQ#13534] Avoid rendering PostgreSQL native cast operator :: which cannot be used in Hibernate native queries

This commit is contained in:
Lukas Eder 2022-05-10 12:27:57 +02:00
parent 91a4e86125
commit ee187a4c04
11 changed files with 224 additions and 137 deletions

View File

@ -45,6 +45,7 @@ import java.sql.SQLFeatureNotSupportedException;
import org.jooq.BindingGetSQLInputContext;
import org.jooq.BindingSQLContext;
import org.jooq.BindingSetSQLOutputContext;
import org.jooq.Context;
import org.jooq.impl.AbstractBinding;
import org.jooq.impl.DSL;
@ -66,34 +67,48 @@ public abstract class AbstractPostgresBinding<T, U> extends AbstractBinding<T, U
return null;
}
@Override
protected void sqlInline(BindingSQLContext<U> ctx) throws SQLException {
if (ctx.value() instanceof Object[]) {
ctx.render().visit(keyword("ARRAY")).sql('[');
/**
* A checked exception throwing {@link Consumer}.
*/
@FunctionalInterface
private interface ThrowingRunnable {
void run() throws SQLException;
}
String separator = "";
for (Object value : ((Object[]) ctx.value())) {
ctx.render().sql(separator).visit(value == null ? keyword("NULL") : DSL.inline("" + value));
separator = ", ";
}
private void castIfNeeded(Context<?> ctx, ThrowingRunnable content) throws SQLException {
String castType = castType();
ctx.render().sql(']');
if (castType != null) {
ctx.visit(keyword("cast")).sql('(');
content.run();
ctx.sql(' ').visit(keyword("as")).sql(' ').sql(castType).sql(')');
}
else
super.sqlInline(ctx);
content.run();
}
String castType = castType();
if (castType != null)
ctx.render().sql("::").sql(castType);
@Override
protected void sqlInline(BindingSQLContext<U> ctx) throws SQLException {
castIfNeeded(ctx.render(), () -> {
if (ctx.value() instanceof Object[]) {
ctx.render().visit(keyword("ARRAY")).sql('[');
String separator = "";
for (Object value : ((Object[]) ctx.value())) {
ctx.render().sql(separator).visit(value == null ? keyword("NULL") : DSL.inline("" + value));
separator = ", ";
}
ctx.render().sql(']');
}
else
super.sqlInline(ctx);
});
}
@Override
protected void sqlBind(BindingSQLContext<U> ctx) throws SQLException {
super.sqlBind(ctx);
String castType = castType();
if (castType != null)
ctx.render().sql("::").sql(castType);
castIfNeeded(ctx.render(), () -> super.sqlBind(ctx));
}
// -------------------------------------------------------------------------

View File

@ -40,21 +40,19 @@ package org.jooq.impl;
import static java.util.Arrays.asList;
// ...
// ...
// ...
import static org.jooq.SQLDialect.POSTGRES;
import static org.jooq.SQLDialect.YUGABYTEDB;
import static org.jooq.impl.Cast.renderCastIf;
import static org.jooq.impl.Keywords.K_ARRAY;
import static org.jooq.impl.Keywords.K_INT;
import static org.jooq.impl.Names.N_ARRAY;
import java.util.Collection;
import java.util.Set;
import java.util.function.Predicate;
import org.jooq.Context;
import org.jooq.DataType;
import org.jooq.Field;
import org.jooq.Function1;
import org.jooq.QueryPart;
import org.jooq.Record;
// ...
@ -95,15 +93,23 @@ final class Array<T> extends AbstractField<T[]> implements QOM.Array<T> {
default:
boolean squareBrackets = true;
renderCastIf(ctx,
c -> {
switch (ctx.family()) {
ctx.visit(K_ARRAY)
.sql(squareBrackets ? '[' : '(')
.visit(fields)
.sql(squareBrackets ? ']' : ')');
if (fields.fields.length == 0 && REQUIRES_CAST.contains(ctx.dialect()))
ctx.sql("::").visit(K_INT).sql("[]");
default:
ctx.visit(K_ARRAY).sql('[').visit(fields).sql(']');
break;
}
},
c -> c.visit(K_INT).sql("[]"),
() -> fields.fields.length == 0 && REQUIRES_CAST.contains(ctx.dialect())
);
break;
}

View File

@ -58,13 +58,11 @@ import static org.jooq.impl.SQLDataType.VARCHAR;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.function.BooleanSupplier;
import org.jooq.Context;
import org.jooq.DataType;
import org.jooq.Field;
import org.jooq.Function1;
import org.jooq.Keyword;
import org.jooq.LanguageContext;
// ...
@ -330,31 +328,57 @@ final class Cast<T> extends AbstractField<T> implements QOM.Cast<T> {
@Override
public void accept(Context<?> ctx) {
// Avoid casting bind values inside an explicit cast...
CastMode castMode = ctx.castMode();
// Default rendering, if no special case has applied yet
ctx.visit(K_CAST).sql('(')
.castMode(CastMode.NEVER)
.visit(expression)
.castMode(castMode)
.sql(' ').visit(K_AS).sql(' ');
if (typeAsKeyword != null)
ctx.visit(typeAsKeyword);
renderCast(ctx,
c -> c.visit(expression),
c -> {
if (typeAsKeyword != null)
c.visit(typeAsKeyword);
else
ctx.sql(type.getCastTypeName(ctx.configuration()));
ctx.sql(')');
else
c.sql(type.getCastTypeName(c.configuration()));
}
);
}
}
static <E extends Throwable> void renderCast(
Context<?> ctx,
ThrowingConsumer<? super Context<?>, E> expression,
ThrowingConsumer<? super Context<?>, E> type
) throws E {
// Avoid casting bind values inside an explicit cast...
CastMode castMode = ctx.castMode();
// Default rendering, if no special case has applied yet
ctx.visit(K_CAST).sql('(')
.castMode(CastMode.NEVER);
expression.accept(ctx);
ctx.castMode(castMode)
.sql(' ').visit(K_AS).sql(' ');
type.accept(ctx);
ctx.sql(')');
}
static <E extends Throwable> void renderCastIf(
Context<?> ctx,
ThrowingConsumer<? super Context<?>, E> expression,
ThrowingConsumer<? super Context<?>, E> type,
BooleanSupplier test
) throws E {
if (test.getAsBoolean())
renderCast(ctx, expression, type);
else
expression.accept(ctx);
}
// -------------------------------------------------------------------------
// XXX: Query Object Model
// -------------------------------------------------------------------------

View File

@ -219,7 +219,7 @@ implements
// [#3824] Ensure that the output for DATE arithmetic will also be of type DATE, not TIMESTAMP
else
ctx.sql('(').visit(date).sql(" + ").visit(interval).sql(" * ").visit(K_INTERVAL).sql(' ').visit(inline(string)).sql(")::date");
ctx.sql("cast((").visit(date).sql(" + ").visit(interval).sql(" * ").visit(K_INTERVAL).sql(' ').visit(inline(string)).sql(") as date)");
else
ctx.sql('(').visit(date).sql(" + ").visit(interval).sql(" * ").visit(K_INTERVAL).sql(' ').visit(inline(string)).sql(")");

View File

@ -94,6 +94,8 @@ import static org.jooq.impl.DSL.using;
import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.REQUIRES_LITERAL_CAST;
import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.infinity;
import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.nan;
import static org.jooq.impl.DefaultBinding.DefaultEnumTypeBinding.pgEnumValue;
import static org.jooq.impl.DefaultBinding.DefaultEnumTypeBinding.pgRenderEnumCast;
import static org.jooq.impl.DefaultBinding.DefaultJSONBBinding.EMULATE_AS_BLOB;
import static org.jooq.impl.DefaultBinding.DefaultResultBinding.readMultisetJSON;
import static org.jooq.impl.DefaultBinding.DefaultResultBinding.readMultisetXML;
@ -123,6 +125,7 @@ import static org.jooq.impl.Keywords.K_TIME_WITH_TIME_ZONE;
import static org.jooq.impl.Keywords.K_TRUE;
import static org.jooq.impl.Keywords.K_YEAR_TO_DAY;
import static org.jooq.impl.Keywords.K_YEAR_TO_FRACTION;
import static org.jooq.impl.Names.N_BYTEA;
import static org.jooq.impl.Names.N_ST_GEOMFROMTEXT;
import static org.jooq.impl.Names.N_ST_GEOMFROMWKB;
import static org.jooq.impl.R2DBC.isR2dbc;
@ -1223,11 +1226,6 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
}
ctx.render().sql(squareBrackets ? ']' : ')');
// [#3214] Some PostgreSQL array type literals need explicit casting
// TODO: This seems mutually exclusive with the previous branch. Still needed?
if ((REQUIRES_ARRAY_CAST.contains(ctx.dialect())) && dataType.getArrayComponentDataType().isEnum())
DefaultEnumTypeBinding.pgRenderEnumCast(ctx.render(), dataType.getType());
}
}
@ -1243,24 +1241,22 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
@Override
final void sqlBind0(BindingSQLContext<U> ctx, Object[] value) throws SQLException {
super.sqlBind0(ctx, value);
// In Postgres, some additional casting must be done in some cases...
switch (ctx.family()) {
case POSTGRES:
case YUGABYTEDB:
Cast.renderCastIf(ctx.render(),
c -> super.sqlBind0(ctx, value),
c -> {
// Postgres needs explicit casting for enum (array) types
if (EnumType.class.isAssignableFrom(dataType.getType().getComponentType()))
DefaultEnumTypeBinding.pgRenderEnumCast(ctx.render(), dataType.getType());
pgRenderEnumCast(ctx.render(), dataType.getType(), pgEnumValue(dataType.getType()));
// ... and also for other array types
else
ctx.render().sql("::")
.sql(dataType.getCastTypeName(ctx.render().configuration()));
}
ctx.render().sql(dataType.getCastTypeName(ctx.render().configuration()));
},
// In Postgres, some additional casting must be done in some cases...
() -> REQUIRES_ARRAY_CAST.contains(ctx.family())
);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@ -2063,11 +2059,10 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
case POSTGRES:
case YUGABYTEDB:
ctx.render()
.sql("E'")
.sql(PostgresUtils.toPGString(value))
.sql("'::bytea");
Cast.renderCast(ctx.render(),
c -> c.sql("E'").sql(PostgresUtils.toPGString(value)).sql("'"),
c -> c.visit(N_BYTEA)
);
break;
default:
@ -2673,19 +2668,28 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
@Override
final void sqlInline0(BindingSQLContext<U> ctx, EnumType value) throws SQLException {
binding(VARCHAR).sql(new DefaultBindingSQLContext<>(ctx.configuration(), ctx.data(), ctx.render(), value.getLiteral()));
EnumType enumValue = pgEnumValue(dataType.getType());
if (REQUIRE_ENUM_CAST.contains(ctx.dialect()))
pgRenderEnumCast(ctx.render(), dataType.getType());
Cast.renderCastIf(ctx.render(),
c -> binding(VARCHAR).sql(new DefaultBindingSQLContext<>(ctx.configuration(), ctx.data(), ctx.render(), value.getLiteral())),
c -> pgRenderEnumCast(c, dataType.getType(), enumValue),
// Postgres needs explicit casting for enum (array) types
() -> REQUIRE_ENUM_CAST.contains(ctx.dialect()) && enumValue.getSchema() != null
);
}
@Override
final void sqlBind0(BindingSQLContext<U> ctx, EnumType value) throws SQLException {
super.sqlBind0(ctx, value);
EnumType enumValue = pgEnumValue(dataType.getType());
// Postgres needs explicit casting for enum (array) types
if (REQUIRE_ENUM_CAST.contains(ctx.dialect()))
pgRenderEnumCast(ctx.render(), dataType.getType());
Cast.renderCastIf(ctx.render(),
c -> super.sqlBind0(ctx, value),
c -> pgRenderEnumCast(c, dataType.getType(), enumValue),
// Postgres needs explicit casting for enum (array) types
() -> REQUIRE_ENUM_CAST.contains(ctx.dialect()) && enumValue.getSchema() != null
);
}
@Override
@ -2718,7 +2722,7 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
return Types.VARCHAR;
}
static final void pgRenderEnumCast(RenderContext render, Class<?> type) {
static final EnumType pgEnumValue(Class<?> type) {
@SuppressWarnings("unchecked")
Class<? extends EnumType> enumType = (Class<? extends EnumType>) (
@ -2731,21 +2735,23 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
if (enums == null || enums.length == 0)
throw new IllegalArgumentException("Not a valid EnumType : " + type);
Schema schema = enums[0].getSchema();
if (schema != null) {
render.sql("::");
return enums[0];
}
schema = using(render.configuration()).map(schema);
if (schema != null && TRUE.equals(render.configuration().settings().isRenderSchema())) {
render.visit(schema);
render.sql('.');
static final void pgRenderEnumCast(Context<?> ctx, Class<?> type, EnumType value) {
Schema schema = value.getSchema();
if (schema != null) {
schema = using(ctx.configuration()).map(schema);
if (schema != null && TRUE.equals(ctx.configuration().settings().isRenderSchema())) {
ctx.visit(schema);
ctx.sql('.');
}
render.visit(name(enums[0].getName()));
}
ctx.visit(name(value.getName()));
if (type.isArray())
render.sql("[]");
if (type.isArray())
ctx.sql("[]");
}
}
static final <E extends EnumType> E getEnumType(Class<? extends E> type, String literal) {
@ -3678,7 +3684,7 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
}
static final class DefaultRecordBinding<U> extends InternalBinding<Record, U> {
private static final Set<SQLDialect> REQUIRE_RECORD_CAST = SQLDialect.supportedBy(POSTGRES, YUGABYTEDB);
static final Set<SQLDialect> REQUIRE_RECORD_CAST = SQLDialect.supportedBy(POSTGRES, YUGABYTEDB);
DefaultRecordBinding(DataType<Record> dataType, Converter<Record, U> converter) {
super(dataType, converter);
@ -3686,20 +3692,25 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
@Override
void sqlBind0(BindingSQLContext<U> ctx, Record value) throws SQLException {
super.sqlBind0(ctx, value);
if (REQUIRE_RECORD_CAST.contains(ctx.dialect()) && value != null)
pgRenderRecordCast(ctx.render(), value);
Cast.renderCastIf(ctx.render(),
c -> super.sqlBind0(ctx, value),
c -> pgRenderRecordCast(ctx.render(), value),
() -> REQUIRE_RECORD_CAST.contains(ctx.dialect()) && value != null
);
}
@Override
final void sqlInline0(BindingSQLContext<U> ctx, Record value) throws SQLException {
if (REQUIRE_RECORD_CAST.contains(ctx.dialect())) {
ctx.render().visit(inline(PostgresUtils.toPGString(value)));
pgRenderRecordCast(ctx.render(), value);
}
else
ctx.render().sql("[UDT]");
Cast.renderCastIf(ctx.render(),
c -> {
if (REQUIRE_RECORD_CAST.contains(ctx.dialect()))
ctx.render().visit(inline(PostgresUtils.toPGString(value)));
else
ctx.render().sql("[UDT]");
},
c -> pgRenderRecordCast(ctx.render(), value),
() -> REQUIRE_RECORD_CAST.contains(ctx.dialect())
);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@ -3817,11 +3828,11 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
// interfaces. Instead, a string representation of a UDT has to be parsed
// -------------------------------------------------------------------------
static final void pgRenderRecordCast(RenderContext render, Record value) {
static final void pgRenderRecordCast(Context<?> ctx, Record value) {
if (value instanceof UDTRecord)
render.sql("::").visit(((UDTRecord<?>) value).getUDT().getQualifiedName());
ctx.visit(((UDTRecord<?>) value).getUDT().getQualifiedName());
else if (value instanceof TableRecord)
render.sql("::").visit(((TableRecord<?>) value).getTable().getQualifiedName());
ctx.visit(((TableRecord<?>) value).getTable().getQualifiedName());
}
@SuppressWarnings("unchecked")

View File

@ -165,6 +165,33 @@ final class Extract extends AbstractField<Integer> implements QOM.Extract {

View File

@ -138,7 +138,10 @@ final class JSONExists extends AbstractCondition implements JSONExistsOnStep, UN
case POSTGRES:
case YUGABYTEDB:
ctx.visit(N_JSONB_PATH_EXISTS).sql('(').visit(castIfNeeded(json, JSONB)).sql(", ").visit(path).sql("::jsonpath)");
ctx.visit(N_JSONB_PATH_EXISTS).sql('(')
.visit(castIfNeeded(json, JSONB)).sql(", ");
Cast.renderCast(ctx, c -> c.visit(path), c -> c.visit(Names.N_JSONPATH));
ctx.sql(')');
break;
default:

View File

@ -219,13 +219,13 @@ implements
ctx,
select(map(columns, col -> col.forOrdinality
? DSL.field("o").as(col.field)
: DSL.field("(jsonb_path_query_first(j, {0}::jsonpath)->>0)::{1}",
: DSL.field("cast((jsonb_path_query_first(j, cast({0} as jsonpath))->>0) as {1})",
col.path != null ? val(col.path) : inline("$." + col.field.getName()),
keyword(col.type.getCastTypeName(ctx.configuration()))
).as(col.field)))
.from(hasOrdinality
? "jsonb_path_query({0}, {1}::jsonpath) {with} {ordinality} {as} t(j, o)"
: "jsonb_path_query({0}, {1}::jsonpath) {as} t(j)",
? "jsonb_path_query({0}, cast({1} as jsonpath)) {with} {ordinality} {as} t(j, o)"
: "jsonb_path_query({0}, cast({1} as jsonpath)) {as} t(j)",
json.getType() == JSONB.class ? json : json.cast(JSONB),
path
),

View File

@ -184,7 +184,7 @@ implements
case POSTGRES:
case YUGABYTEDB:
ctx.visit(function(N_JSONB_PATH_QUERY_FIRST, json.getDataType(), castIfNeeded(json, JSONB), DSL.field("{0}::jsonpath", path)));
ctx.visit(function(N_JSONB_PATH_QUERY_FIRST, json.getDataType(), castIfNeeded(json, JSONB), DSL.field("cast({0} as jsonpath)", path)));
break;
default: {

View File

@ -97,6 +97,7 @@ final class Names {
static final Name N_BIT_X_NOR = systemName("bit_xnor");
static final Name N_BOOLAND_AGG = systemName("booland_agg");
static final Name N_BOOLOR_AGG = systemName("boolor_agg");
static final Name N_BYTEA = systemName("bytea");
static final Name N_BYTE_LENGTH = systemName("byte_length");
static final Name N_CAST = systemName("cast");
static final Name N_CEILING = systemName("ceiling");
@ -173,6 +174,7 @@ final class Names {
static final Name N_JSONB_OBJECT_AGG = systemName("jsonb_object_agg");
static final Name N_JSONB_PATH_EXISTS = systemName("jsonb_path_exists");
static final Name N_JSONB_PATH_QUERY_FIRST = systemName("jsonb_path_query_first");
static final Name N_JSONPATH = systemName("jsonpath");
static final Name N_JSON_AGG = systemName("json_agg");
static final Name N_JSON_ARRAYAGG = systemName("json_arrayagg");
static final Name N_JSON_BUILD_ARRAY = systemName("json_build_array");

View File

@ -40,6 +40,7 @@ package org.jooq.impl;
import static org.jooq.conf.ParamType.INLINED;
import static org.jooq.impl.DSL.val;
import static org.jooq.impl.DefaultBinding.DefaultRecordBinding.REQUIRE_RECORD_CAST;
import static org.jooq.impl.Keywords.K_ROW;
import static org.jooq.impl.Tools.getMappedUDTName;
@ -129,40 +130,38 @@ final class QualifiedRecordConstant<R extends QualifiedRecord<R>> extends Abstra
}
private final void toSQLInline(RenderContext ctx) {
switch (ctx.family()) {
Cast.renderCastIf(ctx,
c -> {
switch (c.family()) {
case POSTGRES:
case YUGABYTEDB:
ctx.visit(K_ROW);
break;
case POSTGRES:
case YUGABYTEDB:
c.visit(K_ROW);
break;
default: {
ctx.visit(value.getQualifier());
break;
}
}
default: {
c.visit(value.getQualifier());
break;
}
}
ctx.sql('(');
c.sql('(');
String separator = "";
for (Field<?> field : value.fields()) {
ctx.sql(separator);
ctx.visit(val(value.get(field), field));
separator = ", ";
}
String separator = "";
for (Field<?> field : value.fields()) {
c.sql(separator);
c.visit(val(value.get(field), field));
separator = ", ";
}
ctx.sql(')');
c.sql(')');
},
// [#13174] Need to cast inline UDT ROW expressions to the UDT type
switch (ctx.family()) {
case POSTGRES:
case YUGABYTEDB:
ctx.sql("::").visit(value.getQualifier());
break;
}
// [#13174] Need to cast inline UDT ROW expressions to the UDT type
c -> c.visit(value.getQualifier()),
() -> REQUIRE_RECORD_CAST.contains(ctx.dialect())
);
}
@Deprecated