[jOOQ/jOOQ#10880] Add DECFLOAT support

This includes:

- Added org.jooq.Decfloat draft implementation
- Added DefaultBinding
- Added SQLDataType and dialect specific ones
- Added binding test
- org.jooq.Decfloat implements org.jooq.Data
- Normalisation of Decfloat for equals(), hashCode(), toString()
- [jOOQ/jOOQ#17060] Add a org.jooq.Data interface, which all the data()
bearing types can implement
This commit is contained in:
Lukas Eder 2024-08-14 10:13:21 +02:00
parent b60097822c
commit aa3fbda55d
10 changed files with 370 additions and 15 deletions

View File

@ -0,0 +1,79 @@
/*
* 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
*
* https://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
* Apache-2.0 license and offer limited warranties, support, maintenance, and
* commercial database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq;
import java.io.Serializable;
import java.sql.SQLXML;
import org.jetbrains.annotations.NotNull;
/**
* A type holding {@link #data()}, which is a {@link String} based
* representation of a SQL data type with no reasonable representation in the
* JDK or in JDBC.
* <p>
* JDBC maps most types to JDK types, but some vendor specific types lack an
* equivalent in the JDK, or the relevant type is unsatisfactory (such as
* {@link SQLXML}, which is a resource). To work around such limitations, jOOQ
* establishes the set of {@link Data} types, with these properties:
* <p>
* <ul>
* <li>They contain a {@link NotNull} representation of their {@link #data()}.
* In other words, a <code>CAST(NULL AS )</code> value is represented by a
* <code>null</code> reference of type {@link Data}, not as
* <code>data() == null</code>. This is consistent with jOOQ's general way of
* returning <code>NULL</code> from {@link Result} and {@link Record}
* methods.</li>
* <li>They're {@link Serializable}</li>
* <li>They may or may not use an internal, normalised representation of their
* {@link #data()} for {@link #equals(Object)}, {@link #hashCode()}, and
* {@link #toString()} (e.g. {@link JSONB})</li>
* </ul>
*
* @author Lukas Eder
*/
public interface Data extends Serializable {
/**
* Get the vendor specific {@link String} representation of this
* {@link Data} object.
*/
@NotNull
String data();
}

View File

@ -0,0 +1,201 @@
/*
* 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
*
* https://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
* Apache-2.0 license and offer limited warranties, support, maintenance, and
* commercial database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq;
import java.math.BigDecimal;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A wrapper type for SQL:2016 standard <code>DECFLOAT</code> data types.
* <p>
* The wrapper represents DECFLOAT {@link #data()} in serialised string form. A
* <code>CAST(NULL AS DECFLOAT)</code> value is represented by a
* <code>null</code> reference of type {@link Decfloat}, not as
* <code>data() == null</code>. This is consistent with jOOQ's general way of
* returning <code>NULL</code> from {@link Result} and {@link Record} methods.
*/
public class Decfloat implements Data {
private final String data;
private transient BigDecimal coefficient;
private transient int exponent;
private transient Special special;
private Decfloat(String data) {
this.data = String.valueOf(data);
}
@Override
@NotNull
public final String data() {
return data;
}
/**
* Create a new {@link Decfloat} instance from string data input.
*/
@NotNull
public static final Decfloat valueOf(String data) {
return new Decfloat(data);
}
/**
* Create a new {@link Decfloat} instance from string data input.
* <p>
* This is the same as {@link #valueOf(String)}, but it can be static
* imported.
*/
@NotNull
public static final Decfloat decfloat(String data) {
return new Decfloat(data);
}
/**
* Create a new {@link Decfloat} instance from string data input, or
* <code>null</code> if the input is <code>null</code>.
*/
@Nullable
public static final Decfloat decfloatOrNull(String data) {
return data == null ? null : decfloat(data);
}
@Override
public int hashCode() {
parse();
if (special != null) {
return special.hashCode();
}
else if (coefficient != null) {
final int prime = 31;
int result = 1;
result = prime * result + ((coefficient == null) ? 0 : coefficient.hashCode());
result = prime * result + exponent;
return result;
}
else
return data.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj instanceof Decfloat x) {
parse();
x.parse();
if (special != null && x.special != null)
return special == x.special;
else if (coefficient != null && x.coefficient != null)
return Objects.equals(coefficient, x.coefficient) && exponent == x.exponent;
else
return Objects.equals(data, x.data);
}
return false;
}
@Override
public String toString() {
parse();
if (special != null) {
switch (special) {
case NAN:
return "NaN";
case POSITIVE_INFINITY:
return "Infinity";
case NEGATIVE_INFINITY:
return "-Infinity";
}
}
else if (coefficient != null)
return coefficient + "E" + exponent;
return String.valueOf(data);
}
private void parse() {
if (coefficient != null || special != null)
return;
switch (data) {
case "+NaN":
case "NaN":
special = Special.NAN;
break;
case "+Infinity":
case "Infinity":
case "+Inf":
case "Inf":
special = Special.POSITIVE_INFINITY;
break;
case "-Infinity":
case "-Inf":
special = Special.NEGATIVE_INFINITY;
break;
default: {
int i = data.indexOf("E");
if (i == -1)
i = data.indexOf("e");
try {
coefficient = new BigDecimal(data.substring(0, i)).stripTrailingZeros();
exponent = Integer.parseInt(data.substring(i + 1));
}
// [#10880] If we cannot represent the value internally, then we'll just work with data
catch (NumberFormatException ignore) {}
break;
}
}
}
private enum Special {
NAN, POSITIVE_INFINITY, NEGATIVE_INFINITY
}
}

View File

@ -37,8 +37,6 @@
*/
package org.jooq;
import java.io.Serializable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -68,7 +66,7 @@ import org.jetbrains.annotations.Nullable;
* <li>{@link #toString()}</li>
* </ul>
*/
public final class JSON implements Serializable {
public final class JSON implements Data {
private final String data;
@ -76,6 +74,7 @@ public final class JSON implements Serializable {
this.data = String.valueOf(data);
}
@Override
@NotNull
public final String data() {
return data;

View File

@ -37,7 +37,6 @@
*/
package org.jooq;
import java.io.Serializable;
import java.util.Objects;
import org.jooq.tools.json.JSONParser;
@ -75,7 +74,7 @@ import org.jetbrains.annotations.Nullable;
* <p>
* The {@link #data()} content, however, is not normalised.
*/
public final class JSONB implements Serializable {
public final class JSONB implements Data {
private final String data;
private transient Object parsed;
@ -84,6 +83,7 @@ public final class JSONB implements Serializable {
this.data = String.valueOf(data);
}
@Override
@NotNull
public final String data() {
return data;

View File

@ -37,10 +37,6 @@
*/
package org.jooq;
import java.io.Serializable;
import org.jetbrains.annotations.NotNull;
/**
* A wrapper type for spatial data obtained from the database.
* <p>
@ -54,9 +50,7 @@ import org.jetbrains.annotations.NotNull;
* <p>
* This data type is supported only by the commercial editions of jOOQ.
*/
public interface Spatial extends Serializable {
public interface Spatial extends Data {
@NotNull
String data();
}

View File

@ -37,8 +37,6 @@
*/
package org.jooq;
import java.io.Serializable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -51,7 +49,7 @@ import org.jetbrains.annotations.Nullable;
* consistent with jOOQ's general way of returning <code>NULL</code> from
* {@link Result} and {@link Record} methods.
*/
public final class XML implements Serializable {
public final class XML implements Data {
private final String data;
@ -59,6 +57,7 @@ public final class XML implements Serializable {
this.data = String.valueOf(data);
}
@Override
@NotNull
public final String data() {
return data;

View File

@ -50,6 +50,8 @@ import static java.util.Arrays.asList;
import static java.util.function.Function.identity;
import static java.util.regex.Matcher.quoteReplacement;
import static org.jooq.ContextConverter.scoped;
import static org.jooq.Decfloat.decfloat;
import static org.jooq.Decfloat.decfloatOrNull;
import static org.jooq.Geography.geography;
import static org.jooq.Geometry.geometry;
// ...
@ -257,6 +259,7 @@ import org.jooq.Converter;
import org.jooq.ConverterContext;
import org.jooq.Converters;
import org.jooq.DataType;
import org.jooq.Decfloat;
import org.jooq.EnumType;
import org.jooq.ExecuteScope;
import org.jooq.Field;
@ -376,6 +379,8 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
return new DefaultDateBinding(dataType, converter);
else if (type == DayToSecond.class)
return new DefaultDayToSecondBinding(dataType, converter);
else if (type == Decfloat.class)
return new DefaultDecfloatBinding(dataType, converter);
else if (type == Double.class || type == double.class)
return new DefaultDoubleBinding(dataType, converter);
else if (type == Float.class || type == float.class)
@ -875,6 +880,16 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
}
}
if (dataType.getType() == Decfloat.class) {
switch (ctx.family()) {
case FIREBIRD:
case H2:
return true;
}
}
@ -1969,6 +1984,43 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
}
}
static final class DefaultDecfloatBinding<U> extends InternalBinding<Decfloat, U> {
DefaultDecfloatBinding(DataType<Decfloat> dataType, Converter<Decfloat, U> converter) {
super(dataType, converter);
}
@Override
final void set0(BindingSetStatementContext<U> ctx, Decfloat value) throws SQLException {
ctx.statement().setString(ctx.index(), value.data());
}
@Override
final void set0(BindingSetSQLOutputContext<U> ctx, Decfloat value) throws SQLException {
ctx.output().writeString(value.data());
}
@Override
final Decfloat get0(BindingGetResultSetContext<U> ctx) throws SQLException {
return decfloatOrNull(ctx.resultSet().getString(ctx.index()));
}
@Override
final Decfloat get0(BindingGetStatementContext<U> ctx) throws SQLException {
return decfloatOrNull(ctx.statement().getString(ctx.index()));
}
@Override
final Decfloat get0(BindingGetSQLInputContext<U> ctx) throws SQLException {
return decfloatOrNull(ctx.input().readString());
}
@Override
final int sqltype(Statement statement, Configuration configuration) {
return Types.VARCHAR;
}
}
static final class DefaultBlobBinding<U> extends InternalBinding<Blob, U> {
DefaultBlobBinding(DataType<Blob> dataType, Converter<Blob, U> converter) {

View File

@ -91,6 +91,7 @@ import java.util.UUID;
import org.jooq.Configuration;
import org.jooq.DataType;
import org.jooq.Decfloat;
import org.jooq.Geography;
import org.jooq.Geometry;
import org.jooq.JSON;
@ -777,6 +778,32 @@ public final class SQLDataType {
*/
public static final DataType<Geometry> GEOMETRY = new BuiltInDataType<>(Geometry.class, "geometry");
/**
* The {@link Decfloat} type.
* <p>
* This is not a JDBC standard. This type handles DECFLOAT types where they
* are supported.
* <p>
* If you want to opt out of code generation support for this type, you can
* specify <code>/configuration/generator/generate/decfloatTypes</code> to
* <code>false</code>.
*/
public static final DataType<Decfloat> DECFLOAT = new BuiltInDataType<>(Decfloat.class, "decfloat(p)");
/**
* The {@link Decfloat} type.
* <p>
* This is not a JDBC standard. This type handles DECFLOAT types where they
* are supported.
* <p>
* If you want to opt out of code generation support for this type, you can
* specify <code>/configuration/generator/generate/decfloatTypes</code> to
* <code>false</code>.
*/
public static final DataType<Decfloat> DECFLOAT(int precision) {
return DECFLOAT.precision(precision, 0);
}
// -------------------------------------------------------------------------
// Static initialisation of dialect-specific data types
// -------------------------------------------------------------------------

View File

@ -47,6 +47,7 @@ import java.time.Year;
import java.util.UUID;
import org.jooq.DataType;
import org.jooq.Decfloat;
import org.jooq.JSON;
import org.jooq.JSONB;
import org.jooq.SQLDialect;
@ -103,6 +104,7 @@ public class FirebirdDataType {
public static final DataType<Time> TIME = new BuiltInDataType<>(FAMILY, SQLDataType.TIME, "time");
public static final DataType<Timestamp> TIMESTAMP = new BuiltInDataType<>(FAMILY, SQLDataType.TIMESTAMP, "timestamp");
public static final DataType<byte[]> BLOB = new BuiltInDataType<>(FAMILY, SQLDataType.BLOB, "blob");
public static final DataType<Decfloat> DECFLOAT = new BuiltInDataType<>(FAMILY, SQLDataType.DECFLOAT, "decfloat");
// TODO Below are HSQLDB data types. Fix this

View File

@ -48,6 +48,7 @@ import java.time.Year;
import java.util.UUID;
import org.jooq.DataType;
import org.jooq.Decfloat;
import org.jooq.JSON;
import org.jooq.JSONB;
import org.jooq.Record;
@ -149,6 +150,7 @@ public class H2DataType {
public static final DataType<JSONB> JSONB = new BuiltInDataType<>(FAMILY, SQLDataType.JSONB, "json");
public static final DataType<YearToMonth> INTERVALYEARTOMONTH = new BuiltInDataType<>(FAMILY, SQLDataType.INTERVALYEARTOMONTH, "interval year to month");
public static final DataType<DayToSecond> INTERVALDAYTOSECOND = new BuiltInDataType<>(FAMILY, SQLDataType.INTERVALDAYTOSECOND, "interval day to second", "interval day(9) to second");
public static final DataType<Decfloat> DECFLOAT = new BuiltInDataType<>(FAMILY, SQLDataType.DECFLOAT, "decfloat");
// -------------------------------------------------------------------------
// Compatibility types for supported SQLDialect.H2, SQLDataTypes