Skip to content

Commit

Permalink
Fixes #1365: add integral number codecs, tests (#1367)
Browse files Browse the repository at this point in the history
  • Loading branch information
tatu-at-datastax authored Sep 3, 2024
1 parent 075c373 commit 869d6af
Show file tree
Hide file tree
Showing 18 changed files with 648 additions and 236 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ public <StmtT extends OngoingWhereClause<StmtT>> StmtT apply(
} catch (UnknownColumnException e) {
throw ErrorCode.TABLE_COLUMN_UNKNOWN.toApiException(e.getMessage());
} catch (MissingJSONCodecException e) {
// TODO AARON - Handle error
throw new RuntimeException(e);
throw ErrorCode.TABLE_COLUMN_TYPE_UNSUPPORTED.toApiException(e.getMessage());
} catch (ToCQLCodecException e) {
// TODO AARON - Handle error
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package io.stargate.sgv2.jsonapi.service.operation.filters.table;

import java.math.BigDecimal;

/** Filter to use any JSON number against a table column. */
public class NumberTableFilter extends NativeTypeTableFilter<BigDecimal> {
public class NumberTableFilter extends NativeTypeTableFilter<Number> {

public NumberTableFilter(String path, Operator operator, BigDecimal value) {
public NumberTableFilter(String path, Operator operator, Number value) {
super(path, operator, value);
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
package io.stargate.sgv2.jsonapi.service.operation.filters.table.codecs;

import com.datastax.oss.driver.api.core.type.DataType;
import com.datastax.oss.driver.api.core.type.DataTypes;
import com.datastax.oss.driver.api.core.type.reflect.GenericType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.stargate.sgv2.jsonapi.service.shredding.tables.RowShredder;
import java.util.function.Function;

/**
* Handles the conversation between the in memory Java representation of a value from a JSON
* Handles the conversation between the in-memory Java representation of a value from a JSON
* document and the Java type that the driver expects for the CQL type of the column.
*
* <p>This is codec sitting above the codec the Java C* driver uses.
*
* <p>The path is:
* <p>The path for converting values contained in incoming JSON Document into values sent to C* is:
*
* <ul>
* <li>JSON Document
* <li>Jackson parses and turns into Java Object (e.g. BigInteger)
* <li>JSON Document parsed by Jackson into a {@link JsonNode}
* <li>{@link RowShredder} converts the Jackson {@JsonNode}s values into Java Objects (from {@code
* TextNode} into {@code String}, {@code BooleanNode} into {@code Boolean}, etc.)
* <li>JSONCodec (this class) turns Java Object into the Java type the C* driver expects (e.g.
* Short
* <li>C* driver codec turns Java type into C* type
Expand All @@ -27,6 +30,16 @@
* for primitive type. Needs a revisit when we doing complex // types where only few fields will
* need to be returned. Will we be creating custom Codec based // on user requests?
*
* <p>Note on Jackson conversion of JSON Numbers: Jackson is used to first read JSON content as
* {@link JsonNode}s, and then values are converted to "natural" Java types: conversion is done in
* {@link RowShredder#shredValue} and results in one of the following types:
*
* <ul>
* <li>{@link java.math.BigDecimal} for numbers that are not integers in JSON
* <li>{@link java.math.BigInteger} for integers that are too big for {@link Long}
* <li>{@link java.lang.Long} for integers that fit in 64-bit signed {@code Long}
* </ul>
*
* @param javaType {@link GenericType} of the Java object that needs to be transformed into the type
* CQL expects.
* @param targetCQLType {@link DataType} of the CQL column type the Java object needs to be
Expand Down Expand Up @@ -105,7 +118,7 @@ public static <JavaT, CqlT> JSONCodec<JavaT, CqlT> unchecked(JSONCodec<?, ?> cod
}

/**
* Function interface that is used by the codec to convert the Java value to the value CQL
* Functional interface that is used by the codec to convert the Java value to the value CQL
* expects.
*
* <p>The interface is used so the conversation function can throw the checked {@link
Expand Down Expand Up @@ -167,16 +180,55 @@ static <JavaT extends Number, CqlT> ToCQL<JavaT, CqlT> safeNumber(
}
};
}

static Integer safeLongToInt(Long value) {
long l = value.longValue();
if (l < Integer.MIN_VALUE) {
throwUnderflow(DataTypes.INT, value);
} else if (l > Integer.MAX_VALUE) {
throwOverflow(DataTypes.INT, value);
}
return (int) l;
}

static Short safeLongToSmallint(Long value) {
long l = value.longValue();
if (l < Short.MIN_VALUE) {
throwUnderflow(DataTypes.SMALLINT, value);
} else if (l > Short.MAX_VALUE) {
throwOverflow(DataTypes.SMALLINT, value);
}
return (short) l;
}

static Byte safeLongToTinyint(Long value) {
long l = value.longValue();
if (l < Byte.MIN_VALUE) {
throwUnderflow(DataTypes.TINYINT, value);
} else if (l > Byte.MAX_VALUE) {
throwOverflow(DataTypes.TINYINT, value);
}
return (byte) l;
}

static void throwOverflow(DataType targetCQLType, Number value) {
throw new ArithmeticException(String.format("Overflow, value too big for %s", targetCQLType));
}

static void throwUnderflow(DataType targetCQLType, Number value) {
throw new ArithmeticException(
String.format("Underflow, value too small for %s", targetCQLType));
}
}

/**
* Function interface that is used by the codec to convert value returned by CQL into a {@link
* Functional interface that is used by the codec to convert value returned by CQL into a {@link
* JsonNode} that can be used to construct the response document for a row.
*
* <p>The interface is used so the conversation function can throw the checked {@link
* ToJSONCodecException}, it is also given the CQL data type to make better exceptions.
*
* <p>Use the static constructors on the interface to get instances, see it's use in the {@link
* <p>Use the static constructors on the interface to get instances, see its use in the {@link
* JSONCodecRegistry}
*
* @param <CqlT> The type Java object the CQL driver expects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,25 +127,34 @@ public static <JavaT, CqlT> JSONCodec<JavaT, CqlT> codecToJSON(DataType targetCQ
}

// Boolean
public static final JSONCodec<Boolean, Boolean> BOOLEAN =
private static final JSONCodec<Boolean, Boolean> BOOLEAN =
new JSONCodec<>(
GenericType.BOOLEAN,
DataTypes.BOOLEAN,
JSONCodec.ToCQL.unsafeIdentity(),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::booleanNode));

// Numeric Codecs
public static final JSONCodec<BigDecimal, Long> BIGINT =
private static final JSONCodec<BigDecimal, Long> BIGINT_FROM_BIG_DECIMAL =
new JSONCodec<>(
GenericType.BIG_DECIMAL,
DataTypes.BIGINT,
JSONCodec.ToCQL.safeNumber(BigDecimal::longValueExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

// TODO Tatu For performance reasons we could also consider only converting FP values into
// BigDecimal JsonNode -- but converting CQL integer values into long-valued JsonNode.
// I think our internal handling can deal with Integer and Long valued JsonNodes and this avoids
// some of BigDecimal overhead (avoids conversion overhead, serialization is faster).
private static final JSONCodec<BigInteger, Long> BIGINT_FROM_BIG_INTEGER =
new JSONCodec<>(
GenericType.BIG_INTEGER,
DataTypes.BIGINT,
JSONCodec.ToCQL.safeNumber(BigInteger::longValueExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

private static final JSONCodec<Long, Long> BIGINT_FROM_LONG =
new JSONCodec<>(
GenericType.LONG,
DataTypes.BIGINT,
JSONCodec.ToCQL.unsafeIdentity(),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigDecimal, BigDecimal> DECIMAL =
new JSONCodec<>(
Expand All @@ -168,34 +177,90 @@ public static <JavaT, CqlT> JSONCodec<JavaT, CqlT> codecToJSON(DataType targetCQ
JSONCodec.ToCQL.safeNumber(BigDecimal::floatValue),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigDecimal, Integer> INT =
public static final JSONCodec<BigDecimal, Integer> INT_FROM_BIG_DECIMAL =
new JSONCodec<>(
GenericType.BIG_DECIMAL,
DataTypes.INT,
JSONCodec.ToCQL.safeNumber(BigDecimal::intValueExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigDecimal, Short> SMALLINT =
public static final JSONCodec<BigInteger, Integer> INT_FROM_BIG_INTEGER =
new JSONCodec<>(
GenericType.BIG_INTEGER,
DataTypes.INT,
JSONCodec.ToCQL.safeNumber(BigInteger::intValueExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<Long, Integer> INT_FROM_LONG =
new JSONCodec<>(
GenericType.LONG,
DataTypes.INT,
JSONCodec.ToCQL.safeNumber(JSONCodec.ToCQL::safeLongToInt),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigDecimal, Short> SMALLINT_FROM_BIG_DECIMAL =
new JSONCodec<>(
GenericType.BIG_DECIMAL,
DataTypes.SMALLINT,
JSONCodec.ToCQL.safeNumber(BigDecimal::shortValueExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigDecimal, Byte> TINYINT =
public static final JSONCodec<BigInteger, Short> SMALLINT_FROM_BIG_INTEGER =
new JSONCodec<>(
GenericType.BIG_INTEGER,
DataTypes.SMALLINT,
JSONCodec.ToCQL.safeNumber(BigInteger::shortValueExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<Long, Short> SMALLINT_FROM_LONG =
new JSONCodec<>(
GenericType.LONG,
DataTypes.SMALLINT,
JSONCodec.ToCQL.safeNumber(JSONCodec.ToCQL::safeLongToSmallint),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigDecimal, Byte> TINYINT_FROM_BIG_DECIMAL =
new JSONCodec<>(
GenericType.BIG_DECIMAL,
DataTypes.TINYINT,
JSONCodec.ToCQL.safeNumber(BigDecimal::byteValueExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigDecimal, BigInteger> VARINT =
public static final JSONCodec<BigInteger, Byte> TINYINT_FROM_BIG_INTEGER =
new JSONCodec<>(
GenericType.BIG_INTEGER,
DataTypes.TINYINT,
JSONCodec.ToCQL.safeNumber(BigInteger::byteValueExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<Long, Byte> TINYINT_FROM_LONG =
new JSONCodec<>(
GenericType.LONG,
DataTypes.TINYINT,
JSONCodec.ToCQL.safeNumber(JSONCodec.ToCQL::safeLongToTinyint),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigDecimal, BigInteger> VARINT_FROM_BIG_DECIMAL =
new JSONCodec<>(
GenericType.BIG_DECIMAL,
DataTypes.VARINT,
JSONCodec.ToCQL.safeNumber(BigDecimal::toBigIntegerExact),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<BigInteger, BigInteger> VARINT_FROM_BIG_INTEGER =
new JSONCodec<>(
GenericType.BIG_INTEGER,
DataTypes.VARINT,
JSONCodec.ToCQL.unsafeIdentity(),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

public static final JSONCodec<Long, BigInteger> VARINT_FROM_LONG =
new JSONCodec<>(
GenericType.LONG,
DataTypes.VARINT,
JSONCodec.ToCQL.safeNumber(BigInteger::valueOf),
JSONCodec.ToJSON.unsafeNodeFactory(JsonNodeFactory.instance::numberNode));

// Text Codecs
public static final JSONCodec<String, String> ASCII =
new JSONCodec<>(
Expand All @@ -215,6 +280,30 @@ public static <JavaT, CqlT> JSONCodec<JavaT, CqlT> codecToJSON(DataType targetCQ
static {
CODECS =
List.of(
BOOLEAN, BIGINT, DECIMAL, DOUBLE, FLOAT, INT, SMALLINT, TINYINT, VARINT, ASCII, TEXT);
// Numeric Codecs, integer types
BIGINT_FROM_BIG_DECIMAL,
BIGINT_FROM_BIG_INTEGER,
BIGINT_FROM_LONG,
INT_FROM_BIG_DECIMAL,
INT_FROM_BIG_INTEGER,
INT_FROM_LONG,
SMALLINT_FROM_BIG_DECIMAL,
SMALLINT_FROM_BIG_INTEGER,
SMALLINT_FROM_LONG,
TINYINT_FROM_BIG_DECIMAL,
TINYINT_FROM_BIG_INTEGER,
TINYINT_FROM_LONG,
VARINT_FROM_BIG_DECIMAL,
VARINT_FROM_BIG_INTEGER,
VARINT_FROM_LONG,
// Numeric Codecs, floating-point types
DECIMAL,
DOUBLE,
FLOAT,
// Text Codecs
ASCII,
TEXT,
// Other codecs
BOOLEAN);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public class ToCQLCodecException extends Exception {
public ToCQLCodecException(Object value, DataType targetCQLType, Exception cause) {
super(
String.format(
"Error trying to convert to targetCQLType `%s` from value.class `%s` and value: %s",
targetCQLType, value.getClass().getName(), value),
"Error trying to convert to targetCQLType `%s` from value.class `%s`, value %s. Root cause: %s",
targetCQLType, value.getClass().getName(), value, cause.getMessage()),
cause);
this.value = value;
this.targetCQLType = targetCQLType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import io.stargate.sgv2.jsonapi.service.operation.filters.table.TextTableFilter;
import io.stargate.sgv2.jsonapi.service.operation.query.DBFilterBase;
import io.stargate.sgv2.jsonapi.service.shredding.collections.DocumentId;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
Expand Down Expand Up @@ -104,7 +103,7 @@ private static List<DBFilterBase> findDynamic(CaptureExpression captureExpressio
captureExpression.path(),
NativeTypeTableFilter.Operator.from(
(ValueComparisonOperator) filterOperation.operator()),
(BigDecimal) rhsValue));
(Number) rhsValue));
} else if (captureExpression.marker() == DYNAMIC_DOCID_GROUP) {
Object actualValue = ((DocumentId) rhsValue).value();
if (actualValue instanceof String) {
Expand All @@ -114,13 +113,13 @@ private static List<DBFilterBase> findDynamic(CaptureExpression captureExpressio
NativeTypeTableFilter.Operator.from(
(ValueComparisonOperator) filterOperation.operator()),
(String) actualValue));
} else if (actualValue instanceof BigDecimal) {
} else if (actualValue instanceof Number) {
filters.add(
new NumberTableFilter(
captureExpression.path(),
NativeTypeTableFilter.Operator.from(
(ValueComparisonOperator) filterOperation.operator()),
(BigDecimal) actualValue));
(Number) actualValue));
} else {
throw new UnsupportedOperationException(
"Unsupported DocumentId type: " + rhsValue.getClass().getName());
Expand Down
Loading

0 comments on commit 869d6af

Please sign in to comment.