diff --git a/pom.xml b/pom.xml
index f08114766..aa7d360c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,7 +107,7 @@
4.19.0
spring-data-cassandra
1.01
- 4.1.0-SNAPSHOT
+ 4.1.0-GH-3400-SNAPSHOT
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/QueryMapper.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/QueryMapper.java
index f6ac33679..e93d23d19 100644
--- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/QueryMapper.java
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/QueryMapper.java
@@ -19,7 +19,6 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
-import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -46,6 +45,7 @@
import org.springframework.data.core.PropertyReferenceException;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyHandler;
@@ -125,15 +125,15 @@ public Filter getMappedObject(Filter filter, CassandraPersistentEntity> entity
Field field = createPropertyField(entity, criteriaDefinition.getColumnName());
- field.getProperty().filter(CassandraPersistentProperty::isCompositePrimaryKey).ifPresent(it -> {
+ if (field.hasProperty(CassandraPersistentProperty::isCompositePrimaryKey)) {
throw new IllegalArgumentException(
"Cannot use composite primary key directly; Reference a property of the composite primary key");
- });
+ }
- field.getProperty().filter(CassandraPersistentProperty::hasOrdinal).ifPresent(it -> {
+ if (field.hasProperty(CassandraPersistentProperty::hasOrdinal)) {
throw new IllegalArgumentException(
String.format("Cannot reference tuple value elements, property [%s]", field.getMappedKey()));
- });
+ }
Predicate predicate = criteriaDefinition.getPredicate();
Object value = predicate.getValue();
@@ -148,10 +148,9 @@ public Filter getMappedObject(Filter filter, CassandraPersistentEntity> entity
protected @Nullable Object getMappedValue(Field field, CriteriaDefinition.Operator operator, Object value) {
- if (field.getProperty().isPresent()
- && field.getProperty().filter(it -> converter.getCustomConversions().hasValueConverter(it)).isPresent()) {
+ if (field.hasProperty() && converter.getCustomConversions().hasValueConverter(field.getProperty())) {
- CassandraPersistentProperty property = field.getProperty().get();
+ CassandraPersistentProperty property = field.getProperty();
CassandraConversionContext conversionContext = new CassandraConversionContext(new PropertyValueProvider<>() {
@Override
public T getPropertyValue(CassandraPersistentProperty property) {
@@ -202,11 +201,23 @@ public List getMappedSelectors(Columns columns, CassandraPersistentEnt
columns.getSelector(column).forEach(selector -> {
- List mappedColumnNames = getCqlIdentifier(column, field);
+ List mappedColumnNames = getCqlIdentifiers(field);
- for (CqlIdentifier mappedColumnName : mappedColumnNames) {
- selectors.add(getMappedSelector(selector, mappedColumnName, field));
+ if (selector instanceof ColumnSelector cs && cs.getAlias().isEmpty()) {
+
+ for (CqlIdentifier mappedColumnName : mappedColumnNames) {
+ selectors.add(ColumnSelector.from(mappedColumnName));
+ }
+
+ return;
+ }
+
+ if (mappedColumnNames.size() != 1) {
+ throw new IllegalArgumentException(
+ "Cannot map selector %s to multiple columns %s".formatted(selector, mappedColumnNames));
}
+
+ selectors.add(getMappedSelector(selector, entity, field));
});
}
@@ -231,13 +242,10 @@ private void addColumns(CassandraPersistentEntity> entity, List sele
});
}
- private Selector getMappedSelector(Selector selector, CqlIdentifier cqlIdentifier, Field field) {
+ private Selector getMappedSelector(Selector selector, CassandraPersistentEntity> entity, Field field) {
if (selector instanceof ColumnSelector columnSelector) {
-
- ColumnSelector mappedColumnSelector = ColumnSelector.from(cqlIdentifier);
-
- return columnSelector.getAlias().map(mappedColumnSelector::as).orElse(mappedColumnSelector);
+ return getMappedSelector(columnSelector, entity);
}
if (selector instanceof FunctionCall functionCall) {
@@ -246,22 +254,18 @@ private Selector getMappedSelector(Selector selector, CqlIdentifier cqlIdentifie
functionCall.getParameters().stream().map(obj -> {
if (obj instanceof Selector sel) {
- return getMappedSelector(sel, cqlIdentifier, field);
+ return getMappedSelector(sel, entity, field);
}
if (obj instanceof ColumnName cn) {
-
- CqlIdentifier identifier = cn.getCqlIdentifier().or(() -> cn.getColumnName().map(CqlIdentifier::fromCql))
- .orElseGet(() -> CqlIdentifier.fromCql(cn.toCql()));
-
- return getMappedSelector(ColumnSelector.from(cn), identifier, field);
+ return getMappedSelector(cn, entity);
}
if (obj instanceof CqlIdentifier identifier) {
- return getMappedSelector(ColumnSelector.from(identifier), identifier, field);
+ return getMappedSelector(ColumnName.from(identifier), entity);
}
- if (field.getProperty().isPresent()) {
+ if (field.hasProperty()) {
return getMappedValue(field, CriteriaDefinition.Operators.EQ, obj);
}
@@ -275,6 +279,17 @@ private Selector getMappedSelector(Selector selector, CqlIdentifier cqlIdentifie
throw new IllegalArgumentException(String.format("Selector [%s] not supported", selector));
}
+ private ColumnSelector getMappedSelector(ColumnSelector columnSelector, CassandraPersistentEntity> entity) {
+
+ ColumnSelector mappedSelector = getMappedSelector(columnSelector.getColumnName(), entity);
+ return columnSelector.getAlias().map(mappedSelector::as).orElse(mappedSelector);
+ }
+
+ private ColumnSelector getMappedSelector(ColumnName columnName, CassandraPersistentEntity> entity) {
+ Field mappedField = createPropertyField(entity, columnName);
+ return ColumnSelector.from(mappedField.getMappedKey());
+ }
+
/**
* Map {@link Columns} with a {@link CassandraPersistentEntity type hint} to column names for included columns.
* Function call selectors or other {@link org.springframework.data.cassandra.core.query.Columns.Selector} types are
@@ -300,12 +315,14 @@ public List getMappedColumnNames(Columns columns, CassandraPersis
for (ColumnName column : columns) {
Field field = createPropertyField(entity, column);
- field.getProperty().ifPresent(seen::add);
+ if (field.hasProperty()) {
+ seen.add(field.getProperty());
+ }
columns.getSelector(column).forEach(columnSelector -> {
if (columnSelector instanceof ColumnSelector) {
- columnNames.addAll(getCqlIdentifier(column, field));
+ columnNames.addAll(getCqlIdentifiers(field));
}
});
}
@@ -342,15 +359,13 @@ public Sort getMappedSort(Sort sort, CassandraPersistentEntity> entity) {
for (Order order : sort) {
- ColumnName columnName = ColumnName.from(order.getProperty());
-
- Field field = createPropertyField(entity, columnName);
+ Field field = createPropertyField(entity, order.getProperty());
if (vector != null) {
vector = getMappedValue(field, CriteriaDefinition.Operators.EQ, vector);
}
- List mappedColumnNames = getCqlIdentifier(columnName, field);
+ List mappedColumnNames = getCqlIdentifiers(field);
if (mappedColumnNames.isEmpty()) {
mappedOrders.add(order);
@@ -364,14 +379,14 @@ public Sort getMappedSort(Sort sort, CassandraPersistentEntity> entity) {
return vector != null ? new VectorSort(mappedOrders, vector) : Sort.by(mappedOrders);
}
- private List getCqlIdentifier(ColumnName column, Field field) {
+ private List getCqlIdentifiers(Field field) {
List identifiers = new ArrayList<>(1);
try {
- if (field.getProperty().isPresent()) {
+ if (field.hasProperty()) {
- CassandraPersistentProperty property = field.getProperty().get();
+ CassandraPersistentProperty property = field.getProperty();
if (property.isCompositePrimaryKey()) {
@@ -383,10 +398,8 @@ private List getCqlIdentifier(ColumnName column, Field field) {
} else {
identifiers.add(property.getRequiredColumnName());
}
- } else if (column.getColumnName().isPresent()) {
- identifiers.add(CqlIdentifier.fromCql(column.getColumnName().get()));
} else {
- column.getCqlIdentifier().ifPresent(identifiers::add);
+ identifiers.add(field.getMappedKey().getRequiredCqlIdentifier());
}
} catch (IllegalStateException cause) {
@@ -396,18 +409,28 @@ private List getCqlIdentifier(ColumnName column, Field field) {
return identifiers;
}
+ Field createPropertyField(@Nullable CassandraPersistentEntity> entity, String key) {
+ return createPropertyField(entity, ColumnName.from(key));
+ }
+
Field createPropertyField(@Nullable CassandraPersistentEntity> entity, ColumnName key) {
- return Optional.ofNullable(entity). map(e -> new MetadataBackedField(key, e, getMappingContext()))
- .orElseGet(() -> new Field(key));
+ if (entity == null) {
+ return new Field(key);
+ }
+
+ return MetadataBackedField.of(key, entity, getMappingContext());
}
ColumnType getColumnType(Field field, @Nullable Object value, ColumnTypeTransformer operator) {
ColumnTypeResolver resolver = converter.getColumnTypeResolver();
- return field.getProperty().map(it -> operator.transform(resolver.resolve(it), it))
- .orElseGet(() -> resolver.resolve(value));
+ if (field.hasProperty()) {
+ return operator.transform(resolver.resolve(field.getProperty()), field.getProperty());
+ }
+
+ return resolver.resolve(value);
}
/**
@@ -513,8 +536,11 @@ ColumnType transform(ColumnType typeDescriptor, CassandraPersistentProperty prop
static ColumnTypeTransformer of(Field field, CriteriaDefinition.Operator operator) {
if (operator == CriteriaDefinition.Operators.CONTAINS) {
- return field.getProperty().filter(CassandraPersistentProperty::isMapLike).map(it -> MAP_VALUE_TYPE)
- .orElse(COLLECTION_COMPONENT_TYPE);
+
+ if (field.hasProperty(CassandraPersistentProperty::isMapLike)) {
+ return MAP_VALUE_TYPE;
+ }
+ return COLLECTION_COMPONENT_TYPE;
}
if (operator == CriteriaDefinition.Operators.CONTAINS_KEY) {
@@ -558,13 +584,37 @@ public Field with(ColumnName name) {
return new Field(name);
}
+ /**
+ * Return {@literal true} if the field is backed by a {@link CassandraPersistentProperty}.
+ *
+ * @return {@literal true} if the field is backed by a property; {@literal false} otherwise.
+ * @since 5.1
+ */
+ public boolean hasProperty() {
+ return false;
+ }
+
+ /**
+ * Return {@literal true} if the field is backed by a {@link CassandraPersistentProperty} and matches the given
+ * {@link Predicate}.
+ *
+ * @return {@literal true} if the field is backed by a property and matches the given predicate; {@literal false}
+ * otherwise.
+ * @since 5.1
+ */
+ public boolean hasProperty(java.util.function.Predicate predicate) {
+ return hasProperty() && predicate.test(getProperty());
+ }
+
/**
* Returns the underlying {@link CassandraPersistentProperty} backing the field. For path traversals this will be
* the property that represents the value to handle. This means it'll be the leaf property for plain paths or the
* association property in case we refer to an association somewhere in the path.
+ *
+ * @throws IllegalStateException if the field is not backed by a property.
*/
- public Optional getProperty() {
- return Optional.empty();
+ public CassandraPersistentProperty getProperty() {
+ throw new IllegalStateException("No property for this field: " + getMappedKey());
}
/**
@@ -586,93 +636,96 @@ protected static class MetadataBackedField extends Field {
private final MappingContext extends CassandraPersistentEntity>, CassandraPersistentProperty> mappingContext;
- private final Optional> path;
+ private final @Nullable PropertyPath propertyPath;
+
+ private final @Nullable PersistentPropertyPath path;
private final @Nullable CassandraPersistentProperty property;
- private final Optional optionalProperty;
+ private MetadataBackedField(ColumnName name, CassandraPersistentEntity> entity,
+ MappingContext extends CassandraPersistentEntity>, CassandraPersistentProperty> mappingContext,
+ @Nullable PropertyPath propertyPath) {
- /**
- * Creates a new {@link MetadataBackedField} with the given name, {@link CassandraPersistentEntity} and
- * {@link MappingContext}.
- *
- * @param name must not be {@literal null} or empty.
- * @param entity must not be {@literal null}.
- * @param mappingContext must not be {@literal null}.
- */
- public MetadataBackedField(ColumnName name, CassandraPersistentEntity> entity,
- MappingContext extends CassandraPersistentEntity>, CassandraPersistentProperty> mappingContext) {
+ super(name);
+ this.entity = entity;
+ this.mappingContext = mappingContext;
+ this.propertyPath = propertyPath;
+
+ if (propertyPath != null) {
- this(name, entity, mappingContext, null);
+ PersistentPropertyPath ppp = mappingContext
+ .getPersistentPropertyPath(propertyPath);
+ this.path = ppp;
+ this.property = ppp.getLeafProperty();
+ } else {
+ this.path = null;
+ this.property = null;
+ }
}
/**
- * Creates a new {@link MetadataBackedField} with the given name, {@link CassandraPersistentProperty} and
- * {@link MappingContext} with the given {@link CassandraPersistentProperty}.
+ * Create a new {@link MetadataBackedField} with the given name, {@link CassandraPersistentEntity} and
+ * {@link MappingContext}.
*
- * @param name must not be {@literal null} or empty.
+ * @param columnName must not be {@literal null} or empty.
* @param entity must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
- * @param property may be {@literal null}.
*/
- public MetadataBackedField(ColumnName name, CassandraPersistentEntity> entity,
- MappingContext extends CassandraPersistentEntity>, CassandraPersistentProperty> mappingContext,
- @Nullable CassandraPersistentProperty property) {
+ public static MetadataBackedField of(ColumnName columnName, CassandraPersistentEntity> entity,
+ CassandraMappingContext mappingContext) {
- super(name);
+ PropertyPath propertyPath;
+ if (columnName.hasPropertyPath()) {
+ propertyPath = columnName.getRequiredPropertyPath();
- Assert.notNull(entity, "CassandraPersistentEntity must not be null");
+ } else {
+ propertyPath = getPath(entity, columnName.toCql());
+ }
- this.entity = entity;
- this.mappingContext = mappingContext;
- this.path = getPath(name.toCql());
- this.property = path.map(PersistentPropertyPath::getLeafProperty).orElse(property);
- this.optionalProperty = Optional.ofNullable(this.property);
+ return new MetadataBackedField(columnName, entity, mappingContext, propertyPath);
}
- /**
- * Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}.
- *
- * @param pathExpression {@link String} containing the path expression to evaluate
- * @return the {@link PersistentPropertyPath} for the given {@code pathExpression}.
- */
- private Optional> getPath(String pathExpression) {
+ private static @Nullable PropertyPath getPath(PersistentEntity, ?> entity, String pathExpression) {
try {
- PropertyPath propertyPath = PropertyPath.from(pathExpression.replaceAll("\\.\\d", ""),
- this.entity.getTypeInformation());
-
- PersistentPropertyPath persistentPropertyPath = this.mappingContext
- .getPersistentPropertyPath(propertyPath);
-
- return Optional.of(persistentPropertyPath);
+ return PropertyPath.from(pathExpression.replaceAll("\\.\\d", ""), entity.getTypeInformation());
} catch (PropertyReferenceException e) {
- return Optional.empty();
+ return null;
}
}
@Override
public MetadataBackedField with(ColumnName name) {
- return new MetadataBackedField(name, entity, mappingContext, property);
+ return new MetadataBackedField(name, entity, mappingContext, propertyPath);
}
@Override
- public Optional getProperty() {
- return this.optionalProperty;
+ public boolean hasProperty() {
+ return this.property != null;
+ }
+
+ @Override
+ public CassandraPersistentProperty getProperty() {
+
+ if (this.property == null) {
+ return super.getProperty();
+ }
+
+ return this.property;
}
@Override
@SuppressWarnings("NullAway")
public ColumnName getMappedKey() {
- if (path.isEmpty()) {
+ if (path == null || path.isEmpty()) {
return name;
}
boolean embedded = false;
CassandraPersistentEntity> parentEntity = null;
CassandraPersistentProperty leafProperty = null;
- for (CassandraPersistentProperty p : path.get()) {
+ for (CassandraPersistentProperty p : path) {
leafProperty = p;
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UpdateMapper.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UpdateMapper.java
index ac3fbccad..f8238edb3 100644
--- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UpdateMapper.java
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UpdateMapper.java
@@ -28,10 +28,10 @@
import java.util.Set;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
+import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.cassandra.core.query.Filter;
import org.springframework.data.cassandra.core.query.Update;
import org.springframework.data.core.TypeInformation;
-import org.springframework.data.mapping.PersistentProperty;
import org.springframework.util.Assert;
import com.datastax.oss.driver.api.core.type.DataType;
@@ -81,10 +81,10 @@ public Update getMappedObject(Update update, CassandraPersistentEntity> entity
Field field = createPropertyField(entity, assignmentOp.getColumnName());
- field.getProperty().filter(it -> it.getOrdinal() != null).ifPresent(it -> {
+ if (field.hasProperty(CassandraPersistentProperty::hasOrdinal)) {
throw new IllegalArgumentException(
String.format("Cannot reference tuple value elements, property [%s]", field.getMappedKey()));
- });
+ }
mapped.add(getMappedUpdateOperation(assignmentOp, field));
}
@@ -128,8 +128,9 @@ private AssignmentOp getMappedUpdateOperation(Field field, SetOp updateOp) {
Assert.state(op.getValue() != null,
() -> String.format("SetAtKeyOp for %s attempts to set null", field.getProperty()));
- Optional extends TypeInformation>> typeInformation = field.getProperty()
- .map(PersistentProperty::getTypeInformation);
+ Optional extends TypeInformation>> typeInformation = field.hasProperty()
+ ? Optional.of(field.getProperty().getTypeInformation())
+ : Optional.empty();
Optional> keyType = typeInformation.map(TypeInformation::getComponentType);
Optional> valueType = typeInformation.map(TypeInformation::getMapValueType);
@@ -161,9 +162,9 @@ private AssignmentOp getMappedUpdateOperation(Field field, SetOp updateOp) {
if (collection.isEmpty()) {
- int protocolCode = field.getProperty()
- .map(property -> getConverter().getColumnTypeResolver().resolve(property).getDataType())
- .map(DataType::getProtocolCode).orElse(ProtocolConstants.DataType.LIST);
+ int protocolCode = field.hasProperty()
+ ? getConverter().getColumnTypeResolver().resolve(field.getProperty()).getDataType().getProtocolCode()
+ : ProtocolConstants.DataType.LIST;
if (protocolCode == ProtocolConstants.DataType.SET) {
return new SetOp(field.getMappedKey(), Collections.emptySet());
@@ -184,7 +185,7 @@ private AssignmentOp getMappedUpdateOperation(Field field, RemoveOp updateOp) {
ColumnType descriptor = getColumnType(field, value, ColumnTypeTransformer.AS_IS);
boolean mapLike = false;
- if (field.getProperty().isPresent() && field.getProperty().get().isMapLike()) {
+ if (field.hasProperty(CassandraPersistentProperty::isMapLike)) {
descriptor = getColumnType(field, value, value instanceof Collection ? ColumnTypeTransformer.ENCLOSING_MAP_KEY_SET
: ColumnTypeTransformer.MAP_KEY_TYPE);
@@ -207,9 +208,9 @@ private AssignmentOp getMappedUpdateOperation(Field field, AddToOp updateOp) {
ColumnType descriptor = getColumnType(field, value, ColumnTypeTransformer.AS_IS);
Collection