documentClass) {
+ return getCollection(getCollectionName(documentClass), documentClass, defaultCollectionOptions());
+ }
+
/**
* Retrieves a {@link Collection} object for the specified collection name with the ability to specify custom options.
*
@@ -754,6 +869,44 @@ public Collection getCollection(String collectionName, Class documentC
return new Collection<>(this, collectionName, options, documentClass);
}
+ /**
+ * Retrieves a {@link Collection} object for a class annotated with {@link DataApiCollection}
+ * with custom options.
+ *
+ * This method provides a way to obtain a {@link Collection} instance by using the collection
+ * name extracted from the {@link DataApiCollection} annotation on the provided class, while
+ * allowing customization of the collection behavior through the specified {@link CollectionOptions}.
+ *
+ *
+ * @param the type of the documents stored in the collection
+ * @param documentClass the class representing the document type; must not be null and must be
+ * annotated with {@link DataApiCollection}
+ * @param options the {@link CollectionOptions} to customize the collection behavior; must not be null
+ * @return a {@link Collection} object representing the collection associated with the annotated class,
+ * configured with the provided options
+ * @throws IllegalArgumentException if the class is not annotated with {@link DataApiCollection}
+ * or if the annotation's name attribute is empty
+ * @throws NullPointerException if {@code documentClass} or {@code options} is null
+ *
+ * Example usage:
+ *
+ * {@code
+ * @DataApiCollection(name = "users")
+ * public class User {
+ * private String id;
+ * private String name;
+ * }
+ *
+ * CollectionOptions options = new CollectionOptions()
+ * .timeout(Duration.ofMillis(1000));
+ * Collection userCollection = database.getCollection(User.class, options);
+ * }
+ *
+ */
+ public Collection getCollection(Class documentClass, CollectionOptions options) {
+ return getCollection(getCollectionName(documentClass), documentClass, options);
+ }
+
// ------------------------------------------
// ---- Create Collection ----
// ------------------------------------------
@@ -842,6 +995,76 @@ public Collection createCollection(String collectionName,
return getCollection(collectionName, documentClass, collectionOptions);
}
+ /**
+ * Creates a new collection based on a class annotated with {@link DataApiCollection}.
+ *
+ * This method creates a collection using the name and definition extracted from the
+ * {@link DataApiCollection} annotation on the provided class. The collection creation
+ * can be customized using the specified {@link CreateCollectionOptions}.
+ *
+ *
+ * @param the type of the documents stored in the collection
+ * @param documentClass the class representing the document type; must not be null and must be
+ * annotated with {@link DataApiCollection}
+ * @param createCollectionOptions additional options for creating the collection, such as timeouts
+ * or retry policies; can be null for default options
+ * @return the created {@link Collection} object configured with the provided options
+ * @throws IllegalArgumentException if the class is not annotated with {@link DataApiCollection}
+ * or if the annotation's name attribute is empty
+ * @throws NullPointerException if {@code documentClass} is null
+ *
+ * Example usage:
+ *
+ * {@code
+ * @DataApiCollection(name = "users")
+ * public class User {
+ * private String id;
+ * private String name;
+ * }
+ *
+ * CreateCollectionOptions options = new CreateCollectionOptions()
+ * .timeout(Duration.ofMillis(1000));
+ * Collection userCollection = database.createCollection(User.class, options);
+ * }
+ *
+ */
+ public Collection createCollection(Class documentClass, CreateCollectionOptions createCollectionOptions) {
+ return createCollection(getCollectionName(documentClass), getCollectionDefinition(documentClass), documentClass, createCollectionOptions);
+ }
+
+ /**
+ * Creates a new collection based on a class annotated with {@link DataApiCollection} using default options.
+ *
+ * This method creates a collection using the name and definition extracted from the
+ * {@link DataApiCollection} annotation on the provided class. The collection is created
+ * with default options derived from the current database settings.
+ *
+ *
+ * @param the type of the documents stored in the collection
+ * @param documentClass the class representing the document type; must not be null and must be
+ * annotated with {@link DataApiCollection}
+ * @return the created {@link Collection} object with default configuration
+ * @throws IllegalArgumentException if the class is not annotated with {@link DataApiCollection}
+ * or if the annotation's name attribute is empty
+ * @throws NullPointerException if {@code documentClass} is null
+ *
+ * Example usage:
+ *
+ * {@code
+ * @DataApiCollection(name = "users")
+ * public class User {
+ * private String id;
+ * private String name;
+ * }
+ *
+ * Collection userCollection = database.createCollection(User.class);
+ * }
+ *
+ */
+ public Collection createCollection(Class documentClass) {
+ return createCollection(getCollectionName(documentClass), getCollectionDefinition(documentClass), documentClass);
+ }
+
/**
* Creates a new collection with the default document type {@link Document}.
*
@@ -986,6 +1209,36 @@ public void dropCollection(String collectionName) {
dropCollection(collectionName, null);
}
+ /**
+ * Deletes a collection from the database based on a class annotated with {@link DataApiCollection}.
+ *
+ * This method deletes the collection whose name is extracted from the {@link DataApiCollection}
+ * annotation on the provided class. The operation uses default options.
+ *
+ *
+ * @param documentClass the class representing the document type; must not be null and must be
+ * annotated with {@link DataApiCollection}
+ * @throws IllegalArgumentException if the class is not annotated with {@link DataApiCollection}
+ * or if the annotation's name attribute is empty
+ * @throws NullPointerException if {@code documentClass} is null
+ *
+ * Example usage:
+ *
+ * {@code
+ * @DataApiCollection(name = "users")
+ * public class User {
+ * private String id;
+ * private String name;
+ * }
+ *
+ * database.dropCollection(User.class);
+ * }
+ *
+ */
+ public void dropCollection(Class> documentClass) {
+ dropCollection(getCollectionName(documentClass), null);
+ }
+
// ------------------------------------------
// ------- List tables -----
// ------------------------------------------
@@ -1196,20 +1449,31 @@ public Table getTable(String tableName, TableOptions tableOptions) {
/**
* Retrieves a table representation for a row class annotated with {@link EntityTable}.
+ *
* The table name is inferred from the {@code value} attribute of the {@code EntityTable} annotation.
+ * This method provides a convenient way to obtain a {@link Table} instance without explicitly
+ * specifying the table name, as it is automatically extracted from the annotation.
+ *
*
- * @param the type of the row objects
- * @param rowClass the class representing the type of rows in the table (must be annotated with {@link EntityTable})
- * @return a {@code Table} instance for the inferred table name and row type
+ * @param the type of the row objects
+ * @param rowClass the class representing the type of rows in the table; must not be null and must be
+ * annotated with {@link EntityTable}
+ * @return a {@code Table} instance for the inferred table name and row type, configured with
+ * default options
* @throws InvalidConfigurationException if the provided class is not annotated with {@link EntityTable}
+ * or if the annotation's value attribute is empty
+ * @throws NullPointerException if {@code rowClass} is null
*
* Example usage:
*
* {@code
* @EntityTable("my_table")
- * public class MyRowType { ... }
+ * public class MyRowType {
+ * private String id;
+ * private String name;
+ * }
*
- * Table table = myFramework.getTable(MyRowType.class);
+ * Table table = database.getTable(MyRowType.class);
* }
*
*/
@@ -1230,17 +1494,35 @@ public Table getTable(Class rowClass) {
/**
* Retrieves the {@link TableUserDefinedTypeDefinition} associated with the specified class.
+ *
+ * The class must be annotated with {@link TableUserDefinedType}, which is used to extract
+ * the metadata needed to construct a UDT (User-Defined Type) definition. This method uses
+ * reflection to analyze the class structure and generate the appropriate type definition.
+ *
*
- * The class must be annotated with {@link TableUserDefinedType}, which is used to extract
- * the metadata needed to construct a UDT (User-Defined Type) definition.
- *
- *
If the class is not annotated correctly, an {@link InvalidConfigurationException} is thrown.
+ *
Note: This method currently returns {@code null} and requires implementation.
*
- * @param rowClass the class annotated with {@link TableUserDefinedType}
* @param the type of the class
- * @return the user-defined type metadata definition for the specified class
+ * @param rowClass the class annotated with {@link TableUserDefinedType}; must not be null
+ * @return the user-defined type metadata definition for the specified class, or {@code null}
+ * if not yet implemented
* @throws InvalidConfigurationException if the class is not annotated with {@link TableUserDefinedType}
- * @throws NullPointerException if {@code rowClass} is {@code null}
+ * or if the annotation's value attribute is empty
+ * @throws NullPointerException if {@code rowClass} is null
+ *
+ * Example usage:
+ *
+ * {@code
+ * @TableUserDefinedType("address_type")
+ * public class Address {
+ * private String street;
+ * private String city;
+ * private String zipCode;
+ * }
+ *
+ * TableUserDefinedTypeDefinition typeDef = database.getType(Address.class);
+ * }
+ *
*/
public TableUserDefinedTypeDefinition getType(Class rowClass) {
TableUserDefinedType ann = rowClass.getAnnotation(TableUserDefinedType.class);
@@ -1256,11 +1538,34 @@ public TableUserDefinedTypeDefinition getType(Class rowClass) {
}
/**
- * Creates a table using default options and runtime configurations.
+ * Retrieves the type name from a class annotated with {@link TableUserDefinedType}.
+ *
+ * This method extracts the type name from the {@code value} attribute of the
+ * {@link TableUserDefinedType} annotation on the provided class. The class must be properly
+ * annotated, and the annotation must specify a non-empty name.
+ *
*
- * @param the type of the row objects that the table will hold
- * @param rowClass the class representing the row type; must not be null
- * @return the created table object
+ * @param the type of the class
+ * @param rowClass the class representing the user-defined type; must not be null and must be
+ * annotated with {@link TableUserDefinedType}
+ * @return the type name as specified in the {@link TableUserDefinedType} annotation
+ * @throws IllegalArgumentException if the class is not annotated with {@link TableUserDefinedType}
+ * or if the annotation's value attribute is empty
+ * @throws NullPointerException if {@code rowClass} is null
+ *
+ * Example usage:
+ *
+ * {@code
+ * @TableUserDefinedType("address_type")
+ * public class Address {
+ * private String street;
+ * private String city;
+ * }
+ *
+ * String typeName = database.getTypeName(Address.class);
+ * // Returns: "address_type"
+ * }
+ *
*/
public String getTypeName(Class rowClass) {
notNull(rowClass, "typeClass");
@@ -1443,7 +1748,7 @@ public void createType(String typeName, TableUserDefinedTypeDefinition tableUser
public void createType(Class UdtBean, CreateTypeOptions createTypeOptions) {
notNull(UdtBean, "udtDefinition");
TableUserDefinedType ann = UdtBean.getAnnotation(TableUserDefinedType.class);
- Command createTypeCmd = new Command("createType", EntityBeanDefinition.createTypeCommand(UdtBean));
+ Command createTypeCmd = new Command("createType", EntityTableBeanDefinition.createTypeCommand(UdtBean));
if (createTypeOptions != null) {
createTypeCmd.append("options", createTypeOptions);
}
@@ -1763,7 +2068,7 @@ public Table createTable(String tableName,
Table table = getTable(tableName, rowClass, tableOptions);
// Creating Vector Index for each column definition
- EntityBeanDefinition.listVectorIndexDefinitions(tableName, rowClass).forEach(index -> {
+ EntityTableBeanDefinition.listVectorIndexDefinitions(tableName, rowClass).forEach(index -> {
CreateVectorIndexOptions options = new CreateVectorIndexOptions().ifNotExists(true)
.dataAPIClientOptions(createTableOptions.getDataAPIClientOptions().enableFeatureFlagTables());
table.createVectorIndex("vidx_" + tableName + "_" + index.getColumn().getName(), index, options);
@@ -1791,11 +2096,22 @@ public String getTableName(Class rowClass) {
return ann.value();
}
+
+
/**
- * Initialize a TableOption from the current database options.
+ * Initializes a {@link TableOptions} object from the current database options.
+ *
+ * This private helper method creates a new {@link TableOptions} instance configured with
+ * the authentication token and client options from the current database settings. The
+ * keyspace is also set to match the current database's keyspace.
+ *
+ *
+ * This method is used internally to provide consistent default options when creating or
+ * accessing tables without explicit option parameters.
+ *
*
- * @return
- * default table options
+ * @return a {@link TableOptions} object initialized with the current database's token,
+ * client options, and keyspace
*/
private TableOptions defaultTableOptions() {
return new TableOptions(this.options.getToken(),
@@ -1804,10 +2120,19 @@ private TableOptions defaultTableOptions() {
}
/**
- * Initialize a TableOption from the current database options.
+ * Initializes a {@link CollectionOptions} object from the current database options.
+ *
+ * This private helper method creates a new {@link CollectionOptions} instance configured with
+ * the authentication token and client options from the current database settings. The
+ * keyspace is also set to match the current database's keyspace.
+ *
+ *
+ * This method is used internally to provide consistent default options when creating or
+ * accessing collections without explicit option parameters.
+ *
*
- * @return
- * default table options
+ * @return a {@link CollectionOptions} object initialized with the current database's token,
+ * client options, and keyspace
*/
private CollectionOptions defaultCollectionOptions() {
return new CollectionOptions(this.options.getToken(),
diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/tables/definition/TableDefinition.java b/astra-db-java/src/main/java/com/datastax/astra/client/tables/definition/TableDefinition.java
index 46dd2c79..894334de 100644
--- a/astra-db-java/src/main/java/com/datastax/astra/client/tables/definition/TableDefinition.java
+++ b/astra-db-java/src/main/java/com/datastax/astra/client/tables/definition/TableDefinition.java
@@ -345,4 +345,49 @@ public TableDefinition clusteringColumns(Sort... clusteringColumns) {
public String toString() {
return new RowSerializer().marshall(this);
}
+
+ /**
+ * Custom equals method for comparing table definitions.
+ * Compares columns and primary key structure.
+ *
+ * @param o the object to compare with
+ * @return true if the table definitions are equal, false otherwise
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TableDefinition that = (TableDefinition) o;
+
+ // Compare columns
+ if (columns == null && that.columns != null) return false;
+ if (columns != null && that.columns == null) return false;
+ if (columns != null) {
+ if (columns.size() != that.columns.size()) return false;
+ for (String key : columns.keySet()) {
+ if (!that.columns.containsKey(key)) return false;
+ TableColumnDefinition thisCol = columns.get(key);
+ TableColumnDefinition thatCol = that.columns.get(key);
+ if (!thisCol.equals(thatCol)) return false;
+ }
+ }
+
+ // Compare primary key
+ if (primaryKey == null && that.primaryKey != null) return false;
+ if (primaryKey != null && that.primaryKey == null) return false;
+ return primaryKey == null || primaryKey.equals(that.primaryKey);
+ }
+
+ /**
+ * Custom hashCode method consistent with equals.
+ *
+ * @return the hash code
+ */
+ @Override
+ public int hashCode() {
+ int result = columns != null ? columns.hashCode() : 0;
+ result = 31 * result + (primaryKey != null ? primaryKey.hashCode() : 0);
+ return result;
+ }
}
diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKey.java b/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKey.java
new file mode 100644
index 00000000..474b33b3
--- /dev/null
+++ b/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKey.java
@@ -0,0 +1,76 @@
+package com.datastax.astra.client.tables.mapping;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a field as containing the primary key for a table entity.
+ *
+ * This annotation is used on a field whose type is annotated with {@link TablePrimaryKeyClass}.
+ * The primary key class encapsulates all partition key and clustering column components.
+ *
+ *
+ * This pattern is similar to Spring Data Cassandra's {@code @PrimaryKey} annotation and allows
+ * for cleaner entity modeling when dealing with composite keys.
+ *
+ * Usage Example:
+ *
+ * {@code
+ * @TablePrimaryKeyClass
+ * public class OrderKey {
+ * @PartitionBy(1)
+ * @Column("customer_id")
+ * private String customerId;
+ *
+ * @PartitionSort(position = 1, order = PartitionSortOrder.ASC)
+ * @Column("order_date")
+ * private LocalDate orderDate;
+ *
+ * // constructors, getters, setters, equals, hashCode
+ * }
+ *
+ * @EntityTable("orders")
+ * public class Order {
+ * @TablePrimaryKey
+ * private OrderKey key;
+ *
+ * @Column("amount")
+ * private BigDecimal amount;
+ *
+ * // getters and setters
+ * }
+ * }
+ *
+ *
+ * Retention: {@code RUNTIME}
+ * This annotation is retained at runtime to allow runtime reflection.
+ *
+ * Target: {@code FIELD}
+ * This annotation can only be applied to fields.
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TablePrimaryKey {
+}
diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKeyClass.java b/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKeyClass.java
new file mode 100644
index 00000000..093b4eef
--- /dev/null
+++ b/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKeyClass.java
@@ -0,0 +1,78 @@
+package com.datastax.astra.client.tables.mapping;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a class as representing a composite primary key for a table entity.
+ *
+ * This annotation is used to define a separate class that encapsulates the primary key
+ * components (partition key + clustering columns) of a table. Fields within this class
+ * should be annotated with {@link PartitionBy} and {@link PartitionSort} to define
+ * the key structure.
+ *
+ *
+ * This pattern is similar to Spring Data Cassandra's {@code @PrimaryKeyClass} and allows
+ * for cleaner entity modeling when dealing with composite keys.
+ *
+ * Usage Example:
+ *
+ * {@code
+ * @TablePrimaryKeyClass
+ * public class OrderKey {
+ * @PartitionBy(1)
+ * @Column("customer_id")
+ * private String customerId;
+ *
+ * @PartitionSort(position = 1, order = PartitionSortOrder.ASC)
+ * @Column("order_date")
+ * private LocalDate orderDate;
+ *
+ * // constructors, getters, setters, equals, hashCode
+ * }
+ *
+ * @EntityTable("orders")
+ * public class Order {
+ * @TablePrimaryKey
+ * private OrderKey key;
+ *
+ * @Column("amount")
+ * private BigDecimal amount;
+ *
+ * // getters and setters
+ * }
+ * }
+ *
+ *
+ * Retention: {@code RUNTIME}
+ * This annotation is retained at runtime to allow runtime reflection.
+ *
+ * Target: {@code TYPE}
+ * This annotation can only be applied to classes.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TablePrimaryKeyClass {
+}
diff --git a/astra-db-java/src/main/java/com/datastax/astra/internal/command/AbstractCommandRunner.java b/astra-db-java/src/main/java/com/datastax/astra/internal/command/AbstractCommandRunner.java
index 12b2b4f9..8b41ffb8 100644
--- a/astra-db-java/src/main/java/com/datastax/astra/internal/command/AbstractCommandRunner.java
+++ b/astra-db-java/src/main/java/com/datastax/astra/internal/command/AbstractCommandRunner.java
@@ -34,8 +34,6 @@
import com.datastax.astra.internal.serdes.DataAPISerializer;
import com.datastax.astra.internal.utils.Assert;
import com.datastax.astra.internal.utils.CompletableFutures;
-import com.datastax.astra.internal.utils.EscapeUtils;
-import com.dtsx.astra.sdk.utils.JsonUtils;
import com.evanlennick.retry4j.Status;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
diff --git a/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java b/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java
similarity index 91%
rename from astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java
rename to astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java
index 6aaad841..bc1b946b 100644
--- a/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java
+++ b/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java
@@ -55,7 +55,7 @@
*/
@Slf4j
@Data
-public class CollectionRecordDefinition {
+public class CollectionBeanDefinition {
/** Class introspected. */
private final Class clazz;
@@ -89,14 +89,14 @@ public class CollectionRecordDefinition {
*
* @param clazz the class type
*/
- public CollectionRecordDefinition(Class clazz) {
+ public CollectionBeanDefinition(Class clazz) {
this.clazz = clazz;
this.fields = new HashMap<>();
// Collection Name
DataApiCollection collectionAnn = clazz.getAnnotation(DataApiCollection.class);
- if (collectionAnn != null && !collectionAnn.value().isEmpty()) {
- this.collectionName = collectionAnn.value();
+ if (collectionAnn != null && !collectionAnn.name().isEmpty()) {
+ this.collectionName = collectionAnn.name();
} else {
this.collectionName = clazz.getSimpleName().toLowerCase();
}
@@ -252,6 +252,48 @@ public String getIdFieldName() {
return idField != null ? idField.getName() : null;
}
+ /**
+ * Checks if the ID field can be written through a setter.
+ *
+ * @return true if an ID field exists and a setter is available
+ */
+ public boolean canSetId() {
+ return idField != null && idField.getSetter() != null;
+ }
+
+ /**
+ * Sets the ID value on the given instance.
+ *
+ * @param instance the instance to update
+ * @param value the new ID value
+ */
+ public void setId(T instance, Object value) {
+ if (instance == null) {
+ throw new IllegalArgumentException("Instance must not be null");
+ }
+
+ if (idField == null) {
+ throw new IllegalStateException(String.format(
+ "No field annotated with @DocumentId found in class '%s'",
+ clazz.getName()));
+ }
+
+ Method setter = idField.getSetter();
+ if (setter == null) {
+ throw new IllegalStateException(String.format(
+ "No setter method found for @DocumentId field '%s' in class '%s'",
+ idField.getName(), clazz.getName()));
+ }
+
+ try {
+ setter.invoke(instance, value);
+ } catch (Exception e) {
+ throw new IllegalStateException(String.format(
+ "Failed to set ID value on field '%s' in class '%s'",
+ idField.getName(), clazz.getName()), e);
+ }
+ }
+
/**
* Gets the vectorize value from the given instance.
*
@@ -449,9 +491,8 @@ public CollectionDefinition buildCollectionDefinition() {
}
// Lexical options
- if (!annotation.lexicalEnabled()) {
- definition.disableLexical();
- } else if (annotation.lexicalAnalyzer() != null) {
+ if (annotation.lexicalEnabled()) {
+ // Enable lexical search with the specified analyzer
definition.lexical(new Analyzer(annotation.lexicalAnalyzer()));
}
diff --git a/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityBeanDefinition.java b/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java
similarity index 91%
rename from astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityBeanDefinition.java
rename to astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java
index 94dc939b..81b6532b 100644
--- a/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityBeanDefinition.java
+++ b/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java
@@ -35,6 +35,8 @@
import com.datastax.astra.client.tables.mapping.EntityTable;
import com.datastax.astra.client.tables.mapping.PartitionBy;
import com.datastax.astra.client.tables.mapping.PartitionSort;
+import com.datastax.astra.client.tables.mapping.TablePrimaryKey;
+import com.datastax.astra.client.tables.mapping.TablePrimaryKeyClass;
import com.dtsx.astra.sdk.utils.Utils;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -69,7 +71,7 @@
*/
@Slf4j
@Data
-public class EntityBeanDefinition {
+public class EntityTableBeanDefinition {
/** Class introspected. */
final Class clazz;
@@ -92,7 +94,7 @@ public class EntityBeanDefinition {
* @param clazz
* class type
*/
- public EntityBeanDefinition(Class clazz) {
+ public EntityTableBeanDefinition(Class clazz) {
this.clazz = clazz;
this.fields = new HashMap<>();
// Table Name
@@ -143,6 +145,31 @@ public EntityBeanDefinition(Class clazz) {
AnnotatedField annfield = property.getField();
if (annfield != null) {
+ // Check if this field is annotated with @TablePrimaryKey
+ TablePrimaryKey tablePrimaryKey = annfield.getAnnotated()
+ .getAnnotation(TablePrimaryKey.class);
+
+ if (tablePrimaryKey != null) {
+ // This field contains a primary key class - expand its fields
+ Class> pkClass = field.getType();
+ if (!pkClass.isAnnotationPresent(TablePrimaryKeyClass.class)) {
+ throw new IllegalArgumentException(String.format(
+ "Field '%s' in class '%s' is annotated with @TablePrimaryKey but its type '%s' " +
+ "is not annotated with @TablePrimaryKeyClass",
+ field.getName(), clazz.getName(), pkClass.getName()));
+ }
+
+ // Introspect the primary key class and add its fields to this entity's fields
+ EntityTableBeanDefinition> pkBean = new EntityTableBeanDefinition<>(pkClass);
+ pkBean.getFields().forEach((pkFieldName, pkField) -> {
+ // Add the primary key field to the entity's field map
+ fields.put(pkFieldName, pkField);
+ });
+
+ // Skip adding the @TablePrimaryKey field itself
+ continue;
+ }
+
Column column = annfield.getAnnotated()
.getAnnotation(Column.class);
ColumnVector columnVector = annfield.getAnnotated()
@@ -297,7 +324,7 @@ public Map getPartitionSort() {
* a list of vector index definitions
*/
public static List listVectorIndexDefinitions(String tableName, Class> clazz) {
- EntityBeanDefinition> bean = new EntityBeanDefinition<>(clazz);
+ EntityTableBeanDefinition> bean = new EntityTableBeanDefinition<>(clazz);
if (Utils.hasLength(bean.getName()) && !bean.getName().equals(tableName)) {
throw new IllegalArgumentException("Table name mismatch, expected '" + tableName + "' but got '" + bean.getName() + "'");
}
@@ -328,7 +355,7 @@ public static List listVectorIndexDefinitions(String
* a document representing the table command
*/
public static Document createTypeCommand(Class> clazz) {
- EntityBeanDefinition> bean = new EntityBeanDefinition<>(clazz);
+ EntityTableBeanDefinition> bean = new EntityTableBeanDefinition<>(clazz);
Document doc = new Document();
doc.append("name", bean.getName());
Document definition = new Document();
@@ -362,7 +389,7 @@ public static Document createTypeCommand(Class> clazz) {
* a document representing the table command
*/
public static Document createTableCommand(String tableName, Class> clazz) {
- EntityBeanDefinition> bean = new EntityBeanDefinition<>(clazz);
+ EntityTableBeanDefinition> bean = new EntityTableBeanDefinition<>(clazz);
if (Utils.hasLength(bean.getName()) && !bean.getName().equals(tableName)) {
throw new IllegalArgumentException("Table name mismatch, expected '" + tableName + "' but got '" + bean.getName() + "'");
}
diff --git a/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java b/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java
index eff556db..56562ed3 100644
--- a/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java
+++ b/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java
@@ -21,7 +21,9 @@
*/
import com.datastax.astra.client.tables.definition.rows.Row;
-import com.datastax.astra.internal.reflection.EntityBeanDefinition;
+import com.datastax.astra.client.tables.mapping.TablePrimaryKey;
+import com.datastax.astra.client.tables.mapping.TablePrimaryKeyClass;
+import com.datastax.astra.internal.reflection.EntityTableBeanDefinition;
import com.datastax.astra.internal.reflection.EntityFieldDefinition;
import com.datastax.astra.internal.serdes.DataAPISerializer;
import com.fasterxml.jackson.core.JsonParser;
@@ -62,10 +64,27 @@ public static Row mapAsRow(T input) {
if (input == null || input instanceof Row) {
return (Row) input;
}
- EntityBeanDefinition> bean = new EntityBeanDefinition<>(input.getClass());
+ EntityTableBeanDefinition> bean = new EntityTableBeanDefinition<>(input.getClass());
Row row = new Row();
+
+ // Check if any field is annotated with @TablePrimaryKey
+ final Field primaryKeyFieldFinal;
+ Field tempPrimaryKeyField = null;
+ for (Field f : input.getClass().getDeclaredFields()) {
+ if (f.isAnnotationPresent(TablePrimaryKey.class)) {
+ tempPrimaryKeyField = f;
+ break;
+ }
+ }
+ primaryKeyFieldFinal = tempPrimaryKeyField;
+
bean.getFields().forEach((name, field) -> {
try {
+ // Skip the @TablePrimaryKey field itself - we'll flatten its contents
+ if (primaryKeyFieldFinal != null && name.equals(primaryKeyFieldFinal.getName())) {
+ return;
+ }
+
Object value = field.getGetter().invoke(input);
// Check if map and key is not String
@@ -79,10 +98,61 @@ public static Row mapAsRow(T input) {
} else {
row.put(field.getColumnName() != null ? field.getColumnName() : name, value);
}
- } catch (IllegalAccessException | InvocationTargetException e) {
- throw new RuntimeException(e);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+ // If we get IllegalArgumentException, this might be a field from a primary key class
+ // Try to get it from the primary key object instead
+ if (primaryKeyFieldFinal != null && e instanceof IllegalArgumentException) {
+ try {
+ primaryKeyFieldFinal.setAccessible(true);
+ Object pkValue = primaryKeyFieldFinal.get(input);
+ if (pkValue != null) {
+ Object nestedValue = field.getGetter().invoke(pkValue);
+ if (nestedValue instanceof Map, ?> map &&
+ field.getGenericKeyType() != null &&
+ !String.class.equals(field.getGenericKeyType())) {
+ List> pairs = new ArrayList<>();
+ map.forEach((k, v) -> pairs.add(List.of(k, v)));
+ row.put(field.getColumnName() != null ? field.getColumnName() : name, pairs);
+ } else {
+ row.put(field.getColumnName() != null ? field.getColumnName() : name, nestedValue);
+ }
+ }
+ } catch (Exception ex) {
+ throw new RuntimeException("Failed to extract field from primary key: " + name, ex);
+ }
+ } else {
+ throw new RuntimeException(e);
+ }
}
});
+
+ // If there's a @TablePrimaryKey field, flatten its contents into the row
+ if (primaryKeyFieldFinal != null) {
+ try {
+ primaryKeyFieldFinal.setAccessible(true);
+ Object primaryKeyValue = primaryKeyFieldFinal.get(input);
+ if (primaryKeyValue != null) {
+ Class> pkClass = primaryKeyValue.getClass();
+ if (pkClass.isAnnotationPresent(TablePrimaryKeyClass.class)) {
+ // Create a bean definition for the primary key class
+ EntityTableBeanDefinition> pkBean = new EntityTableBeanDefinition<>(pkClass);
+ pkBean.getFields().forEach((pkFieldName, pkField) -> {
+ try {
+ Object pkFieldValue = pkField.getGetter().invoke(primaryKeyValue);
+ String columnName = pkField.getColumnName() != null ?
+ pkField.getColumnName() : pkFieldName;
+ row.put(columnName, pkFieldValue);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Failed to extract primary key field: " + pkFieldName, e);
+ }
+ });
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Failed to access @TablePrimaryKey field", e);
+ }
+ }
+
return row;
}
@@ -109,10 +179,59 @@ public static T mapFromRow(Row row, DataAPISerializer serializer, Class i
return null;
}
- EntityBeanDefinition beanDef = new EntityBeanDefinition<>(inputRowClass);
+ EntityTableBeanDefinition beanDef = new EntityTableBeanDefinition<>(inputRowClass);
T input = inputRowClass.getDeclaredConstructor().newInstance();
+ // Check if any field is annotated with @TablePrimaryKey
+ Field primaryKeyField = null;
+ for (Field f : inputRowClass.getDeclaredFields()) {
+ if (f.isAnnotationPresent(TablePrimaryKey.class)) {
+ primaryKeyField = f;
+ break;
+ }
+ }
+
+ // If there's a @TablePrimaryKey field, reconstruct it from flattened columns
+ Object primaryKeyInstance = null;
+ EntityTableBeanDefinition> pkBeanDef = null;
+ if (primaryKeyField != null) {
+ Class> pkClass = primaryKeyField.getType();
+ if (pkClass.isAnnotationPresent(TablePrimaryKeyClass.class)) {
+ primaryKeyInstance = pkClass.getDeclaredConstructor().newInstance();
+ pkBeanDef = new EntityTableBeanDefinition<>(pkClass);
+
+ // Map flattened columns to primary key fields
+ for (EntityFieldDefinition pkFieldDef : pkBeanDef.getFields().values()) {
+ String columnName = pkFieldDef.getColumnName() != null ?
+ pkFieldDef.getColumnName() : pkFieldDef.getName();
+ Object columnValue = row.columnMap.get(columnName);
+
+ if (columnValue != null) {
+ JavaType javaType = pkFieldDef.getJavaType();
+ Object value = serializer.getMapper().convertValue(columnValue, javaType);
+
+ if (pkFieldDef.getSetter() != null) {
+ pkFieldDef.getSetter().invoke(primaryKeyInstance, value);
+ } else {
+ Field field = pkClass.getDeclaredField(pkFieldDef.getName());
+ field.setAccessible(true);
+ field.set(primaryKeyInstance, value);
+ }
+ }
+ }
+
+ // Set the reconstructed primary key to the bean
+ primaryKeyField.setAccessible(true);
+ primaryKeyField.set(input, primaryKeyInstance);
+ }
+ }
+
for (EntityFieldDefinition fieldDef : beanDef.getFields().values()) {
+ // Skip fields that belong to the primary key class - they've already been handled
+ if (pkBeanDef != null && pkBeanDef.getFields().containsKey(fieldDef.getName())) {
+ continue;
+ }
+
String columnName = fieldDef.getColumnName() != null ? fieldDef.getColumnName() : fieldDef.getName();
Object columnValue = row.columnMap.get(columnName);
if (columnValue == null) {
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java b/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java
index 05ebd423..c48c36a6 100644
--- a/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java
@@ -46,6 +46,7 @@
import com.datastax.astra.client.exceptions.DataAPIResponseException;
import com.datastax.astra.internal.api.DataAPIResponse;
import com.datastax.astra.internal.utils.EscapeUtils;
+import com.datastax.astra.test.integration.model.Book;
import com.datastax.astra.test.integration.model.ProductString;
import com.datastax.astra.test.integration.utils.TestDataset;
import lombok.extern.slf4j.Slf4j;
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java b/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java
index 38ea326b..0ec9a8b3 100644
--- a/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java
@@ -39,6 +39,8 @@
import com.datastax.astra.client.tables.definition.columns.TableColumnTypes;
import com.datastax.astra.client.tables.definition.indexes.*;
import com.datastax.astra.client.tables.definition.rows.Row;
+import com.datastax.astra.test.integration.model.OrderBean;
+import com.datastax.astra.test.integration.model.OrderKey;
import com.datastax.astra.test.integration.model.TableCompositeAnnotatedRow;
import com.datastax.astra.test.integration.model.TableCompositeRow;
import com.datastax.astra.test.integration.model.TableCompositeRowGenerator;
@@ -700,4 +702,77 @@ public void should_findOne_with_null_collections() {
// Cleanup
getDatabase().dropTable(tableName, IF_EXISTS);
}
+
+ @Test
+ @Order(50)
+ @DisplayName("50. Should support @TablePrimaryKeyClass pattern for insertOne")
+ public void should_support_tablePrimaryKeyClass_insertOne() {
+ // Given: Create table using @TablePrimaryKeyClass pattern
+ String tableName = "orders_pk_class";
+ getDatabase().createTable(OrderBean.class);
+ Table table = getDatabase().getTable(tableName, OrderBean.class);
+
+ // When: Insert an order using the @TablePrimaryKeyClass pattern
+ OrderKey key = new OrderKey("CUST123", LocalDate.of(2024, 1, 15));
+ OrderBean orderBean = new OrderBean(key, "ORD001", new BigDecimal("99.99"), "PENDING");
+ table.insertOne(orderBean);
+
+ // Then: Verify the order was inserted and can be retrieved
+ Optional found = table.findOne(
+ Filters.and(
+ Filters.eq("customer_id", "CUST123"),
+ Filters.eq("order_date", LocalDate.of(2024, 1, 15))
+ )
+ );
+
+ assertThat(found).isPresent();
+ OrderBean retrievedOrderBean = found.get();
+ assertThat(retrievedOrderBean.getKey().getCustomerId()).isEqualTo("CUST123");
+ assertThat(retrievedOrderBean.getKey().getOrderDate()).isEqualTo(LocalDate.of(2024, 1, 15));
+ assertThat(retrievedOrderBean.getOrderId()).isEqualTo("ORD001");
+ assertThat(retrievedOrderBean.getAmount()).isEqualByComparingTo(new BigDecimal("99.99"));
+ assertThat(retrievedOrderBean.getStatus()).isEqualTo("PENDING");
+
+ log.info("✓ @TablePrimaryKeyClass pattern: insertOne successful");
+
+ // Cleanup
+ getDatabase().dropTable(tableName, IF_EXISTS);
+ }
+
+ @Test
+ @Order(51)
+ @DisplayName("51. Should support @TablePrimaryKeyClass pattern for TableDefinition creation")
+ public void should_support_tablePrimaryKeyClass_tableDefinition() {
+ // Given: An entity class using @TablePrimaryKeyClass pattern
+ String tableName = "orders_pk_class";
+
+ // When: Create table from the annotated class
+ getDatabase().createTable(OrderBean.class);
+
+ // Then: Verify the table was created with correct structure
+ assertThat(getDatabase().tableExists(tableName)).isTrue();
+
+ Table table = getDatabase().getTable(tableName, OrderBean.class);
+ TableDefinition definition = table.getDefinition();
+
+ // Verify partition keys
+ assertThat(definition.getPrimaryKey().getPartitionBy()).containsExactly("customer_id");
+
+ // Verify clustering columns
+ assertThat(definition.getPrimaryKey().getPartitionSort()).containsKey("order_date");
+
+ // Verify all columns are present (flattened from primary key class + entity fields)
+ assertThat(definition.getColumns()).containsKeys(
+ "customer_id", // from OrderKey
+ "order_date", // from OrderKey
+ "order_id", // from Order
+ "amount", // from Order
+ "status" // from Order
+ );
+
+ log.info("✓ @TablePrimaryKeyClass pattern: TableDefinition created correctly");
+
+ // Cleanup
+ getDatabase().dropTable(tableName, IF_EXISTS);
+ }
}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/integration/astra/Astra_Collections_01_CrudIT.java b/astra-db-java/src/test/java/com/datastax/astra/test/integration/astra/Astra_Collections_01_CrudIT.java
index e3084019..cf24317b 100644
--- a/astra-db-java/src/test/java/com/datastax/astra/test/integration/astra/Astra_Collections_01_CrudIT.java
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/integration/astra/Astra_Collections_01_CrudIT.java
@@ -22,14 +22,20 @@
import com.datastax.astra.client.collections.Collection;
import com.datastax.astra.client.collections.commands.results.CollectionInsertOneResult;
+import com.datastax.astra.client.collections.definition.CollectionDefinition;
import com.datastax.astra.client.collections.definition.documents.Document;
import com.datastax.astra.test.integration.AbstractCollectionIT;
+import com.datastax.astra.test.integration.model.Book;
import com.datastax.astra.test.integration.utils.EnabledIfAstra;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
+import java.util.Optional;
+
import static com.datastax.astra.test.integration.utils.TestDataset.*;
+import static org.assertj.core.api.Assertions.assertThat;
/**
* Collection integration tests against Astra DB.
@@ -69,4 +75,5 @@ void should_sort_vector_on_replaceOne() {
+
}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/integration/astra/Astra_Collections_04_FindAndRerankIT.java b/astra-db-java/src/test/java/com/datastax/astra/test/integration/astra/Astra_Collections_04_FindAndRerankIT.java
index 416db5be..b4f63b47 100644
--- a/astra-db-java/src/test/java/com/datastax/astra/test/integration/astra/Astra_Collections_04_FindAndRerankIT.java
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/integration/astra/Astra_Collections_04_FindAndRerankIT.java
@@ -20,9 +20,19 @@
* #L%
*/
+import com.datastax.astra.client.collections.Collection;
+import com.datastax.astra.client.collections.commands.results.CollectionInsertOneResult;
+import com.datastax.astra.client.collections.definition.CollectionDefinition;
import com.datastax.astra.test.integration.AbstractCollectionFindAndRerankIT;
+import com.datastax.astra.test.integration.model.Book;
import com.datastax.astra.test.integration.utils.EnabledIfAstra;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
/**
* FindAndRerank integration tests against Astra DB.
@@ -39,4 +49,75 @@ protected String getRerankingApiKey() {
// Nvidia reranking uses the Astra token
return getConfig().getAstraToken();
}
+
+
+ // ========== Annotation-based Collection Operations ==========
+
+ @Test
+ @Order(83)
+ void should_createCollection_fromAnnotatedClass() {
+ // Create collection from annotated class
+ Collection bookCollection =
+ getDatabase().createCollection(Book.class);
+
+ assertThat(bookCollection).isNotNull();
+ assertThat(bookCollection.getCollectionName()).isEqualTo("c_book_auto");
+
+ // Clean up
+ //getDatabase().dropCollection(Book.class);
+ }
+
+ @Test
+ @Order(84)
+ void should_getCollection_fromAnnotatedClass() {
+ // Create collection first
+ getDatabase().createCollection(Book.class);
+
+ // Get collection using annotated class
+ Collection bookCollection =
+ getDatabase().getCollection(Book.class);
+
+ assertThat(bookCollection).isNotNull();
+ assertThat(bookCollection.getCollectionName()).isEqualTo("c_book_auto");
+
+ // Insert a book
+ Book book = new Book()
+ .id("book1")
+ .title("The Java Programming Language")
+ .author("James Gosling")
+ .numberOfPages(500)
+ .isCheckedOut(false)
+ .vectorize("A comprehensive guide to Java programming")
+ .lexical("java programming language guide");
+
+ CollectionInsertOneResult result = bookCollection.insertOne(book);
+ assertThat(result).isNotNull();
+ assertThat(result.getInsertedId()).isEqualTo("book1");
+
+ // Find the book
+ Optional foundBook = bookCollection.findById("book1");
+ assertThat(foundBook).isPresent();
+ assertThat(foundBook.get().getTitle()).isEqualTo("The Java Programming Language");
+ assertThat(foundBook.get().getAuthor()).isEqualTo("James Gosling");
+
+ // Clean up
+ getDatabase().dropCollection(com.datastax.astra.test.integration.model.Book.class);
+ }
+
+ @Test
+ @Order(85)
+ void should_getCollectionName_fromAnnotatedClass() {
+ String collectionName = getDatabase().getCollectionName(com.datastax.astra.test.integration.model.Book.class);
+ assertThat(collectionName).isEqualTo("c_book_auto");
+ }
+
+ @Test
+ @Order(86)
+ void should_getCollectionDefinition_fromAnnotatedClass() {
+ CollectionDefinition definition = getDatabase().getCollectionDefinition(com.datastax.astra.test.integration.model.Book.class);
+ assertThat(definition).isNotNull();
+ // Verify the definition was created from the annotation
+ assertThat(definition.getVector()).isNotNull();
+ }
+
}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Book.java b/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Book.java
new file mode 100644
index 00000000..7587fbef
--- /dev/null
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Book.java
@@ -0,0 +1,147 @@
+package com.datastax.astra.test.integration.model;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import com.datastax.astra.client.collections.mapping.DataApiCollection;
+import com.datastax.astra.client.collections.mapping.DocumentId;
+import com.datastax.astra.client.collections.mapping.Lexical;
+import com.datastax.astra.client.collections.mapping.Vectorize;
+import com.datastax.astra.client.core.vector.SimilarityMetric;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+import java.util.Set;
+
+import static com.datastax.astra.client.core.lexical.AnalyzerTypes.STANDARD;
+
+/**
+ * Test model class representing a Book document with advanced features:
+ * - Vector search with vectorization
+ * - Lexical search
+ * - Reranking capabilities
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@DataApiCollection(
+ name = "c_book_auto",
+ defaultIdType = "",
+ // Vector
+ vectorDimension = 1024,
+ vectorSimilarity = SimilarityMetric.COSINE,
+ // Vectorize
+ vectorizeModel = "NV-Embed-QA",
+ vectorizeProvider = "nvidia",
+ // Lexical
+ lexicalEnabled = true,
+ lexicalAnalyzer = STANDARD,
+ // Rerank
+ rerankEnabled = true,
+ rerankProvider = "nvidia",
+ rerankModel = "nvidia/llama-3.2-nv-rerankqa-1b-v2"
+)
+public class Book {
+
+ @DocumentId
+ String id;
+
+ String title;
+
+ String author;
+
+ boolean is_checked_out;
+
+ @Vectorize
+ String vectorize;
+
+ @Lexical
+ String lexical;
+
+ @JsonProperty("number_of_pages")
+ Integer numberOfPages;
+
+ String genre;
+
+ String description;
+
+ Set genres;
+
+ Map metadata;
+
+ // Fluent interface methods
+ public Book id(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public Book title(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public Book author(String author) {
+ this.author = author;
+ return this;
+ }
+
+ public Book isCheckedOut(boolean isCheckedOut) {
+ this.is_checked_out = isCheckedOut;
+ return this;
+ }
+
+ public Book vectorize(String vectorize) {
+ this.vectorize = vectorize;
+ return this;
+ }
+
+ public Book lexical(String lexical) {
+ this.lexical = lexical;
+ return this;
+ }
+
+ public Book numberOfPages(Integer numberOfPages) {
+ this.numberOfPages = numberOfPages;
+ return this;
+ }
+
+ public Book genre(String genre) {
+ this.genre = genre;
+ return this;
+ }
+
+ public Book description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Book genres(Set genres) {
+ this.genres = genres;
+ return this;
+ }
+
+ public Book metadata(Map metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderBean.java b/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderBean.java
new file mode 100644
index 00000000..691de582
--- /dev/null
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderBean.java
@@ -0,0 +1,93 @@
+package com.datastax.astra.test.integration.model;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import com.datastax.astra.client.tables.mapping.Column;
+import com.datastax.astra.client.tables.mapping.EntityTable;
+import com.datastax.astra.client.tables.mapping.TablePrimaryKey;
+
+import java.math.BigDecimal;
+
+/**
+ * Test model for @TablePrimaryKeyClass pattern.
+ * Entity with a composite primary key using @TablePrimaryKey annotation.
+ */
+@EntityTable("orders_pk_class")
+public class OrderBean {
+
+ @TablePrimaryKey
+ private OrderKey key;
+
+ @Column(name = "order_id")
+ private String orderId;
+
+ @Column(name = "amount")
+ private BigDecimal amount;
+
+ @Column(name = "status")
+ private String status;
+
+ public OrderBean() {}
+
+ public OrderBean(OrderKey key, String orderId, BigDecimal amount, String status) {
+ this.key = key;
+ this.orderId = orderId;
+ this.amount = amount;
+ this.status = status;
+ }
+
+ public OrderKey getKey() {
+ return key;
+ }
+
+ public void setKey(OrderKey key) {
+ this.key = key;
+ }
+
+ public String getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(String orderId) {
+ this.orderId = orderId;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ @Override
+ public String toString() {
+ return "Order{key=" + key + ", orderId='" + orderId + "', amount=" + amount + ", status='" + status + "'}";
+ }
+}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java b/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java
new file mode 100644
index 00000000..9c28df9c
--- /dev/null
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java
@@ -0,0 +1,88 @@
+package com.datastax.astra.test.integration.model;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import com.datastax.astra.client.core.query.SortOrder;
+import com.datastax.astra.client.tables.mapping.Column;
+import com.datastax.astra.client.tables.mapping.PartitionBy;
+import com.datastax.astra.client.tables.mapping.PartitionSort;
+import com.datastax.astra.client.tables.mapping.TablePrimaryKeyClass;
+
+import java.time.LocalDate;
+import java.util.Objects;
+
+/**
+ * Test model for @TablePrimaryKeyClass pattern.
+ * Represents a composite primary key with partition key and clustering column.
+ */
+@TablePrimaryKeyClass
+public class OrderKey {
+
+ @PartitionBy(1)
+ @Column(name = "customer_id")
+ private String customerId;
+
+ @PartitionSort(position = 1, order = SortOrder.ASCENDING)
+ @Column(name = "order_date")
+ private LocalDate orderDate;
+
+ public OrderKey() {}
+
+ public OrderKey(String customerId, LocalDate orderDate) {
+ this.customerId = customerId;
+ this.orderDate = orderDate;
+ }
+
+ public String getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(String customerId) {
+ this.customerId = customerId;
+ }
+
+ public LocalDate getOrderDate() {
+ return orderDate;
+ }
+
+ public void setOrderDate(LocalDate orderDate) {
+ this.orderDate = orderDate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ OrderKey orderKey = (OrderKey) o;
+ return Objects.equals(customerId, orderKey.customerId) &&
+ Objects.equals(orderDate, orderKey.orderDate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(customerId, orderDate);
+ }
+
+ @Override
+ public String toString() {
+ return "OrderKey{customerId='" + customerId + "', orderDate=" + orderDate + "}";
+ }
+}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassDemo.java b/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassDemo.java
new file mode 100644
index 00000000..29bc2fd3
--- /dev/null
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassDemo.java
@@ -0,0 +1,80 @@
+package com.datastax.astra.test.unit;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import com.datastax.astra.client.tables.definition.rows.Row;
+import com.datastax.astra.internal.reflection.EntityTableBeanDefinition;
+import com.datastax.astra.internal.serdes.tables.RowMapper;
+import com.datastax.astra.test.integration.model.OrderBean;
+import com.datastax.astra.test.integration.model.OrderKey;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * Standalone demo to verify @TablePrimaryKeyClass pattern implementation.
+ * Run with: java -cp ... com.datastax.astra.test.unit.TablePrimaryKeyClassDemo
+ */
+public class TablePrimaryKeyClassDemo {
+
+ public static void main(String[] args) {
+ System.out.println("=== @TablePrimaryKeyClass Pattern Demo ===\n");
+
+ // Test 1: Row Serialization (flattening)
+ System.out.println("Test 1: Row Serialization");
+ OrderKey key = new OrderKey("CUST123", LocalDate.of(2024, 1, 15));
+ OrderBean orderBean = new OrderBean(key, "ORD001", new BigDecimal("99.99"), "PENDING");
+
+ Row row = RowMapper.mapAsRow(orderBean);
+
+ System.out.println(" ✓ Primary key fields flattened:");
+ System.out.println(" - customer_id: " + row.get("customer_id", String.class));
+ System.out.println(" - order_date: " + row.get("order_date", LocalDate.class));
+ System.out.println(" ✓ Entity fields present:");
+ System.out.println(" - order_id: " + row.get("order_id", String.class));
+ System.out.println(" - amount: " + row.get("amount", BigDecimal.class));
+ System.out.println(" - status: " + row.get("status", String.class));
+ System.out.println(" ✓ @TablePrimaryKey field NOT in row: " + !row.getColumnMap().containsKey("key"));
+
+ // Test 2: Bean Definition (field expansion)
+ System.out.println("\nTest 2: Bean Definition");
+ EntityTableBeanDefinition beanDef = new EntityTableBeanDefinition<>(OrderBean.class);
+
+ System.out.println(" ✓ Fields expanded from OrderKey:");
+ System.out.println(" - customer_id present: " + beanDef.getFields().containsKey("customer_id"));
+ System.out.println(" - order_date present: " + beanDef.getFields().containsKey("order_date"));
+ System.out.println(" ✓ Entity fields present:");
+ System.out.println(" - order_id present: " + beanDef.getFields().containsKey("order_id"));
+ System.out.println(" - amount present: " + beanDef.getFields().containsKey("amount"));
+ System.out.println(" - status present: " + beanDef.getFields().containsKey("status"));
+ System.out.println(" ✓ @TablePrimaryKey field NOT in fields: " + !beanDef.getFields().containsKey("key"));
+
+ // Test 3: Partition Keys
+ System.out.println("\nTest 3: Partition Keys");
+ System.out.println(" ✓ Partition keys: " + beanDef.getPartitionBy());
+
+ // Test 4: Clustering Columns
+ System.out.println("\nTest 4: Clustering Columns");
+ System.out.println(" ✓ Clustering columns: " + beanDef.getPartitionSort());
+
+ System.out.println("\n=== All Tests Passed! ===");
+ }
+}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java b/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java
new file mode 100644
index 00000000..066274e0
--- /dev/null
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java
@@ -0,0 +1,112 @@
+package com.datastax.astra.test.unit;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import com.datastax.astra.client.tables.definition.rows.Row;
+import com.datastax.astra.internal.reflection.EntityTableBeanDefinition;
+import com.datastax.astra.internal.serdes.tables.RowMapper;
+import com.datastax.astra.test.integration.model.OrderBean;
+import com.datastax.astra.test.integration.model.OrderKey;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for @TablePrimaryKeyClass pattern support.
+ * Tests serialization and TableDefinition creation without requiring database connection.
+ */
+public class TablePrimaryKeyClassTest {
+
+ @Test
+ public void should_flatten_primaryKey_fields_in_row_serialization() {
+ // Given: An Order entity with @TablePrimaryKey
+ OrderKey key = new OrderKey("CUST123", LocalDate.of(2024, 1, 15));
+ OrderBean orderBean = new OrderBean(key, "ORD001", new BigDecimal("99.99"), "PENDING");
+
+ // When: Serialize to Row
+ Row row = RowMapper.mapAsRow(orderBean);
+
+ // Then: Primary key fields should be flattened at the same level as other columns
+ assertThat(row.getColumnMap()).containsKeys(
+ "customer_id", // from OrderKey (flattened)
+ "order_date", // from OrderKey (flattened)
+ "order_id", // from Order
+ "amount", // from Order
+ "status" // from Order
+ );
+
+ // Verify values
+ assertThat(row.get("customer_id", String.class)).isEqualTo("CUST123");
+ assertThat(row.get("order_date", LocalDate.class)).isEqualTo(LocalDate.of(2024, 1, 15));
+ assertThat(row.get("order_id", String.class)).isEqualTo("ORD001");
+ assertThat(row.get("amount", BigDecimal.class)).isEqualByComparingTo(new BigDecimal("99.99"));
+ assertThat(row.get("status", String.class)).isEqualTo("PENDING");
+
+ // The @TablePrimaryKey field itself should NOT be in the row
+ assertThat(row.getColumnMap()).doesNotContainKey("key");
+ }
+
+ @Test
+ public void should_expand_primaryKey_class_fields_in_bean_definition() {
+ // Given: Order class with @TablePrimaryKeyClass pattern
+ EntityTableBeanDefinition beanDef = new EntityTableBeanDefinition<>(OrderBean.class);
+
+ // Then: Fields from OrderKey should be expanded into the entity's field map
+ assertThat(beanDef.getFields()).containsKeys(
+ "customerId", // from OrderKey (expanded)
+ "orderDate", // from OrderKey (expanded)
+ "orderId", // from Order
+ "amount", // from Order
+ "status" // from Order
+ );
+
+ // The @TablePrimaryKey field itself should NOT be in the fields map
+ assertThat(beanDef.getFields()).doesNotContainKey("key");
+ }
+
+ @Test
+ public void should_extract_partition_keys_from_primaryKey_class() {
+ // Given: Order class with @TablePrimaryKeyClass pattern
+ EntityTableBeanDefinition beanDef = new EntityTableBeanDefinition<>(OrderBean.class);
+
+ // When: Get partition keys
+ var partitionKeys = beanDef.getPartitionBy();
+
+ // Then: Should contain the partition key from OrderKey
+ assertThat(partitionKeys).containsExactly("customer_id");
+ }
+
+ @Test
+ public void should_extract_clustering_columns_from_primaryKey_class() {
+ // Given: Order class with @TablePrimaryKeyClass pattern
+ EntityTableBeanDefinition beanDef = new EntityTableBeanDefinition<>(OrderBean.class);
+
+ // When: Get clustering columns
+ var clusteringColumns = beanDef.getPartitionSort();
+
+ // Then: Should contain the clustering column from OrderKey
+ assertThat(clusteringColumns).containsKey("order_date");
+ assertThat(clusteringColumns.get("order_date")).isEqualTo(1); // ASC order
+ }
+}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyDeserializationTest.java b/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyDeserializationTest.java
new file mode 100644
index 00000000..b74e2769
--- /dev/null
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyDeserializationTest.java
@@ -0,0 +1,86 @@
+package com.datastax.astra.test.unit;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import com.datastax.astra.client.tables.definition.rows.Row;
+import com.datastax.astra.internal.serdes.tables.RowMapper;
+import com.datastax.astra.internal.serdes.tables.RowSerializer;
+import com.datastax.astra.test.integration.model.OrderBean;
+import com.datastax.astra.test.integration.model.OrderKey;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for deserializing flattened rows back to beans with @TablePrimaryKey.
+ */
+public class TablePrimaryKeyDeserializationTest {
+
+ @Test
+ public void should_deserialize_flattened_row_to_bean_with_primaryKey() {
+ // Given: A flattened row (as returned by the server)
+ Row row = new Row();
+ row.put("customer_id", "CUST123");
+ row.put("order_date", LocalDate.of(2024, 1, 15));
+ row.put("order_id", "ORD001");
+ row.put("amount", new BigDecimal("99.99"));
+ row.put("status", "PENDING");
+
+ // When: Deserialize to OrderBean
+ RowSerializer serializer = new RowSerializer();
+ OrderBean bean = RowMapper.mapFromRow(row, serializer, OrderBean.class);
+
+ // Then: The bean should be properly reconstructed with the primary key
+ assertThat(bean).isNotNull();
+ assertThat(bean.getKey()).isNotNull();
+ assertThat(bean.getKey().getCustomerId()).isEqualTo("CUST123");
+ assertThat(bean.getKey().getOrderDate()).isEqualTo(LocalDate.of(2024, 1, 15));
+ assertThat(bean.getOrderId()).isEqualTo("ORD001");
+ assertThat(bean.getAmount()).isEqualByComparingTo(new BigDecimal("99.99"));
+ assertThat(bean.getStatus()).isEqualTo("PENDING");
+ }
+
+ @Test
+ public void should_roundtrip_serialize_and_deserialize() {
+ // Given: An OrderBean with composite primary key
+ OrderKey key = new OrderKey("CUST456", LocalDate.of(2024, 2, 20));
+ OrderBean original = new OrderBean(key, "ORD002", new BigDecimal("150.50"), "SHIPPED");
+
+ // When: Serialize to Row
+ Row row = RowMapper.mapAsRow(original);
+
+ // And: Deserialize back to OrderBean
+ RowSerializer serializer = new RowSerializer();
+ OrderBean deserialized = RowMapper.mapFromRow(row, serializer, OrderBean.class);
+
+ // Then: The deserialized bean should match the original
+ assertThat(deserialized).isNotNull();
+ assertThat(deserialized.getKey()).isNotNull();
+ assertThat(deserialized.getKey().getCustomerId()).isEqualTo(original.getKey().getCustomerId());
+ assertThat(deserialized.getKey().getOrderDate()).isEqualTo(original.getKey().getOrderDate());
+ assertThat(deserialized.getOrderId()).isEqualTo(original.getOrderId());
+ assertThat(deserialized.getAmount()).isEqualByComparingTo(original.getAmount());
+ assertThat(deserialized.getStatus()).isEqualTo(original.getStatus());
+ }
+}
diff --git a/astra-db-java/src/test/java/com/datastax/astra/test/unit/tables/TableDefinitionEqualsTest.java b/astra-db-java/src/test/java/com/datastax/astra/test/unit/tables/TableDefinitionEqualsTest.java
new file mode 100644
index 00000000..1b90f8db
--- /dev/null
+++ b/astra-db-java/src/test/java/com/datastax/astra/test/unit/tables/TableDefinitionEqualsTest.java
@@ -0,0 +1,169 @@
+package com.datastax.astra.test.unit.tables;
+
+/*-
+ * #%L
+ * Data API Java Client
+ * --
+ * Copyright (C) 2024 DataStax
+ * --
+ * Licensed under the Apache License, Version 2.0
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ * #L%
+ */
+
+import com.datastax.astra.client.core.query.Sort;
+import com.datastax.astra.client.core.query.SortOrder;
+import com.datastax.astra.client.tables.definition.TableDefinition;
+import com.datastax.astra.client.tables.definition.columns.TableColumnTypes;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for TableDefinition equals and hashCode methods.
+ */
+class TableDefinitionEqualsTest {
+
+ @Test
+ void should_be_equal_when_same_structure() {
+ // Given: Two identical table definitions
+ TableDefinition def1 = new TableDefinition()
+ .addColumnText("id")
+ .addColumnText("name")
+ .addColumnInt("age")
+ .partitionKey("id");
+
+ TableDefinition def2 = new TableDefinition()
+ .addColumnText("id")
+ .addColumnText("name")
+ .addColumnInt("age")
+ .partitionKey("id");
+
+ // Then: They should be equal
+ assertThat(def1).isEqualTo(def2);
+ assertThat(def1.hashCode()).isEqualTo(def2.hashCode());
+ }
+
+ @Test
+ void should_not_be_equal_when_different_columns() {
+ // Given: Two table definitions with different columns
+ TableDefinition def1 = new TableDefinition()
+ .addColumnText("id")
+ .addColumnText("name")
+ .partitionKey("id");
+
+ TableDefinition def2 = new TableDefinition()
+ .addColumnText("id")
+ .addColumnInt("age")
+ .partitionKey("id");
+
+ // Then: They should not be equal
+ assertThat(def1).isNotEqualTo(def2);
+ }
+
+ @Test
+ void should_not_be_equal_when_different_column_types() {
+ // Given: Two table definitions with same column names but different types
+ TableDefinition def1 = new TableDefinition()
+ .addColumnText("id")
+ .addColumnText("value")
+ .partitionKey("id");
+
+ TableDefinition def2 = new TableDefinition()
+ .addColumnText("id")
+ .addColumnInt("value")
+ .partitionKey("id");
+
+ // Then: They should not be equal
+ assertThat(def1).isNotEqualTo(def2);
+ }
+
+ @Test
+ void should_not_be_equal_when_different_primary_key() {
+ // Given: Two table definitions with different primary keys
+ TableDefinition def1 = new TableDefinition()
+ .addColumnText("id")
+ .addColumnText("name")
+ .partitionKey("id");
+
+ TableDefinition def2 = new TableDefinition()
+ .addColumnText("id")
+ .addColumnText("name")
+ .partitionKey("name");
+
+ // Then: They should not be equal
+ assertThat(def1).isNotEqualTo(def2);
+ }
+
+ @Test
+ void should_be_equal_with_clustering_columns() {
+ // Given: Two table definitions with clustering columns
+ TableDefinition def1 = new TableDefinition()
+ .addColumnText("customer_id")
+ .addColumnTimestamp("order_date")
+ .addColumnInt("amount")
+ .partitionKey("customer_id")
+ .clusteringColumns(Sort.ascending("order_date"));
+
+ TableDefinition def2 = new TableDefinition()
+ .addColumnText("customer_id")
+ .addColumnTimestamp("order_date")
+ .addColumnInt("amount")
+ .partitionKey("customer_id")
+ .clusteringColumns(Sort.ascending("order_date"));
+
+ // Then: They should be equal
+ assertThat(def1).isEqualTo(def2);
+ assertThat(def1.hashCode()).isEqualTo(def2.hashCode());
+ }
+
+ @Test
+ void should_not_be_equal_when_different_clustering_order() {
+ // Given: Two table definitions with different clustering order
+ TableDefinition def1 = new TableDefinition()
+ .addColumnText("customer_id")
+ .addColumnTimestamp("order_date")
+ .partitionKey("customer_id")
+ .clusteringColumns(Sort.ascending("order_date"));
+
+ TableDefinition def2 = new TableDefinition()
+ .addColumnText("customer_id")
+ .addColumnTimestamp("order_date")
+ .partitionKey("customer_id")
+ .clusteringColumns(Sort.descending("order_date"));
+
+ // Then: They should not be equal
+ assertThat(def1).isNotEqualTo(def2);
+ }
+
+ @Test
+ void should_handle_null_comparison() {
+ // Given: A table definition
+ TableDefinition def = new TableDefinition()
+ .addColumnText("id")
+ .partitionKey("id");
+
+ // Then: Should not equal null
+ assertThat(def).isNotEqualTo(null);
+ }
+
+ @Test
+ void should_be_equal_to_itself() {
+ // Given: A table definition
+ TableDefinition def = new TableDefinition()
+ .addColumnText("id")
+ .partitionKey("id");
+
+ // Then: Should equal itself
+ assertThat(def).isEqualTo(def);
+ }
+}
diff --git a/astra-db-java/src/test/resources/junit-platform.properties b/astra-db-java/src/test/resources/junit-platform.properties
index 177e9426..2642cd10 100644
--- a/astra-db-java/src/test/resources/junit-platform.properties
+++ b/astra-db-java/src/test/resources/junit-platform.properties
@@ -4,8 +4,8 @@
# Order test classes alphabetically by class name.
# This allows using numbered prefixes (e.g., Astra01_, Astra02_) to control
-# execution order when tests have dependencies on each other.
-junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$ClassName
+# execution orderBean when tests have dependencies on each other.
+junit.jupiter.testclass.orderBean.default=org.junit.jupiter.api.ClassOrderer$ClassName
# Parallel execution is disabled by default to ensure ordered execution
junit.jupiter.execution.parallel.enabled=false
diff --git a/astra-db-java/src/test/resources/philosopher-quotes.csv b/astra-db-java/src/test/resources/philosopher-quotes.csv
index 2260ef91..00de4e2d 100644
--- a/astra-db-java/src/test/resources/philosopher-quotes.csv
+++ b/astra-db-java/src/test/resources/philosopher-quotes.csv
@@ -31,7 +31,7 @@ aristotle,"A friend is another I.",
aristotle,"He who hath many friends hath none.",ethics
aristotle,"The hand is the tool of tools.",
aristotle,"Good moral character is not something that we can achieve on our own. We need a culture that supports the conditions under which self-love and friendship flourish.",ethics
-aristotle,"We give up leisure in order that we may have leisure, just as we go to war in order that we may have peace.",ethics
+aristotle,"We give up leisure in orderBean that we may have leisure, just as we go to war in orderBean that we may have peace.",ethics
aristotle,"We must be neither cowardly nor rash but courageous.",ethics;knowledge
aristotle,"The true nature of anything is what it becomes at its highest.",knowledge
aristotle,"To give away money is an easy matter and in any man's power. But to decide to whom to give it and how large and when, and for what purpose and how, is neither in every man's power nor an easy matter.",knowledge;ethics;politics
@@ -60,7 +60,7 @@ schopenhauer,"Pleasure is never as pleasant as we expected it to be and pain is
schopenhauer,"at the death of every friendly soul",history
schopenhauer,"arises from the feeling that there is",
schopenhauer,"To be alone is the fate of all great mindsa fate deplored at times, but still always chosen as the less grievous of two evils.",knowledge;ethics
-schopenhauer,"However, for the man who studies to gain insight, books and studies are merely rungs of the ladder on which he climbs to the summit of knowledge. As soon as a rung has raised him up one step, he leaves it behind. On the other hand, the many who study in order to fill their memory do not use the rungs of the ladder for climbing, but take them off and load themselves with them to take away, rejoicing at the increasing weight of the burden. They remain below forever, because they bear what should have bourne them.",knowledge;education
+schopenhauer,"However, for the man who studies to gain insight, books and studies are merely rungs of the ladder on which he climbs to the summit of knowledge. As soon as a rung has raised him up one step, he leaves it behind. On the other hand, the many who study in orderBean to fill their memory do not use the rungs of the ladder for climbing, but take them off and load themselves with them to take away, rejoicing at the increasing weight of the burden. They remain below forever, because they bear what should have bourne them.",knowledge;education
schopenhauer,"There is not a grain of dust, not an atom that can become nothing, yet man believes that death is the annhilation of his being.",ethics;religion
schopenhauer,"Human life, like all inferior goods, is covered on the outside with a false glitter; what suffers always conceals itself.",ethics
schopenhauer,"Just as one spoils the stomach by overfeeding and thereby impairs the whole body, so can one overload and choke the mind by giving it too much nourishment. For the more one reads the fewer are the traces left of what one has read; the mind is like a tablet that has been written over and over. Hence it is impossible to reflect; and it is only by reflection that one can assimilate what one has read. If one reads straight ahead without pondering over it later, what has been read does not take root, but is for the most part lost.",knowledge;ethics
@@ -92,7 +92,7 @@ schopenhauer,"He who can see truly in the midst of general infatuation is like a
schopenhauer,"If a person is stupid, we excuse him by saying that he cannot help it; but if we attempted to excuse in precisely the same way the person who is bad, we should be laughed at.",ethics
schopenhauer,"My body and my will are one.",
schopenhauer,"The ordinary method of education is to imprint ideas and opinions, in the strict sense of the word, prejudices, on the mind of the child, before it has had any but a very few particular observations. It is thus that he afterwards comes to view the world and gather experience through the medium of those ready-made ideas, rather than to let his ideas be formed for him out of his own experience of life, as they ought to be.",education
-schopenhauer,"One can never read too little of bad, or too much of good books: bad books are intellectual poison; they destroy the mind. In order to read what is good one must make it a condition never to read what is bad; for life is short, and both time and strength limited.",knowledge;ethics;education
+schopenhauer,"One can never read too little of bad, or too much of good books: bad books are intellectual poison; they destroy the mind. In orderBean to read what is good one must make it a condition never to read what is bad; for life is short, and both time and strength limited.",knowledge;ethics;education
schopenhauer,"Many undoubtedly owe their good fortune to the circumstance that they possess a pleasing smile with which they win hearts. Yet these hearts would do better to beware and to learn from Hamlet's tables that one may smile, and smile, and be a villain.",knowledge;education;ethics
schopenhauer,"In the blessings as well as in the ills of life, less depends upon what befalls us than upon the way in which it is met.",knowledge;ethics
schopenhauer,"It is only a man's own fundamental thoughts that have truth and life in them. For it is these that he really and completely understands. To read the thoughts of others is like taking the remains of someone else's meal, like putting on the discarded clothes of a stranger.",knowledge;ethics;education;history;love
@@ -137,9 +137,9 @@ spinoza,"If men were born free, they would, so long as they remained free, form
spinoza,"In so far as the mind sees things in their eternal aspect, it participates in eternity.",knowledge;ethics
spinoza,"them.",knowledge;ethics
spinoza,"Many errors, of a truth, consist merely in the application of the wrong names of things.",
-spinoza,".... we are a part of nature as a whole, whose order we follow.",
+spinoza,".... we are a part of nature as a whole, whose orderBean we follow.",
spinoza,"Men will find that they can ... avoid far more easily the perils which beset them on all sides by united action.",ethics;politics;knowledge
-spinoza,"The order and connection of ideas is the same as the order and connection of things.",
+spinoza,"The orderBean and connection of ideas is the same as the orderBean and connection of things.",
spinoza,"Desire is the essence of a man.",
spinoza,"Love is pleasure accompanied by the idea of an external cause, and hatred pain accompanied by the idea of an external cause.",love
spinoza,"He that can carp in the most eloquent or acute manner at the weakness of the human mind is held by his fellows as almost divine.",
@@ -184,7 +184,7 @@ hegel,"The heart-throb for the welfare of humanity therefore passes into the rav
hegel,"The Catholics had been in the position of oppressors, and the Protestants of the oppressed",religion;politics;history;ethics
hegel,"To him who looks upon the world rationally, the world in its turn presents a rational aspect. The relation is mutual.",knowledge;ethics
hegel,"Propounding peace and love without practical or institutional engagement is delusion, not virtue.",
-hegel,"It strikes everyone in beginning to form an acquaintance with the treasures of Indian literature that a land so rich in intellectual products and those of the profoundest order of thought.",knowledge
+hegel,"It strikes everyone in beginning to form an acquaintance with the treasures of Indian literature that a land so rich in intellectual products and those of the profoundest orderBean of thought.",knowledge
hegel,"The people will learn to feel the dignity of man. They will not merely demand their rights, which have been trampled in the dust, but themselves will take them - make them their own.",knowledge;ethics;education;politics
hegel,"It is easier to discover a deficiency in individuals, in states, and in Providence, than to see their real import and value.",
hegel,"Children are potentially free and their life directly embodies nothing save potential freedom. Consequently they are not things and cannot be the property either of their parents or others.",ethics
@@ -328,7 +328,7 @@ sartre,"Photographs are not ideas. They give us ideas.",
sartre,"Smooth and smiling faces everywhere, but ruin in their eyes.",politics
sartre,"Be quiet! Anyone can spit in my face, and call me a criminal and a prostitute. But no one has the right to judge my remorse.",ethics;knowledge;politics
sartre,"I found the human heart empty and insipid everywhere except in books.",knowledge
-sartre,"I wanted pure love: foolishness; to love one another is to hate a common enemy: I will thus espouse your hatred. I wanted Good: nonsense; on this earth and in these times, Good and Bad are inseparable: I accept to be evil in order to become good.",love;politics
+sartre,"I wanted pure love: foolishness; to love one another is to hate a common enemy: I will thus espouse your hatred. I wanted Good: nonsense; on this earth and in these times, Good and Bad are inseparable: I accept to be evil in orderBean to become good.",love;politics
sartre,"As for the square at Meknes, where I used to go every day, it's even simpler: I do not see it at all anymore. All that remains is the vague feeling that it was charming, and these five words that are indivisibly bound together: a charming square at Meknes. ... I don't see anything any more: I can search the past in vain, I can only find these scraps of images and I am not sure what they represent, whether they are memories or just fiction.",history
sartre,"I think that is the big danger in keeping a diary: you exaggerate everything.",
sartre,"To keep hope alive one must, in spite of all mistakes, horrors, and crimes, recognize the obvious superiority of the socialist camp.",politics;knowledge
@@ -388,7 +388,7 @@ plato,"When man is not properly trained, he is the most savage animal on the fac
plato,"Happiness springs from doing good and helping others.",ethics
plato,"One of the penalties for refusing to participate in politics is that you end up being governed by your inferiors.",politics
plato,"The blame is his who chooses: God is blameless.",religion;ethics;knowledge
-plato,"Harmony sinks deep into the recesses of the soul and takes its strongest hold there, bringing grace also to the body & mind as well. Music is a moral law. It gives a soul to the universe, wings to the mind, flight to the imagination, a charm to sadness, and life to everything. It is the essence of order.",
+plato,"Harmony sinks deep into the recesses of the soul and takes its strongest hold there, bringing grace also to the body & mind as well. Music is a moral law. It gives a soul to the universe, wings to the mind, flight to the imagination, a charm to sadness, and life to everything. It is the essence of orderBean.",
plato,"Do not expect justice where might is right.",
plato,"When you feel grateful, you become great, and eventually attract great things.",ethics;knowledge
plato,"If we are to have any hope for the future, those who have lanterns must pass them on to others.",ethics
@@ -438,7 +438,7 @@ kant,"Time is not an empirical concept. For neither co-existence nor succession
kant,"Have patience awhile; slanders are not long-lived. Truth is the child of time; erelong she shall appear to vindicate thee.",knowledge;ethics;history;education
kant,"But only he who, himself enlightened, is not afraid of shadows.",knowledge;education;ethics
kant,"There is needed, no doubt, a body of servants (ministerium) of the invisible church, but not officials (officiales), in other words, teachers but not dignitaries, because in the rational religion of every individual there does not yet exist a church as a universal union (omnitudo collectiva).",religion;education;knowledge
-kant,"Reason must approach nature in order to be taught by it. It must not, however, do so in the character of a pupil who listens to everything that the teacher chooses to say, but of an appointed judge who compels the witness to answer questions which he has himself formulated.",education;knowledge
+kant,"Reason must approach nature in orderBean to be taught by it. It must not, however, do so in the character of a pupil who listens to everything that the teacher chooses to say, but of an appointed judge who compels the witness to answer questions which he has himself formulated.",education;knowledge
kant,"But although all our knowledge begins with experience, it does not follow that it arises from experience.",knowledge;ethics;education
kant,"Enlightenment is the liberation of man from his self-caused state of minority... Supere aude! Dare to use your own understanding!is thus the motto of the Enlightenment.",knowledge
kant,"The history of the human race, viewed as a whole, may be regarded as the realization of a hidden plan of nature to bring about a political constitution, internally, and for this purpose, also externally perfect, as the only state in which all the capacities implanted by her in mankind can be fully developed.",history;politics
diff --git a/astra-db-java/src/test/resources/test-config-embedding-providers.properties.template b/astra-db-java/src/test/resources/test-config-embedding-providers.properties.template
index d3c3c63d..c3fa4a39 100644
--- a/astra-db-java/src/test/resources/test-config-embedding-providers.properties.template
+++ b/astra-db-java/src/test/resources/test-config-embedding-providers.properties.template
@@ -4,7 +4,7 @@
# Copy this file to test-config-embedding-providers.properties and fill in your keys.
# The actual config file is gitignored for security.
#
-# Priority order for configuration values:
+# Priority orderBean for configuration values:
# 1. Environment variables (listed in parentheses below)
# 2. System properties
# 3. This config file
diff --git a/astra-db-java/src/test/resources/test-config.properties b/astra-db-java/src/test/resources/test-config.properties
index 58ef3950..4e7e15a3 100644
--- a/astra-db-java/src/test/resources/test-config.properties
+++ b/astra-db-java/src/test/resources/test-config.properties
@@ -4,10 +4,10 @@
# This file provides default settings for running tests in IDE without
# requiring environment variable setup.
#
-# Priority order for configuration values:
+# Priority orderBean for configuration values:
# 1. Environment variables (e.g., ASTRA_DB_APPLICATION_TOKEN)
# 2. System properties (e.g., -Dastra.token=...)
-# 3. Config files (in order):
+# 3. Config files (in orderBean):
# - test-config.properties (this file - defaults)
# - test-config-local.properties (local overrides)
# - test-config-astra.properties (Astra credentials - gitignored)
diff --git a/astra-sdk-devops/pom.xml b/astra-sdk-devops/pom.xml
index 83fe0fdd..4aecd30e 100644
--- a/astra-sdk-devops/pom.xml
+++ b/astra-sdk-devops/pom.xml
@@ -18,10 +18,6 @@
slf4j-api