Skip to content

Commit

Permalink
POC for table projections and using JSONCodec (#1314)
Browse files Browse the repository at this point in the history
Co-authored-by: Yuqi Du <istimdu@gmail.com>
  • Loading branch information
amorton and Yuqi-Du authored Jul 29, 2024
1 parent bb82b8f commit d9b9cd5
Show file tree
Hide file tree
Showing 17 changed files with 566 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker;

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.querybuilder.relation.Relation;
import com.datastax.oss.driver.api.querybuilder.select.Select;
import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.ValueComparisonOperator;
import io.stargate.sgv2.jsonapi.exception.ErrorCode;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject;
import io.stargate.sgv2.jsonapi.service.operation.builder.BuiltCondition;
import io.stargate.sgv2.jsonapi.service.operation.builder.BuiltConditionPredicate;
import io.stargate.sgv2.jsonapi.service.operation.filters.table.codecs.FromJavaCodecException;
import io.stargate.sgv2.jsonapi.service.operation.filters.table.codecs.JSONCodecRegistry;
import io.stargate.sgv2.jsonapi.service.operation.filters.table.codecs.MissingJSONCodecException;
import io.stargate.sgv2.jsonapi.service.operation.filters.table.codecs.ToCQLCodecException;
import io.stargate.sgv2.jsonapi.service.operation.filters.table.codecs.UnknownColumnException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -99,29 +101,20 @@ public BuiltCondition get() {
public Select apply(
TableSchemaObject tableSchemaObject, Select select, List<Object> positionalValues) {

// TODO: AARON return the correct errors, this is POC work now
// TODO: Checking for valid column should be part of request deserializer or to be done in
// resolver. Should not be left till operation classes.
var column =
tableSchemaObject
.tableMetadata
.getColumn(path)
.orElseThrow(() -> new IllegalArgumentException("Column not found: " + path));

var codec =
JSONCodecRegistry.codecFor(column.getType(), columnValue)
.orElseThrow(
() ->
ErrorCode.ERROR_APPLYING_CODEC.toApiException(
"No Codec for a value of type %s with table column %s it has CQL type %s",
columnValue.getClass(),
column.getName(),
column.getType().asCql(true, false)));

try {
positionalValues.add(codec.apply(columnValue));
} catch (FromJavaCodecException e) {
throw ErrorCode.ERROR_APPLYING_CODEC.toApiException(e, "Error applying codec");
var codec =
JSONCodecRegistry.codecToCQL(
tableSchemaObject.tableMetadata, CqlIdentifier.fromCql(path), columnValue);
positionalValues.add(codec.toCQL(columnValue));
} catch (UnknownColumnException e) {
// TODO AARON - Handle error
throw new RuntimeException(e);
} catch (MissingJSONCodecException e) {
// TODO AARON - Handle error
throw new RuntimeException(e);
} catch (ToCQLCodecException e) {
// TODO AARON - Handle error
throw new RuntimeException(e);
}

return select.where(Relation.column(path).build(operator.predicate.cql, bindMarker()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import com.datastax.oss.driver.api.core.type.DataType;
import com.datastax.oss.driver.api.core.type.reflect.GenericType;
import java.util.function.BiPredicate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.function.Function;

/**
Expand All @@ -28,42 +29,75 @@
* CQL expects.
* @param targetCQLType {@link DataType} of the CQL column type the Java object needs to be
* transformed into.
* @param fromJava Function that transforms the Java object into the CQL object
* @param toCQL Function that transforms the Java object into the CQL object
* @param toJSON Function that transforms the value returned by CQL into a JsonNode
* @param <JavaT> The type of the Java object that needs to be transformed into the type CQL expects
* @param <CqlT> The type Java object the CQL driver expects
*/
public record JSONCodec<JavaT, CqlT>(
GenericType<JavaT> javaType, DataType targetCQLType, FromJava<JavaT, CqlT> fromJava)
implements BiPredicate<DataType, Object> {
GenericType<JavaT> javaType,
// TODO Mahesh, The codec looks fine 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?
DataType targetCQLType,
ToCQL<JavaT, CqlT> toCQL,
ToJSON<CqlT> toJSON) {

/**
* Call to check if this codec can convert the type of the `value` into the type needed for a
* column of the `targetCQLType`.
* column of the `toCQLType`.
*
* <p>Used to filter the list of codecs to find one that works, which can then be unchecked cast
* using {@link JSONCodec#unchecked(JSONCodec)}
*
* @param targetCQLType {@link DataType} of the CQL column the value will be written to.
* @param toCQLType {@link DataType} of the CQL column the value will be written to.
* @param value Instance of a Java value that will be written to the column.
* @return True if the codec can convert the value into the type needed for the column.
*/
@Override
public boolean test(DataType targetCQLType, Object value) {
public boolean testToCQL(DataType toCQLType, Object value) {
// java value tests comes from TypeCodec.accepts(Object value) in the driver
return this.targetCQLType.equals(targetCQLType)
return this.targetCQLType.equals(toCQLType)
&& javaType.getRawType().isAssignableFrom(value.getClass());
}

/**
* Applies the codec to the value.
* Applies the codec to the Java value read from a JSON document to convert it int the value the
* CQL driver expects.
*
* @param value Json value of type {@link JavaT} that needs to be transformed into the type CQL
* expects.
* @return Value of type {@link CqlT} that the CQL driver expects.
* @throws FromJavaCodecException if there was an error converting the value.
* @throws ToCQLCodecException if there was an error converting the value.
*/
public CqlT apply(JavaT value) throws FromJavaCodecException {
return fromJava.convert(value, targetCQLType);
public CqlT toCQL(JavaT value) throws ToCQLCodecException {
return toCQL.apply(targetCQLType, value);
}

/**
* Test if this codec can convert the CQL value into a JSON node.
*
* <p>See help for {@link #testToCQL(DataType, Object)}
*
* @param fromCQLType
* @return
*/
public boolean testToJSON(DataType fromCQLType) {
return this.targetCQLType.equals(fromCQLType);
}

/**
* Applies the codec to the value read from the CQL Driver to create a JSON node representation of
* it.
*
* @param objectMapper {@link ObjectMapper} the codec should use if it needs one.
* @param value The value read from the CQL driver that needs to be transformed into a {@link
* JsonNode}
* @return {@link JsonNode} that represents the value only, this does not include the column name.
* @throws ToJSONCodecException Checked exception raised if any error happens, users of the codec
* should convert this into the appropriate exception for the use case.
*/
public JsonNode toJSON(ObjectMapper objectMapper, CqlT value) throws ToJSONCodecException {
return toJSON.toJson(objectMapper, targetCQLType, value);
}

@SuppressWarnings("unchecked")
Expand All @@ -76,28 +110,29 @@ public static <JavaT, CqlT> JSONCodec<JavaT, CqlT> unchecked(JSONCodec<?, ?> cod
* expects.
*
* <p>The interface is used so the conversation function can throw the checked {@link
* FromJavaCodecException} and the function is also passed the target type so it can construct a
* better exception.
* ToCQLCodecException}, the function is also passed the target type, so it can construct a better
* exception.
*
* <p>Use the static constructors on the interface to get instances, see it's use in the {@link
* JSONCodecRegistry}
*
* @param <T> The type of the Java object that needs to be transformed into the type CQL expects
* @param <R> The type Java object the CQL driver expects
* @param <JavaT> The type of the Java object that needs to be transformed into the type CQL
* expects
* @param <CqlT> The type Java object the CQL driver expects
*/
@FunctionalInterface
public interface FromJava<T, R> {
public interface ToCQL<JavaT, CqlT> {

/**
* Converts the current Java value to the type CQL expects.
*
* @param t
* @param targetType The type of the CQL column the value will be written to, passed so it can
* @param toCQLType The type of the CQL column the value will be written to, passed, so it can
* be used when creating an exception if there was a error doing the transformation.
* @param value The Java value that needs to be transformed into the type CQL expects.
* @return
* @throws FromJavaCodecException
* @throws ToCQLCodecException
*/
R convert(T t, DataType targetType) throws FromJavaCodecException;
CqlT apply(DataType toCQLType, JavaT value) throws ToCQLCodecException;

/**
* Returns an instance that just returns the value passed in, the same as {@link
Expand All @@ -106,31 +141,80 @@ public interface FromJava<T, R> {
* <p>Unsafe because it does not catch any errors from the conversion, because there are none.
*
* @return
* @param <T>
* @param <JavaT>
*/
// TODO what is the point here? Is it for type-casting purpose or why is this needed?
static <T> FromJava<T, T> unsafeIdentity() {
return (t, targetType) -> t;
static <JavaT> ToCQL<JavaT, JavaT> unsafeIdentity() {
return (toCQLType, value) -> value;
}

/**
* Returns an instance that converts the value to the target type, catching any arithmetic
* exceptions and throwing them as a {@link FromJavaCodecException}
* exceptions and throwing them as a {@link ToCQLCodecException}
*
* @param function the function that does the conversion, it is expected it may throw a {@link
* ArithmeticException}
* @return
* @param <T>
* @param <R>
* @param <JavaT>
* @param <CqlT>
*/
static <T extends Number, R> FromJava<T, R> safeNumber(Function<T, R> function) {
return (t, targetType) -> {
static <JavaT extends Number, CqlT> ToCQL<JavaT, CqlT> safeNumber(
Function<JavaT, CqlT> function) {
return (toCQLType, value) -> {
try {
return function.apply(t);
return function.apply(value);
} catch (ArithmeticException e) {
throw new FromJavaCodecException(t, targetType, e);
throw new ToCQLCodecException(value, toCQLType, e);
}
};
}
}

/**
* Function 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
* JSONCodecRegistry}
*
* @param <CqlT> The type Java object the CQL driver expects
*/
@FunctionalInterface
public interface ToJSON<CqlT> {

/**
* Converts the value read from CQL to a {@link JsonNode}
*
* @param objectMapper A {@link ObjectMapper} to use to create the {@link JsonNode} if needed.
* @param fromCQLType The CQL {@link DataType} of the column that was read from CQL.
* @param value The value that was read from the CQL driver.
* @return A {@link JsonNode} that represents the value, this is just the value does not include
* the column name.
* @throws ToJSONCodecException Checked exception raised for any error, users of the function
* must catch and convert to the appropriate error for the use case.
*/
JsonNode toJson(ObjectMapper objectMapper, DataType fromCQLType, CqlT value)
throws ToJSONCodecException;

/**
* Returns an instance that will call the nodeFactoryMethod, this is typically a function from
* the {@link com.fasterxml.jackson.databind.node.JsonNodeFactory} that will create the correct
* type of node.
*
* <p>See usage in the {@link JSONCodecRegistry}
*
* <p>Unsafe because it does not catch any errors from the conversion.
*
* @param nodeFactoryMethod A function that will create a {@link JsonNode} from value of the
* {@param CqlT} type.
* @return
* @param <CqlT> The type of the Java value the driver returned.
*/
static <CqlT> ToJSON<CqlT> unsafeNodeFactory(Function<CqlT, JsonNode> nodeFactoryMethod) {
return (objectMapper, fromCQLType, value) -> nodeFactoryMethod.apply(value);
}
}
}
Loading

0 comments on commit d9b9cd5

Please sign in to comment.