From a69e48df639e29453b2e4abc6d878398bed9fb12 Mon Sep 17 00:00:00 2001 From: Cedrick Lunven Date: Wed, 15 Apr 2026 18:44:33 +0200 Subject: [PATCH 01/10] spring --- .bob/notes/pending-notes.txt | 43 ++ astra-db-java-tools/src/test/1-post-mortem.MD | 8 - .../src/test/2-route-cause-analysis.MD | 27 - .../src/test/3-key-bservability-metrics.MD | 5 - .../src/test/4-Impact-Assessment.MD | 3 - .../src/test/5-resolution-steps.MD | 23 - .../src/test/6-lesson-learned.MD | 25 - .../astra/client/collections/Collection.java | 82 ++- .../CollectionInsertManyException.java | 75 ++ .../mapping/DataApiCollection.java | 204 ++++++ .../collections/mapping/DocumentId.java | 41 ++ .../client/collections/mapping/Lexical.java | 58 ++ .../client/collections/mapping/Vector.java | 63 ++ .../client/collections/mapping/Vectorize.java | 58 ++ .../client/exceptions/DataAPIException.java | 3 + .../CollectionRecordDefinition.java | 471 +++++++++++++ .../integration/AbstractCollectionIT.java | 95 +++ integrations/README.md | 103 +++ .../pom.xml | 68 ++ .../DataAPIAutoConfiguration.java | 268 +++++++ .../DataAPIClientProperties.java | 242 +++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../data-api-spring-boot-3x-starter/pom.xml | 56 ++ .../DataApiCollectionCrudRepository.java | 74 ++ .../spring/DataApiTableCrudRepository.java | 4 + .../langchain4j-astradb}/pom.xml | 0 .../src/license/apache2/header.txt | 0 .../src/license/apache2/license.txt | 0 .../src/license/licenses.properties | 0 .../rag/AstraVectorizeContentRetriever.java | 0 .../rag/AstraVectorizeIngestor.java | 0 .../embedding/AstraDbEmbeddingStore.java | 0 .../store/embedding/AstraDbFilterMapper.java | 0 .../EmbeddingSearchRequestAstra.java | 0 .../store/embedding/package-info.java | 0 .../store/memory/AstraDbChatMemory.java | 0 .../store/memory/AstraDbChatMemoryStore.java | 0 .../store/memory/AstraDbChatMessage.java | 0 .../store/memory/AstraDbContent.java | 0 .../store/memory/package-info.java | 0 .../tables/AstraDBTableChatMessage.java | 0 .../memory/tables/AstraDbTableChatMemory.java | 0 .../datastax/astra/langchain4j/Assistant.java | 0 .../astra/langchain4j/AstraDBTestSupport.java | 0 .../langchain4j/store/CultzymeFixTestIT.java | 0 .../astradb/AstraDbEmbeddingStoreIT.java | 0 .../astradb/GettingStartedGuideTestIT.java | 0 .../GettingStartedGuideVectorizedTestIT.java | 0 .../chat/astradb/AstraDbChatMemoryIT.java | 0 .../src/test/resources/johnny.txt | 0 .../src/test/resources/logback-test.xml | 0 .../src/test/resources/shadow.txt | 0 integrations/pom.xml | 24 + pom.xml | 7 +- samples/pom.xml | 5 +- .../src/main/resources/application.properties | 3 - samples/sample-openrag-api/README.MD | 31 + samples/sample-openrag-api/pom.xml | 46 ++ .../java/com/ibm/openrag/OpenRagClient.java | 657 ++++++++++++++++++ .../com/ibm/openrag/OpenRagClientDemo.java | 165 +++++ .../src/main/resources/application.properties | 8 + .../src/main/resources/mcp-definition.json | 17 + .../pom.xml | 18 +- .../DataApiStarterSpringBootApplication.java | 4 +- .../java/com/ibm/astra/demo/books/Book.java | 102 +++ .../ibm/astra/demo/books/BookRepository.java | 21 + .../com/ibm/astra/demo/books/DataSet.java | 290 ++++++++ .../config/ApplicationStartupListener.java | 30 + .../demo/controller/HelloController.java | 36 + .../src/main/resources/application.yaml | 101 +++ .../src/main/resources/banner.txt | 8 + ...aApiStarterSpringBootApplicationTests.java | 58 ++ tools/pom.xml | 36 + .../tool-import-csv}/pom.xml | 7 +- .../astra/tool/loader/csv/CsvLoader.java | 0 .../tool/loader/csv/CsvLoaderSettings.java | 0 .../astra/tool/loader/csv/CsvRowMapper.java | 0 .../src/main/resources/logback.xml | 0 .../astra/samples/CsvCustomerSupport.java | 0 .../astra/samples/CsvLoaderAnoop.java | 0 .../astra/samples/CsvLoaderListing.java | 0 .../astra/samples/CsvPhilosophers.java | 0 .../resources/customer_support_tickets.csv | 0 .../src/test/resources/philosopher-quotes.csv | 0 tools/tool-import-json/pom.xml | 25 + .../tool/loader/json/JsonDocumentLoader.java | 0 .../tool/loader/json/JsonLoaderSettings.java | 0 .../tool/loader/json/JsonRecordMapper.java | 0 .../src/main/resources/logback.xml | 21 + .../astra/samples/JsonLoaderMtgSets.java | 0 .../src/test/resources/demo-set-list.json | 0 tools/tool-pdf/pom.xml | 25 + .../astra/tool/loader/pdf/PdfLoader.java | 0 tools/tool-pdf/src/main/resources/logback.xml | 21 + tools/tool-rag/pom.xml | 25 + .../astra/tool/loader/rag/RagGenericTest.java | 0 .../astra/tool/loader/rag/RagRepository.java | 0 .../rag/ingestion/RagEmbeddingsModels.java | 0 .../rag/ingestion/RagIngestionConfig.java | 0 .../loader/rag/ingestion/RagIngestionJob.java | 0 .../tool/loader/rag/sources/RagJobStatus.java | 0 .../tool/loader/rag/sources/RagSource.java | 0 .../rag/sources/RagSourceCreationRequest.java | 0 .../loader/rag/sources/RagSourceStatus.java | 0 .../tool/loader/rag/sources/RagSources.java | 0 .../tool/loader/rag/stores/RagStore.java | 0 tools/tool-rag/src/main/resources/logback.xml | 21 + 107 files changed, 3789 insertions(+), 123 deletions(-) delete mode 100644 astra-db-java-tools/src/test/1-post-mortem.MD delete mode 100644 astra-db-java-tools/src/test/2-route-cause-analysis.MD delete mode 100644 astra-db-java-tools/src/test/3-key-bservability-metrics.MD delete mode 100644 astra-db-java-tools/src/test/4-Impact-Assessment.MD delete mode 100644 astra-db-java-tools/src/test/5-resolution-steps.MD delete mode 100644 astra-db-java-tools/src/test/6-lesson-learned.MD create mode 100644 astra-db-java/src/main/java/com/datastax/astra/client/collections/exceptions/CollectionInsertManyException.java create mode 100644 astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java create mode 100644 astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DocumentId.java create mode 100644 astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Lexical.java create mode 100644 astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vector.java create mode 100644 astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vectorize.java create mode 100644 astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java create mode 100644 integrations/README.md create mode 100644 integrations/data-api-spring-boot-3x-autoconfigure/pom.xml create mode 100644 integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java create mode 100644 integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java create mode 100644 integrations/data-api-spring-boot-3x-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 integrations/data-api-spring-boot-3x-starter/pom.xml create mode 100644 integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java create mode 100644 integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java rename {langchain4j-astradb => integrations/langchain4j-astradb}/pom.xml (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/license/apache2/header.txt (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/license/apache2/license.txt (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/license/licenses.properties (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeContentRetriever.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeIngestor.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbEmbeddingStore.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbFilterMapper.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/embedding/EmbeddingSearchRequestAstra.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/embedding/package-info.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemory.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemoryStore.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMessage.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbContent.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/memory/package-info.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDBTableChatMessage.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDbTableChatMemory.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/java/com/datastax/astra/langchain4j/Assistant.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/java/com/datastax/astra/langchain4j/AstraDBTestSupport.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/java/dev/langchain4j/store/CultzymeFixTestIT.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/java/dev/langchain4j/store/embedding/astradb/AstraDbEmbeddingStoreIT.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideTestIT.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideVectorizedTestIT.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/java/dev/langchain4j/store/memory/chat/astradb/AstraDbChatMemoryIT.java (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/resources/johnny.txt (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/resources/logback-test.xml (100%) rename {langchain4j-astradb => integrations/langchain4j-astradb}/src/test/resources/shadow.txt (100%) create mode 100644 integrations/pom.xml delete mode 100644 samples/sample-astra-spring-boot3x/src/main/resources/application.properties create mode 100644 samples/sample-openrag-api/README.MD create mode 100644 samples/sample-openrag-api/pom.xml create mode 100644 samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java create mode 100644 samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClientDemo.java create mode 100644 samples/sample-openrag-api/src/main/resources/application.properties create mode 100644 samples/sample-openrag-api/src/main/resources/mcp-definition.json rename samples/{sample-astra-spring-boot3x => sample-spring-boot3x}/pom.xml (78%) rename samples/{sample-astra-spring-boot3x/src/main/java/com/ibm/api => sample-spring-boot3x/src/main/java/com/ibm/astra}/demo/DataApiStarterSpringBootApplication.java (91%) create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/DataSet.java create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/config/ApplicationStartupListener.java create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HelloController.java create mode 100644 samples/sample-spring-boot3x/src/main/resources/application.yaml create mode 100644 samples/sample-spring-boot3x/src/main/resources/banner.txt create mode 100644 samples/sample-spring-boot3x/src/test/java/com/ibm/astra/demo/DataApiStarterSpringBootApplicationTests.java create mode 100644 tools/pom.xml rename {astra-db-java-tools => tools/tool-import-csv}/pom.xml (88%) rename {astra-db-java-tools => tools/tool-import-csv}/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoader.java (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoaderSettings.java (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/main/java/com/datastax/astra/tool/loader/csv/CsvRowMapper.java (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/main/resources/logback.xml (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/test/java/com/datastax/astra/samples/CsvCustomerSupport.java (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/test/java/com/datastax/astra/samples/CsvLoaderAnoop.java (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/test/java/com/datastax/astra/samples/CsvLoaderListing.java (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/test/java/com/datastax/astra/samples/CsvPhilosophers.java (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/test/resources/customer_support_tickets.csv (100%) rename {astra-db-java-tools => tools/tool-import-csv}/src/test/resources/philosopher-quotes.csv (100%) create mode 100644 tools/tool-import-json/pom.xml rename {astra-db-java-tools => tools/tool-import-json}/src/main/java/com/datastax/astra/tool/loader/json/JsonDocumentLoader.java (100%) rename {astra-db-java-tools => tools/tool-import-json}/src/main/java/com/datastax/astra/tool/loader/json/JsonLoaderSettings.java (100%) rename {astra-db-java-tools => tools/tool-import-json}/src/main/java/com/datastax/astra/tool/loader/json/JsonRecordMapper.java (100%) create mode 100644 tools/tool-import-json/src/main/resources/logback.xml rename {astra-db-java-tools => tools/tool-import-json}/src/test/java/com/datastax/astra/samples/JsonLoaderMtgSets.java (100%) rename {astra-db-java-tools => tools/tool-import-json}/src/test/resources/demo-set-list.json (100%) create mode 100644 tools/tool-pdf/pom.xml rename {astra-db-java-tools => tools/tool-pdf}/src/main/java/com/datastax/astra/tool/loader/pdf/PdfLoader.java (100%) create mode 100644 tools/tool-pdf/src/main/resources/logback.xml create mode 100644 tools/tool-rag/pom.xml rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/RagGenericTest.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/RagRepository.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagEmbeddingsModels.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionConfig.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionJob.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagJobStatus.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSource.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceCreationRequest.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceStatus.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSources.java (100%) rename {astra-db-java-tools => tools/tool-rag}/src/main/java/com/datastax/astra/tool/loader/rag/stores/RagStore.java (100%) create mode 100644 tools/tool-rag/src/main/resources/logback.xml diff --git a/.bob/notes/pending-notes.txt b/.bob/notes/pending-notes.txt index e69de29b..e192aecb 100644 --- a/.bob/notes/pending-notes.txt +++ b/.bob/notes/pending-notes.txt @@ -0,0 +1,43 @@ +{"id":"086eb265-e894-41bc-982b-311e33732f20","ts":"2026-04-13T15:56:02.753Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/exceptions/CollectionInsertManyException.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"af6db2e4-499e-4bdb-b9e4-6736b6847236","ts":"2026-04-13T15:57:33.358Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"29749d0c-d0bc-4925-8a74-bfca709270f3","ts":"2026-04-13T15:57:46.321Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"20b7a488-0180-4e22-a627-71a95171ee0e","ts":"2026-04-13T15:57:51.822Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"4d3843b4-e92c-497b-8658-ef31332179fc","ts":"2026-04-13T16:01:04.441Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"3c77efd0-e081-43e7-be2a-80e833e19dcc","ts":"2026-04-13T16:07:36.628Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"514c93b4-6dae-40f9-b5ba-ce0a67c76d8c","ts":"2026-04-14T13:06:17.635Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"31b8dcd9-4ee4-432c-a218-377bb8d1d49b","ts":"2026-04-14T13:06:49.780Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"e0ba8d48-a632-49e5-abb5-7feb6a426cce","ts":"2026-04-14T13:41:58.880Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} +{"id":"890d99cb-bd46-46b2-af08-455cb6e72b9c","ts":"2026-04-14T13:59:52.748Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} +{"id":"ea38a881-70f7-433e-a182-b6af74dd0286","ts":"2026-04-14T14:01:46.525Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClientDemo.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} +{"id":"8540479c-b0f6-4f6e-88e8-6221a78cd0ee","ts":"2026-04-14T14:02:51.798Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClientDemo.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} +{"id":"5fcfc69e-021f-45ef-a320-34881f5d6702","ts":"2026-04-14T14:05:25.994Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} +{"id":"b25f29bc-2d8d-420a-9824-f409b3ef7311","ts":"2026-04-14T14:08:13.658Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} +{"id":"a8c45b94-aad3-47dc-ab60-78590392ebb0","ts":"2026-04-14T14:09:31.786Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} +{"id":"208796a0-89a9-4bff-b80d-f89e673f2eb5","ts":"2026-04-14T14:10:00.290Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} +{"id":"6a17898d-5400-410a-830d-74eaa6b077ce","ts":"2026-04-15T13:30:05.367Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} +{"id":"56434bfd-7538-40ef-97f1-dfb28b596972","ts":"2026-04-15T13:30:13.586Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/tool-import-csv/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} +{"id":"97e41698-1904-4f34-934a-d943d51a8b06","ts":"2026-04-15T13:30:19.124Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/tool-import-json/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} +{"id":"1f10ae47-46d4-4f43-af5f-e86c7a4a755f","ts":"2026-04-15T13:30:25.064Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/tool-rag/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} +{"id":"827cac40-7c98-4b78-afde-32f70374d157","ts":"2026-04-15T13:31:34.940Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} +{"id":"f6956f76-c041-43c5-ae03-e6f2083042a7","ts":"2026-04-15T13:32:59.210Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} +{"id":"d7fcbfb0-354e-44fc-81fd-44d631d12067","ts":"2026-04-15T13:36:03.584Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/tool-pdf/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} +{"id":"755ce287-c36c-4920-8dc6-11df85af1906","ts":"2026-04-15T13:36:09.591Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} +{"id":"63efa172-1290-4da2-975c-81f41c33c48e","ts":"2026-04-15T15:03:45.601Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/pom.xml","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} +{"id":"b9a95084-4702-4a94-bb09-e8bbf1be0a9f","ts":"2026-04-15T15:03:52.269Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} +{"id":"d5da5d21-2825-4a2c-8a58-0b355646bbad","ts":"2026-04-15T15:05:17.901Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} +{"id":"43288406-8a7d-4308-b368-263249ad1fc4","ts":"2026-04-15T15:05:52.818Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} +{"id":"1ec74f02-5409-4635-b8f9-5e4e6ba72ed0","ts":"2026-04-15T15:05:59.064Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} +{"id":"dfd292f5-b392-4b4f-880f-f8e171107de2","ts":"2026-04-15T15:06:17.200Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} +{"id":"d12f3244-8ce3-44cf-b42d-57344f6a04fb","ts":"2026-04-15T15:06:51.294Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/README.md","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} +{"id":"af971520-f282-4b05-82b9-e9deb9950e19","ts":"2026-04-15T15:33:23.923Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"119c45f7-36d4-426c-8a7f-9fbcc94e49c3","ts":"2026-04-15T15:38:22.443Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vectorize.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"c10b914b-8ae6-44ca-b3e6-3c2fac514833","ts":"2026-04-15T15:38:38.323Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"f6de1171-ad13-4889-b2b6-74edfe2f6e85","ts":"2026-04-15T15:43:22.996Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Lexical.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"fbe14c90-3e84-4b32-a921-ccd4f64b9db5","ts":"2026-04-15T15:43:38.369Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"07bb888c-3c39-4cb4-9c7a-7a35427f60c0","ts":"2026-04-15T15:44:56.544Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vector.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"c5019869-c249-4fd1-9cd4-c67245843759","ts":"2026-04-15T15:45:16.825Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"b5639047-f9aa-476d-a66f-0f9546443795","ts":"2026-04-15T15:45:54.657Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"863643ef-a8f9-4f13-a129-de26297bad4f","ts":"2026-04-15T15:49:28.185Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"a54dc518-f6c4-43cd-a44d-0fbdc060ac29","ts":"2026-04-15T15:50:14.871Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/DataSet.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"78412107-2cdd-47d1-8950-d3fbac5c3d5f","ts":"2026-04-15T15:58:52.204Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"64c74f4e-d910-4816-899f-0a7a9b660a01","ts":"2026-04-15T15:59:08.980Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} diff --git a/astra-db-java-tools/src/test/1-post-mortem.MD b/astra-db-java-tools/src/test/1-post-mortem.MD deleted file mode 100644 index f95fd1a5..00000000 --- a/astra-db-java-tools/src/test/1-post-mortem.MD +++ /dev/null @@ -1,8 +0,0 @@ -Issue: -Following a kube/cluster migration, some data was missing in Fiverr's customer-facing application due to an incorrect configuration on the new infrastructure. - -Impact: Conversations between users on the Fiverr website were temporarily unavailable. - -Root cause: This happened because the min_num_rack_nodes_for_loading_data setting was incorrectly left at 9 when the number of writer nodes was scaled down to 1, preventing writers from reloading sstables and resulting in unavailable data. - -Steps to resolve: We reset min_num_rack_nodes_for_loading_data to 0, restarted the writer nodes, enabled cache preloading, and triggered manual preloading on each rack sequentially to hydrate the cache, restoring data access and confirming resolution with the customer. \ No newline at end of file diff --git a/astra-db-java-tools/src/test/2-route-cause-analysis.MD b/astra-db-java-tools/src/test/2-route-cause-analysis.MD deleted file mode 100644 index c57c6f20..00000000 --- a/astra-db-java-tools/src/test/2-route-cause-analysis.MD +++ /dev/null @@ -1,27 +0,0 @@ -Root cause - -A Kubernetes cluster migration for this tenant was performed on Saturday, February 8th, 2025, at 9:00 PM PT. During the migration, non-standard steps were taken to accelerate tenant activation on the target cluster. - -The PCU on the source cluster was deleted. - -The tenant was scaled to 9 dedicated replicas. - -min_num_rack_nodes_for_loading_data was set to ensure faster activation on the target cluster.Once migration was completed: - -The PCU was recreated with min=1, max=1, and reserved=1. - -New writers were successfully added. - -However, min_num_rack_nodes_for_loading_data was not reset to 0, which hindered data reloads across new writers, as the actual number of writer replicas was lower than the configured min_num_rack_nodes_for_loading_data value.This missing step ultimately caused the incident. - -Whys - -Why was min_num_rack_nodes_for_loading_data set? To accelerate tenant activation on the target Kubernetes cluster by increasing replicas to 9. - -Why was this additional step needed? The tenant contained 95 GB of metadata (no SAI), which was estimated to take around 30 minutes for the full outage phase—too long for the customer. - -Why wasn’t min_num_rack_nodes_for_loading_data reset after migration? Post-migration, the PE team identified that the target cluster was not on the vSearch HF12 version, while the source cluster was. This version mismatch diverted focus toward checking potential compatibility issues, and the reset was overlooked, preventing data reload and leading to the incident. - -Why did min_nodes=9 cause the issue? After the tenant was moved back to the PCU (configured with min=1, max=1, and reserved=1), the issue was triggered. The actual number of writer replicas was less than the configured min_num_rack_nodes_for_loading_data=9. When the number of active nodes falls below this value, the full SSTable reload does not occur. In this case, min_num_rack_nodes_for_loading_data was set to 9, but there was only 1 writer per rack. As a result, full data reload didn’t occur and writers continued serving reads with inconsistent data. - -Why were no alerts triggered? Alerts weren’t triggered since there were no detectable anomalies—such as request failures, elevated latencies, or other metrics—that would have surfaced this issue. \ No newline at end of file diff --git a/astra-db-java-tools/src/test/3-key-bservability-metrics.MD b/astra-db-java-tools/src/test/3-key-bservability-metrics.MD deleted file mode 100644 index a1bca25b..00000000 --- a/astra-db-java-tools/src/test/3-key-bservability-metrics.MD +++ /dev/null @@ -1,5 +0,0 @@ -Detection - -The issue was reported by the customer. - -No alerts were triggered that could have helped identify the issue before the customer report. \ No newline at end of file diff --git a/astra-db-java-tools/src/test/4-Impact-Assessment.MD b/astra-db-java-tools/src/test/4-Impact-Assessment.MD deleted file mode 100644 index d997b585..00000000 --- a/astra-db-java-tools/src/test/4-Impact-Assessment.MD +++ /dev/null @@ -1,3 +0,0 @@ -Customer impact - -There was no actual data loss in this case; however, the customer was unable to access the data because it had not been fully loaded onto the writers. \ No newline at end of file diff --git a/astra-db-java-tools/src/test/5-resolution-steps.MD b/astra-db-java-tools/src/test/5-resolution-steps.MD deleted file mode 100644 index 3ff4a166..00000000 --- a/astra-db-java-tools/src/test/5-resolution-steps.MD +++ /dev/null @@ -1,23 +0,0 @@ -Resolution summary - -Set min_num_rack_nodes_for_loading_data to 0. - -Rolled out writer nodes. - -Enabled the preload cache. - -Manually triggered cache preload rack by rack. - -Verification - -On the Writer dashboard, the PE team monitored: - -Remote file cache disk usage - -Relative file cache size percentage - -Both metrics confirmed that data had returned to pre-migration levels. - -Partition keys provided by the customer were verified on writers using the nodetool API. - -After all data preloading was completed, the customer confirmed successful recovery. \ No newline at end of file diff --git a/astra-db-java-tools/src/test/6-lesson-learned.MD b/astra-db-java-tools/src/test/6-lesson-learned.MD deleted file mode 100644 index 25c6f4f7..00000000 --- a/astra-db-java-tools/src/test/6-lesson-learned.MD +++ /dev/null @@ -1,25 +0,0 @@ -What went well - -The DB core team, support, and PE collaborated to identify the root cause and implement the resolution. - -What didn't go so well - -Escalations workflow didn't work ( Based on feedback from DM) - -The PE on-call engineer was new and unfamiliar with this type of issue, which contributed to a longer mitigation time. - -Areas for improvement - -Any additional settings added during migrations, need to be taken off after they finish. - -Make sure to monitor RemoteFileCacheDiskUsage size after the migration till it reaches pre-migration level. - -Issue triggered has been addressed in vSearch HF12. The fix ensures that, instead of returning no data, the system now returns an error. - -Fix details https://github.com/riptano/cndb/pull/15705 and https://github.com/riptano/cndb/issues/15673 - -Key takeaways - -Migration acceleration can introduce risks, and the runbook was updated to address these risks. - -The issue was related to data inaccessibility, not actual data loss. \ No newline at end of file diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java b/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java index 7136806c..d0ef2376 100644 --- a/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java +++ b/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java @@ -51,6 +51,7 @@ import com.datastax.astra.client.collections.definition.documents.types.ObjectId; import com.datastax.astra.client.collections.definition.documents.types.UUIDv6; import com.datastax.astra.client.collections.definition.documents.types.UUIDv7; +import com.datastax.astra.client.collections.exceptions.CollectionInsertManyException; import com.datastax.astra.client.collections.exceptions.TooManyDocumentsToCountException; import com.datastax.astra.client.core.DataAPIKeywords; import com.datastax.astra.client.core.commands.Command; @@ -63,7 +64,9 @@ import com.datastax.astra.client.core.vector.DataAPIVector; import com.datastax.astra.client.databases.Database; import com.datastax.astra.client.exceptions.DataAPIException; +import com.datastax.astra.client.exceptions.DataAPIResponseException; import com.datastax.astra.client.exceptions.UnexpectedDataAPIResponseException; +import com.datastax.astra.internal.api.DataAPIResponse; import com.datastax.astra.client.tables.commands.options.TableDistinctOptions; import com.datastax.astra.internal.api.DataAPIResponse; import com.datastax.astra.internal.api.DataAPIStatus; @@ -590,12 +593,30 @@ public CollectionInsertManyResult insertMany(List documents, Collec // Grouping All Insert ids in the same list. CollectionInsertManyResult finalResult = new CollectionInsertManyResult(); + List partialExceptions = new ArrayList<>(); + try { + // Collect results from all futures, even if some fail for (Future future : futures) { - CollectionInsertManyResult res = future.get(); - finalResult.getInsertedIds().addAll(res.getInsertedIds()); - finalResult.getDocumentResponses().addAll(res.getDocumentResponses()); + try { + CollectionInsertManyResult res = future.get(); + finalResult.getInsertedIds().addAll(res.getInsertedIds()); + finalResult.getDocumentResponses().addAll(res.getDocumentResponses()); + } catch (ExecutionException e) { + // Collect partial insertion exceptions + if (e.getCause() instanceof CollectionInsertManyException) { + CollectionInsertManyException partialEx = (CollectionInsertManyException) e.getCause(); + // Add the partial IDs from this failed chunk + finalResult.getInsertedIds().addAll(partialEx.getInsertedIds()); + partialExceptions.add(partialEx); + } else if (e.getCause() instanceof DataAPIException) { + throw (DataAPIException) e.getCause(); + } else { + throw new DataAPIException(ERROR_CODE_INTERRUPTED, "Error during insertMany execution", e.getCause()); + } + } } + // Set a default timeouts for the overall operation long totalTimeout = this.options.getTimeout(); if (options.getDataAPIClientOptions() != null) { @@ -607,10 +628,18 @@ public CollectionInsertManyResult insertMany(List documents, Collec } else { throw new DataAPIException(ERROR_CODE_TIMEOUT, "Request did not complete within "); } - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof DataAPIException) { - throw (DataAPIException) e.getCause(); + + // If we collected any partial exceptions, throw a consolidated one + if (!partialExceptions.isEmpty()) { + throw new CollectionInsertManyException( + finalResult.getInsertedIds(), + String.format("Partial insertion: %d documents inserted across %d chunks, %d chunks failed", + finalResult.getInsertedIds().size(), + futures.size(), + partialExceptions.size()) + ); } + } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new DataAPIException(ERROR_CODE_INTERRUPTED, "Thread was interrupted while waiting", e); } @@ -794,15 +823,40 @@ private Callable getInsertManyResultCallable(List insertedIds = status.getInsertedIds().stream().map(this::unmarshallDocumentId).toList(); + result.setInsertedIds(insertedIds); + } + + if (status.getDocumentResponses()!= null && !status.getDocumentResponses().isEmpty()) { + result.setDocumentResponses(status.getDocumentResponses()); + } + + return result; + } catch (DataAPIResponseException e) { + // Extract partial insertedIds from the response before the error + List partialIds = new ArrayList<>(); + if (e.getCommandsList() != null && !e.getCommandsList().isEmpty()) { + DataAPIResponse errorResponse = e.getCommandsList().get(0).getResponse(); + if (errorResponse != null && errorResponse.getStatus() != null + && errorResponse.getStatus().getInsertedIds() != null) { + partialIds = errorResponse.getStatus().getInsertedIds().stream() + .map(this::unmarshallDocumentId) + .toList(); + } + } + + // Throw CollectionInsertManyException with partial IDs + throw new CollectionInsertManyException(partialIds, + "Partial insertion: " + partialIds.size() + " documents inserted before error. " + + "Error: " + e.getMessage()); } - return result; }; } diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/collections/exceptions/CollectionInsertManyException.java b/astra-db-java/src/main/java/com/datastax/astra/client/collections/exceptions/CollectionInsertManyException.java new file mode 100644 index 00000000..3b580537 --- /dev/null +++ b/astra-db-java/src/main/java/com/datastax/astra/client/collections/exceptions/CollectionInsertManyException.java @@ -0,0 +1,75 @@ +package com.datastax.astra.client.collections.exceptions; + +/*- + * #%L + * Data API Java Client + * -- + * Copyright (C) 2024 - 2026 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.options.DataAPIClientOptions; +import com.datastax.astra.client.exceptions.DataAPIException; + +import java.util.List; + +public class CollectionInsertManyException extends DataAPIException { + + /** + * List of successfully inserted document IDs before the error occurred. + */ + private final List insertedIds; + + /** + * Default constructor. + */ + public CollectionInsertManyException() { + super(ERROR_CODE_PARTIAL_INSERTION, "Some documents were not inserted, check insertedIds property."); + this.insertedIds = List.of(); + } + + /** + * Constructor with inserted IDs. + * + * @param insertedIds List of successfully inserted document IDs + */ + public CollectionInsertManyException(List insertedIds) { + super(ERROR_CODE_PARTIAL_INSERTION, + String.format("Partial insertion: %d documents were inserted before error occurred.", + insertedIds != null ? insertedIds.size() : 0)); + this.insertedIds = insertedIds != null ? List.copyOf(insertedIds) : List.of(); + } + + /** + * Constructor with inserted IDs and custom message. + * + * @param insertedIds List of successfully inserted document IDs + * @param message Custom error message + */ + public CollectionInsertManyException(List insertedIds, String message) { + super(ERROR_CODE_PARTIAL_INSERTION, message); + this.insertedIds = insertedIds != null ? List.copyOf(insertedIds) : List.of(); + } + + /** + * Get the list of successfully inserted document IDs. + * + * @return Unmodifiable list of inserted IDs + */ + public List getInsertedIds() { + return insertedIds; + } + +} diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java new file mode 100644 index 00000000..10ab8df3 --- /dev/null +++ b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java @@ -0,0 +1,204 @@ +package com.datastax.astra.client.collections.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 com.datastax.astra.client.collections.definition.CollectionDefaultIdTypes; +import com.datastax.astra.client.core.lexical.AnalyzerTypes; +import com.datastax.astra.client.core.vector.SimilarityMetric; + +import javax.lang.model.type.NullType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to define a collection and its configuration options. + *

+ * This annotation allows you to specify collection-level settings including: + *

    + *
  • Collection name
  • + *
  • Default ID type
  • + *
  • Indexing options (allow/deny lists)
  • + *
  • Vector configuration (dimension, similarity metric)
  • + *
  • Vectorization service settings
  • + *
  • Lexical search configuration
  • + *
  • Reranking service settings
  • + *
+ *

+ * + *

Example usage:

+ *
+ * {@code
+ * @DataApiCollection(
+ *     value = "my_collection",
+ *     defaultIdType = CollectionDefaultIdTypes.UUID,
+ *     vectorDimension = 1536,
+ *     vectorSimilarity = SimilarityMetric.COSINE,
+ *     indexingDeny = {"internal_field", "temp_data"}
+ * )
+ * public class MyDocument {
+ *     @DocumentId
+ *     private String id;
+ *     // ... other fields
+ * }
+ * }
+ * 
+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DataApiCollection { + + /** + * Collection name. If not provided, the class name (lowercase) will be used. + * + * @return the collection name + */ + String value() default ""; + + // --------------------- + // DefaultId Options + // --------------------- + + /** + * The default ID type for documents in this collection. + * Defaults to UNDEFINED (no default ID type specified). + * + * @return the default ID type + */ + String defaultIdType() default ""; + + // --------------------- + // Indexing Options + // --------------------- + + /** + * List of field names to exclude from indexing. + * Mutually exclusive with {@link #indexingAllow()}. + * + * @return array of field names to deny indexing + */ + String[] indexingDeny() default {}; + + /** + * List of field names to include in indexing (only these will be indexed). + * Mutually exclusive with {@link #indexingDeny()}. + * + * @return array of field names to allow indexing + */ + String[] indexingAllow() default {}; + + // --------------------- + // Vector Options + // --------------------- + + /** + * Vector dimension size. Must be greater than 0 to enable vector search. + * Default is -1 (no vector configuration). + * + * @return the vector dimension + */ + int vectorDimension() default -1; + + /** + * Similarity metric for vector search. + * Only used when {@link #vectorDimension()} is specified. + * + * @return the similarity metric + */ + SimilarityMetric vectorSimilarity() default SimilarityMetric.COSINE; + + // --------------------- + // Vectorize Options + // --------------------- + + /** + * Vectorization service provider (e.g., "openai", "huggingface"). + * When specified, enables automatic vectorization. + * + * @return the vectorization provider + */ + String vectorizeProvider() default ""; + + /** + * Vectorization model name (e.g., "text-embedding-ada-002"). + * Required when {@link #vectorizeProvider()} is specified. + * + * @return the model name + */ + String vectorizeModel() default ""; + + /** + * Shared secret key name for vectorization service authentication. + * Optional, used when the provider requires authentication. + * + * @return the shared secret key name + */ + String vectorizeSharedSecret() default ""; + + // --------------------- + // Lexical Options + // --------------------- + + /** + * Enable or disable lexical search for this collection. + * Default is true (enabled). + * + * @return true to enable lexical search, false to disable + */ + boolean lexicalEnabled() default true; + + /** + * Analyzer type for lexical search. + * Only used when {@link #lexicalEnabled()} is true. + * + * @return the analyzer type + */ + AnalyzerTypes lexicalAnalyzer() default AnalyzerTypes.STANDARD; + + // --------------------- + // Rerank Options + // --------------------- + + /** + * Enable or disable reranking for this collection. + * Default is false (disabled). + * + * @return true to enable reranking, false to disable + */ + boolean rerankEnabled() default false; + + /** + * Reranking service provider (e.g., "cohere"). + * Required when {@link #rerankEnabled()} is true. + * + * @return the reranking provider + */ + String rerankProvider() default ""; + + /** + * Reranking model name (e.g., "rerank-english-v2.0"). + * Required when {@link #rerankEnabled()} is true. + * + * @return the reranking model name + */ + String rerankModel() default ""; +} diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DocumentId.java b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DocumentId.java new file mode 100644 index 00000000..e0359ec6 --- /dev/null +++ b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DocumentId.java @@ -0,0 +1,41 @@ +package com.datastax.astra.client.collections.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 com.fasterxml.jackson.annotation.JsonProperty; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to define properties for a database column. This annotation can be used on fields + * to specify custom column names, types, and additional properties. + * + *

The {@code Column} annotation provides flexibility for mapping fields to database columns, + * with options to customize column name, type, and other attributes.

+ */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JsonProperty("_id") +public @interface DocumentId { +} diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Lexical.java b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Lexical.java new file mode 100644 index 00000000..a2c6c38f --- /dev/null +++ b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Lexical.java @@ -0,0 +1,58 @@ +package com.datastax.astra.client.collections.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 com.fasterxml.jackson.annotation.JsonProperty; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a field for lexical search in a collection document. + *

+ * This annotation indicates that the field should be serialized with the special + * property name "$lexical" for use with DataStax Astra DB's lexical search features. + * The field must be of type String. + *

+ * + *

Example usage:

+ *
+ * {@code
+ * @DataApiCollection
+ * public class MyDocument {
+ *     @DocumentId
+ *     private String id;
+ *     
+ *     @Lexical
+ *     private String searchableText;
+ *     
+ *     // getters and setters
+ * }
+ * }
+ * 
+ */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JsonProperty("$lexical") +public @interface Lexical { +} diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vector.java b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vector.java new file mode 100644 index 00000000..e746f14e --- /dev/null +++ b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vector.java @@ -0,0 +1,63 @@ +package com.datastax.astra.client.collections.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 com.fasterxml.jackson.annotation.JsonProperty; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a field as a vector in a collection document. + *

+ * This annotation indicates that the field should be serialized with the special + * property name "$vector" for use with DataStax Astra DB's vector search features. + * The field must be of type {@code float[]} or {@code DataAPIVector}. + *

+ * + *

Example usage:

+ *
+ * {@code
+ * @DataApiCollection
+ * public class MyDocument {
+ *     @DocumentId
+ *     private String id;
+ *     
+ *     @Vector
+ *     private float[] embedding;
+ *     
+ *     // or
+ *     
+ *     @Vector
+ *     private DataAPIVector embedding;
+ *     
+ *     // getters and setters
+ * }
+ * }
+ * 
+ */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JsonProperty("$vector") +public @interface Vector { +} diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vectorize.java b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vectorize.java new file mode 100644 index 00000000..4d8e8bcc --- /dev/null +++ b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vectorize.java @@ -0,0 +1,58 @@ +package com.datastax.astra.client.collections.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 com.fasterxml.jackson.annotation.JsonProperty; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a field for vectorization in a collection document. + *

+ * This annotation indicates that the field should be serialized with the special + * property name "$vectorize" for use with DataStax Astra DB's vectorization features. + * The field must be of type String. + *

+ * + *

Example usage:

+ *
+ * {@code
+ * @DataApiCollection
+ * public class MyDocument {
+ *     @DocumentId
+ *     private String id;
+ *     
+ *     @Vectorize
+ *     private String content;
+ *     
+ *     // getters and setters
+ * }
+ * }
+ * 
+ */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JsonProperty("$vectorize") +public @interface Vectorize { +} diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/exceptions/DataAPIException.java b/astra-db-java/src/main/java/com/datastax/astra/client/exceptions/DataAPIException.java index 1a8463f8..03462ee9 100644 --- a/astra-db-java/src/main/java/com/datastax/astra/client/exceptions/DataAPIException.java +++ b/astra-db-java/src/main/java/com/datastax/astra/client/exceptions/DataAPIException.java @@ -56,6 +56,9 @@ public class DataAPIException extends RuntimeException { /** Default error code. */ public static final String DEFAULT_ERROR_CODE = "CLIENT_ERROR"; + /** Default error code. */ + public static final String ERROR_CODE_PARTIAL_INSERTION = "CLIENT_PARTIAL_INSERTION"; + /** Default error code. */ public static final String ERROR_CODE_TIMEOUT = "CLIENT_TIMEOUT"; 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/CollectionRecordDefinition.java new file mode 100644 index 00000000..6aaad841 --- /dev/null +++ b/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java @@ -0,0 +1,471 @@ +package com.datastax.astra.internal.reflection; + +/*- + * #%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.definition.CollectionDefaultIdTypes; +import com.datastax.astra.client.collections.definition.CollectionDefinition; +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.Vector; +import com.datastax.astra.client.collections.mapping.Vectorize; +import com.datastax.astra.client.core.lexical.Analyzer; +import com.datastax.astra.client.core.vector.DataAPIVector; +import com.dtsx.astra.sdk.utils.Utils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.introspect.AnnotatedField; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.type.TypeFactory; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Provides introspection and metadata for a collection document entity. + *

+ * This class inspects a JavaBean of type {@code T} annotated with {@link DataApiCollection} + * to extract and manage metadata about its properties, particularly the document ID field + * marked with {@link DocumentId}. + *

+ * + * @param the type of the collection document entity being introspected + */ +@Slf4j +@Data +public class CollectionRecordDefinition { + + /** Class introspected. */ + private final Class clazz; + + /** Collection name. */ + private final String collectionName; + + /** All fields in the bean. */ + private final Map fields; + + /** The field marked with @DocumentId. */ + private EntityFieldDefinition idField; + + /** The field marked with @Vectorize. */ + private EntityFieldDefinition vectorizeField; + + /** The field marked with @Lexical. */ + private EntityFieldDefinition lexicalField; + + /** The field marked with @Vector. */ + private EntityFieldDefinition vectorField; + + /** + * Mapper for the serialization + */ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + + /** + * Constructor for the collection record definition. + * + * @param clazz the class type + */ + public CollectionRecordDefinition(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(); + } else { + this.collectionName = clazz.getSimpleName().toLowerCase(); + } + + // Find properties + List properties = OBJECT_MAPPER + .getSerializationConfig() + .introspect(TypeFactory.defaultInstance().constructType(clazz)) + .findProperties(); + + // Fields + for (BeanPropertyDefinition property : properties) { + EntityFieldDefinition field = new EntityFieldDefinition(); + field.setName(property.getName()); + field.setType(property.getPrimaryType().getRawClass()); + field.setJavaType(property.getPrimaryType()); + + // Set getter and setter + if (property.getGetter() != null) { + field.setGetter(property.getGetter().getAnnotated()); + } + if (property.getSetter() != null) { + field.setSetter(property.getSetter().getAnnotated()); + } + + // Check for @DocumentId, @Vectorize, @Lexical, and @Vector annotations + AnnotatedField annfield = property.getField(); + if (annfield != null) { + // Count how many special annotations are present on this field + int annotationCount = 0; + DocumentId documentIdAnn = annfield.getAnnotated().getAnnotation(DocumentId.class); + Vectorize vectorizeAnn = annfield.getAnnotated().getAnnotation(Vectorize.class); + Lexical lexicalAnn = annfield.getAnnotated().getAnnotation(Lexical.class); + Vector vectorAnn = annfield.getAnnotated().getAnnotation(Vector.class); + + if (documentIdAnn != null) annotationCount++; + if (vectorizeAnn != null) annotationCount++; + if (lexicalAnn != null) annotationCount++; + if (vectorAnn != null) annotationCount++; + + // Validate that only one special annotation is present + if (annotationCount > 1) { + throw new IllegalArgumentException(String.format( + "Field '%s' in class '%s' can only have one of @DocumentId, @Vectorize, @Lexical, or @Vector annotations.", + field.getName(), clazz.getName())); + } + + if (documentIdAnn != null) { + if (this.idField != null) { + throw new IllegalArgumentException(String.format( + "Multiple fields annotated with @DocumentId in class '%s'. Only one field can be marked as document ID.", + clazz.getName())); + } + this.idField = field; + } + + if (vectorizeAnn != null) { + if (this.vectorizeField != null) { + throw new IllegalArgumentException(String.format( + "Multiple fields annotated with @Vectorize in class '%s'. Only one field can be marked for vectorization.", + clazz.getName())); + } + if (!String.class.equals(field.getType())) { + throw new IllegalArgumentException(String.format( + "Field '%s' annotated with @Vectorize in class '%s' must be of type String.", + field.getName(), clazz.getName())); + } + this.vectorizeField = field; + } + + if (lexicalAnn != null) { + if (this.lexicalField != null) { + throw new IllegalArgumentException(String.format( + "Multiple fields annotated with @Lexical in class '%s'. Only one field can be marked for lexical search.", + clazz.getName())); + } + if (!String.class.equals(field.getType())) { + throw new IllegalArgumentException(String.format( + "Field '%s' annotated with @Lexical in class '%s' must be of type String.", + field.getName(), clazz.getName())); + } + this.lexicalField = field; + } + + if (vectorAnn != null) { + if (this.vectorField != null) { + throw new IllegalArgumentException(String.format( + "Multiple fields annotated with @Vector in class '%s'. Only one field can be marked as vector.", + clazz.getName())); + } + if (!float[].class.equals(field.getType()) && !DataAPIVector.class.equals(field.getType())) { + throw new IllegalArgumentException(String.format( + "Field '%s' annotated with @Vector in class '%s' must be of type float[] or DataAPIVector.", + field.getName(), clazz.getName())); + } + this.vectorField = field; + } + } + + fields.put(field.getName(), field); + } + } + + /** + * Gets the ID value from the given instance. + * + * @param instance the instance to extract the ID from + * @return the ID value, or null if no @DocumentId field is defined or the value is null + * @throws IllegalStateException if the ID field cannot be accessed + */ + public Object getId(T instance) { + if (instance == null) { + return null; + } + + if (idField == null) { + throw new IllegalStateException(String.format( + "No field annotated with @DocumentId found in class '%s'", + clazz.getName())); + } + + Method getter = idField.getGetter(); + if (getter == null) { + throw new IllegalStateException(String.format( + "No getter method found for @DocumentId field '%s' in class '%s'", + idField.getName(), clazz.getName())); + } + + try { + return getter.invoke(instance); + } catch (Exception e) { + throw new IllegalStateException(String.format( + "Failed to get ID value from field '%s' in class '%s'", + idField.getName(), clazz.getName()), e); + } + } + + /** + * Checks if this collection record has a field annotated with @DocumentId. + * + * @return true if an ID field exists, false otherwise + */ + public boolean hasIdField() { + return idField != null; + } + + /** + * Gets the name of the ID field. + * + * @return the ID field name, or null if no @DocumentId field is defined + */ + public String getIdFieldName() { + return idField != null ? idField.getName() : null; + } + + /** + * Gets the vectorize value from the given instance. + * + * @param instance the instance to extract the vectorize value from + * @return the vectorize value, or null if no @Vectorize field is defined or the value is null + * @throws IllegalStateException if the vectorize field cannot be accessed + */ + public String getVectorize(T instance) { + if (instance == null) { + return null; + } + + if (vectorizeField == null) { + return null; + } + + Method getter = vectorizeField.getGetter(); + if (getter == null) { + throw new IllegalStateException(String.format( + "No getter method found for @Vectorize field '%s' in class '%s'", + vectorizeField.getName(), clazz.getName())); + } + + try { + return (String) getter.invoke(instance); + } catch (Exception e) { + throw new IllegalStateException(String.format( + "Failed to get vectorize value from field '%s' in class '%s'", + vectorizeField.getName(), clazz.getName()), e); + } + } + + /** + * Checks if this collection record has a field annotated with @Vectorize. + * + * @return true if a vectorize field exists, false otherwise + */ + public boolean hasVectorizeField() { + return vectorizeField != null; + } + + /** + * Gets the name of the vectorize field. + * + * @return the vectorize field name, or null if no @Vectorize field is defined + */ + public String getVectorizeFieldName() { + return vectorizeField != null ? vectorizeField.getName() : null; + } + + /** + * Gets the lexical value from the given instance. + * + * @param instance the instance to extract the lexical value from + * @return the lexical value, or null if no @Lexical field is defined or the value is null + * @throws IllegalStateException if the lexical field cannot be accessed + */ + public String getLexical(T instance) { + if (instance == null) { + return null; + } + + if (lexicalField == null) { + return null; + } + + Method getter = lexicalField.getGetter(); + if (getter == null) { + throw new IllegalStateException(String.format( + "No getter method found for @Lexical field '%s' in class '%s'", + lexicalField.getName(), clazz.getName())); + } + + try { + return (String) getter.invoke(instance); + } catch (Exception e) { + throw new IllegalStateException(String.format( + "Failed to get lexical value from field '%s' in class '%s'", + lexicalField.getName(), clazz.getName()), e); + } + } + + /** + * Checks if this collection record has a field annotated with @Lexical. + * + * @return true if a lexical field exists, false otherwise + */ + public boolean hasLexicalField() { + return lexicalField != null; + } + + /** + * Gets the name of the lexical field. + * + * @return the lexical field name, or null if no @Lexical field is defined + */ + public String getLexicalFieldName() { + return lexicalField != null ? lexicalField.getName() : null; + } + + /** + * Gets the vector value from the given instance. + * + * @param instance the instance to extract the vector value from + * @return the vector value (float[] or DataAPIVector), or null if no @Vector field is defined or the value is null + * @throws IllegalStateException if the vector field cannot be accessed + */ + public Object getVector(T instance) { + if (instance == null) { + return null; + } + + if (vectorField == null) { + return null; + } + + Method getter = vectorField.getGetter(); + if (getter == null) { + throw new IllegalStateException(String.format( + "No getter method found for @Vector field '%s' in class '%s'", + vectorField.getName(), clazz.getName())); + } + + try { + return getter.invoke(instance); + } catch (Exception e) { + throw new IllegalStateException(String.format( + "Failed to get vector value from field '%s' in class '%s'", + vectorField.getName(), clazz.getName()), e); + } + } + + /** + * Checks if this collection record has a field annotated with @Vector. + * + * @return true if a vector field exists, false otherwise + */ + public boolean hasVectorField() { + return vectorField != null; + } + + /** + * Gets the name of the vector field. + * + * @return the vector field name, or null if no @Vector field is defined + */ + public String getVectorFieldName() { + return vectorField != null ? vectorField.getName() : null; + } + + /** + * Builds a CollectionDefinition from the @DataApiCollection annotation properties. + * + * @return a CollectionDefinition configured according to the annotation, or null if no annotation present + */ + public CollectionDefinition buildCollectionDefinition() { + DataApiCollection annotation = clazz.getAnnotation(DataApiCollection.class); + if (annotation == null) { + return null; + } + + CollectionDefinition definition = new CollectionDefinition(); + + // DefaultId + if (annotation.defaultIdType() != null && !annotation.defaultIdType().isEmpty()) { + definition.defaultId(CollectionDefaultIdTypes.fromValue(annotation.defaultIdType())); + } + + // Indexing options + if (annotation.indexingDeny().length > 0 && annotation.indexingAllow().length > 0) { + throw new IllegalArgumentException(String.format( + "Class '%s' has both indexingDeny and indexingAllow specified. These are mutually exclusive.", + clazz.getName())); + } + if (annotation.indexingDeny().length > 0) { + definition.indexingDeny(annotation.indexingDeny()); + } + if (annotation.indexingAllow().length > 0) { + definition.indexingAllow(annotation.indexingAllow()); + } + + // Vector options + if (annotation.vectorDimension() > 0) { + definition.vector(annotation.vectorDimension(), annotation.vectorSimilarity()); + } + + // Vectorize options + if (Utils.hasLength(annotation.vectorizeProvider()) && Utils.hasLength(annotation.vectorizeModel())) { + if (Utils.hasLength(annotation.vectorizeSharedSecret())) { + definition.vectorize(annotation.vectorizeProvider(), annotation.vectorizeModel(), + annotation.vectorizeSharedSecret()); + } else { + definition.vectorize(annotation.vectorizeProvider(), annotation.vectorizeModel()); + } + } + + // Lexical options + if (!annotation.lexicalEnabled()) { + definition.disableLexical(); + } else if (annotation.lexicalAnalyzer() != null) { + definition.lexical(new Analyzer(annotation.lexicalAnalyzer())); + } + + // Rerank options + if (annotation.rerankEnabled()) { + if (Utils.hasLength(annotation.rerankProvider()) && Utils.hasLength(annotation.rerankModel())) { + definition.rerank(annotation.rerankProvider(), annotation.rerankModel()); + } else { + throw new IllegalArgumentException(String.format( + "Class '%s' has rerankEnabled=true but missing rerankProvider or rerankModel", + clazz.getName())); + } + } + + return definition; + } +} 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 10a79833..05ebd423 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 @@ -86,6 +86,7 @@ void setupCollections() { // FAST TRACK FOR TESTS collectionSimple = getDatabase().getCollection(COLLECTION_SIMPLE); + /* dropAllCollections(); dropAllTables(); collectionSimple = getDatabase().createCollection(COLLECTION_SIMPLE); @@ -96,6 +97,7 @@ void setupCollections() { ProductString.class); log.info("Initialized collectionSimple='{}' and collectionVector='{}'", COLLECTION_SIMPLE, COLLECTION_VECTOR); + */ } @@ -212,6 +214,99 @@ void should_insertMany_distributed() throws TooManyDocumentsToCountException { assertThat(collectionSimple.countDocuments(200)).isEqualTo(155); } + @Test + @Order(9) + void should_insertMany_handlePartialInsertion() { + collectionSimple.deleteAll(); + + // Create 30 documents: first 20 with unique IDs, then duplicate one ID in the second batch + List docList = new ArrayList<>(); + + // First 20 documents with unique IDs (will succeed) + for (int i = 0; i < 20; i++) { + docList.add(Document.create(i).append("name", "doc_" + i)); + } + + // Next 10 documents, but duplicate ID 5 (will cause partial failure) + docList.add(Document.create(5).append("name", "duplicate_doc")); // This will fail + for (int i = 21; i < 30; i++) { + docList.add(Document.create(i).append("name", "doc_" + i)); + } + + try { + // Attempt to insert all 30 documents with ordered=true to stop on first error + collectionSimple.insertMany(docList, new CollectionInsertManyOptions() + .ordered(true) + .chunkSize(20)); + + Assertions.fail("Expected CollectionInsertManyException to be thrown"); + } catch (com.datastax.astra.client.collections.exceptions.CollectionInsertManyException e) { + // Verify we got partial insertion exception + assertThat(e.getInsertedIds()).isNotNull(); + + // Should have inserted the first 20 documents successfully + assertThat(e.getInsertedIds().size()).isGreaterThan(0); + log.info("Partial insertion: {} documents inserted before error", e.getInsertedIds().size()); + log.info("Error message: {}", e.getMessage()); + + // Verify the inserted IDs are accessible + List insertedIds = e.getInsertedIds(); + assertThat(insertedIds).isNotEmpty(); + + // Verify we can query the partially inserted documents + long actualCount = collectionSimple.countDocuments(100); + assertThat(actualCount).isEqualTo(insertedIds.size()); + log.info("Verified {} documents in collection match partial insertion count", actualCount); + } + } + + @Test + @Order(10) + void should_insertMany_handlePartialInsertion_concurrent() { + collectionSimple.deleteAll(); + + // Create 30 documents with duplicate IDs across chunks + List docList = new ArrayList<>(); + + // First chunk: 20 unique documents (will succeed) + for (int i = 0; i < 20; i++) { + docList.add(Document.create(i).append("name", "doc_" + i)); + } + + // Second chunk: 10 documents with one duplicate from first chunk + docList.add(Document.create(10).append("name", "duplicate_doc")); // Duplicate ID 10 + for (int i = 21; i < 30; i++) { + docList.add(Document.create(i).append("name", "doc_" + i)); + } + + try { + // Use concurrent processing with ordered=false + collectionSimple.insertMany(docList, new CollectionInsertManyOptions() + .ordered(false) + .concurrency(2) + .chunkSize(20)); + + Assertions.fail("Expected CollectionInsertManyException to be thrown"); + } catch (com.datastax.astra.client.collections.exceptions.CollectionInsertManyException e) { + // Verify we got partial insertion exception with aggregated IDs from all chunks + assertThat(e.getInsertedIds()).isNotNull(); + + // Should have inserted documents from successful chunks and partial from failed chunk + assertThat(e.getInsertedIds().size()).isGreaterThan(0); + log.info("Concurrent partial insertion: {} documents inserted across chunks", e.getInsertedIds().size()); + log.info("Error message: {}", e.getMessage()); + + // Verify the message indicates multiple chunks were processed + assertThat(e.getMessage()).contains("chunks"); + + // Verify actual count matches reported insertions + long actualCount = collectionSimple.countDocuments(100); + assertThat(actualCount).isEqualTo(e.getInsertedIds().size()); + } + } + + + // ========== findAll ========== @Test diff --git a/integrations/README.md b/integrations/README.md new file mode 100644 index 00000000..d9d009ba --- /dev/null +++ b/integrations/README.md @@ -0,0 +1,103 @@ +# Integrations + +This module contains integration libraries for various frameworks and platforms. + +## Modules + +### 1. langchain4j-astradb + +Integration with LangChain4j for vector store and embedding capabilities using AstraDB. + +**Artifact:** +```xml + + com.datastax.astra + langchain4j-astradb + 2.2.1-SNAPSHOT + +``` + +### 2. data-api-spring-boot-3x-autoconfigure + +Spring Boot 3.x auto-configuration module for DataAPI Client. This module provides: +- `DataAPIClientProperties` - Configuration properties class with prefix `astra.data-api` +- `DataAPIAutoConfiguration` - Auto-configuration for `DataAPIClient` and `Database` beans +- Automatic registration via `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` + +**Package:** `com.datastax.astra.boot.autoconfigure` + +**Configuration Properties:** +```yaml +astra: + data-api: + token: ${ASTRA_DB_APPLICATION_TOKEN} + endpoint-url: ${ASTRA_DB_API_ENDPOINT} + keyspace: ${ASTRA_DB_KEYSPACE:default_keyspace} + options: + http: + connect-timeout: 10000 + request-timeout: 10000 + timeout: + general-method-timeout: 30000 + collection-admin-timeout: 60000 + table-admin-timeout: 60000 + database-admin-timeout: 60000 + keyspace-admin-timeout: 60000 +``` + +### 3. astra-spring-boot-3x-starter +Spring Boot 3.x starter module that provides a convenient way to include DataAPI Client in Spring Boot applications. + +**Usage:** +```xml + + com.datastax.astra + astra-spring-boot-3x-starter + 2.2.1-SNAPSHOT + +``` + +This starter automatically includes: +- `data-api-spring-boot-3x-autoconfigure` module +- `spring-boot-starter` dependencies +- All necessary DataAPI Client dependencies + +**Auto-configured Beans:** +- `DataAPIClient` - Configured from `astra.data-api` properties +- `Database` - Configured with endpoint-url and optional keyspace + +## Building + +```bash +cd integrations +mvn clean install +``` + +## Spring Boot Integration Example + +1. Add the starter dependency to your `pom.xml` +2. Configure properties in `application.yaml`: +```yaml +astra: + data-api: + token: ${ASTRA_DB_APPLICATION_TOKEN} + endpoint-url: ${ASTRA_DB_API_ENDPOINT} + keyspace: default_keyspace +``` + +3. Inject beans in your application: +```java +@Service +public class MyService { + + @Autowired + private DataAPIClient client; + + @Autowired + private Database database; + + public void doSomething() { + // Use client or database + } +} +``` diff --git a/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml b/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml new file mode 100644 index 00000000..d0f5fe6a --- /dev/null +++ b/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + com.datastax.astra + integrations + 2.2.1-SNAPSHOT + + + data-api-spring-boot-3x-autoconfigure + + data-api-spring-boot-3x-autoconfigure + jar + Spring Boot 3.x Auto-Configuration for DataAPI Client + + + 3.5.13 + + + + + + + com.datastax.astra + astra-db-java + ${project.version} + + + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot.version} + + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot.version} + true + + + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + + + + + diff --git a/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java new file mode 100644 index 00000000..51b44683 --- /dev/null +++ b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java @@ -0,0 +1,268 @@ +package com.datastax.astra.boot.autoconfigure; + +import com.datastax.astra.client.DataAPIClient; +import com.datastax.astra.client.core.options.DataAPIClientOptions; +import com.datastax.astra.client.core.options.TimeoutOptions; +import com.datastax.astra.client.core.http.HttpClientOptions; +import com.datastax.astra.client.core.http.HttpProxy; +import com.datastax.astra.client.DataAPIDestination; +import com.datastax.astra.client.databases.Database; +import com.dtsx.astra.sdk.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.net.http.HttpClient; +import java.time.Duration; +import java.util.Map; + +/** + * Spring Boot Auto-Configuration for DataAPI Client. + * + * Initializing DataAPIClient (if class present in classpath) + * - #1 Configuration with application.properties/yaml + * - #2 Configuration with environment variables + * - #3 Configuration with .astrarc on file system in user.home + * + * You can also define your {@link DataAPIClient} explicitly. + * + * @author Cedrick LUNVEN (@clunven) + */ +@Configuration +@ConditionalOnClass(DataAPIClient.class) +@EnableConfigurationProperties(DataAPIClientProperties.class) +public class DataAPIAutoConfiguration { + + /** Logger for our Client. */ + private static final Logger LOGGER = LoggerFactory.getLogger(DataAPIAutoConfiguration.class); + + /** Reference Properties. */ + @Autowired + private DataAPIClientProperties dataAPIClientProperties; + + /** + * Spring Configuration + */ + @Autowired + private ConfigurableEnvironment env; + + /** + * Default constructor + */ + public DataAPIAutoConfiguration() {} + + /** + * Accessing DataAPI client. + * + * @return dataAPI client + */ + @Bean + @ConditionalOnMissingBean + public DataAPIClient dataAPIClient() { + LOGGER.info("Setup of DataAPIClient from application.yaml"); + + /* + * Load properties and initialize the client options + */ + DataAPIClientOptions clientOptions = new DataAPIClientOptions(); + + // Set destination + if (Utils.hasLength(dataAPIClientProperties.getDestination())) { + LOGGER.debug("+ Destination detected: {}", dataAPIClientProperties.getDestination()); + clientOptions.destination(DataAPIDestination.valueOf(dataAPIClientProperties.getDestination())); + } + + // Enable request logging + if (dataAPIClientProperties.getLogRequest() != null && dataAPIClientProperties.getLogRequest()) { + LOGGER.debug("+ Request logging enabled"); + clientOptions.logRequests(); + } + + // Configure options if present + if (dataAPIClientProperties.getOptions() != null) { + DataAPIClientProperties.Options options = dataAPIClientProperties.getOptions(); + + // Embedding API Key + if (Utils.hasLength(options.getEmbeddingApiKey())) { + LOGGER.debug("+ Embedding API key detected"); + clientOptions.embeddingAPIKey(options.getEmbeddingApiKey()); + } + + // HTTP Configuration + if (options.getHttp() != null) { + DataAPIClientProperties.Http http = options.getHttp(); + HttpClientOptions httpOptions = new HttpClientOptions(); + + if (http.getRetryCount() != null && http.getRetryDelay() != null) { + LOGGER.debug("+ HTTP retry configuration: count={}, delay={}ms", + http.getRetryCount(), http.getRetryDelay()); + httpOptions.httpRetries(http.getRetryCount(), Duration.ofMillis(http.getRetryDelay())); + } + + if (Utils.hasLength(http.getVersion())) { + LOGGER.debug("+ HTTP version: {}", http.getVersion()); + httpOptions.httpVersion(HttpClient.Version.valueOf(http.getVersion())); + } + + if (Utils.hasLength(http.getRedirect())) { + LOGGER.debug("+ HTTP redirect policy: {}", http.getRedirect()); + httpOptions.httpRedirect(HttpClient.Redirect.valueOf(http.getRedirect())); + } + + clientOptions.httpClientOptions(httpOptions); + } + + // Proxy Configuration + if (options.getProxy() != null) { + DataAPIClientProperties.Proxy proxy = options.getProxy(); + if (Utils.hasLength(proxy.getHostname()) && proxy.getPort() != null) { + LOGGER.debug("+ HTTP proxy configured: {}:{}", proxy.getHostname(), proxy.getPort()); + HttpProxy httpProxy = new HttpProxy(proxy.getHostname(), proxy.getPort()); + // Note: HttpProxy class doesn't support username/password in constructor + // Authentication would need to be handled separately if supported by the API + clientOptions.httpClientOptions( + new HttpClientOptions().httpProxy(httpProxy) + ); + } + } + + // Caller Tracking + if (options.getCallers() != null && !options.getCallers().isEmpty()) { + LOGGER.debug("+ Caller tracking configured with {} caller(s)", options.getCallers().size()); + for (DataAPIClientProperties.Caller caller : options.getCallers()) { + if (Utils.hasLength(caller.getName()) && Utils.hasLength(caller.getVersion())) { + clientOptions.addCaller(caller.getName(), caller.getVersion()); + } + } + } + + // Timeout Configuration + if (options.getTimeout() != null) { + DataAPIClientProperties.Timeout timeout = options.getTimeout(); + TimeoutOptions timeoutOptions = new TimeoutOptions(); + + if (timeout.getConnect() != null) { + LOGGER.debug("+ Connect timeout: {}ms", timeout.getConnect()); + timeoutOptions.connectTimeoutMillis(timeout.getConnect()); + } + if (timeout.getRequest() != null) { + LOGGER.debug("+ Request timeout: {}ms", timeout.getRequest()); + timeoutOptions.requestTimeoutMillis(timeout.getRequest()); + } + if (timeout.getGeneral() != null) { + LOGGER.debug("+ General timeout: {}ms", timeout.getGeneral()); + timeoutOptions.generalMethodTimeoutMillis(timeout.getGeneral()); + } + if (timeout.getDbAdmin() != null) { + LOGGER.debug("+ Database admin timeout: {}ms", timeout.getDbAdmin()); + timeoutOptions.databaseAdminTimeoutMillis(timeout.getDbAdmin()); + } + if (timeout.getKeyspaceAdmin() != null) { + LOGGER.debug("+ Keyspace admin timeout: {}ms", timeout.getKeyspaceAdmin()); + timeoutOptions.keyspaceAdminTimeoutMillis(timeout.getKeyspaceAdmin()); + } + if (timeout.getCollectionAdmin() != null) { + LOGGER.debug("+ Collection admin timeout: {}ms", timeout.getCollectionAdmin()); + timeoutOptions.collectionAdminTimeoutMillis(timeout.getCollectionAdmin()); + } + if (timeout.getTableAdmin() != null) { + LOGGER.debug("+ Table admin timeout: {}ms", timeout.getTableAdmin()); + timeoutOptions.tableAdminTimeoutMillis(timeout.getTableAdmin()); + } + + clientOptions.timeoutOptions(timeoutOptions); + } + + // Additional Headers + if (options.getHeaders() != null) { + if (options.getHeaders().getDb() != null) { + LOGGER.debug("+ Database headers configured: {} header(s)", + options.getHeaders().getDb().size()); + for (Map.Entry header : options.getHeaders().getDb().entrySet()) { + clientOptions.addDatabaseAdditionalHeader(header.getKey(), header.getValue()); + } + } + if (options.getHeaders().getAdmin() != null) { + LOGGER.debug("+ Admin headers configured: {} header(s)", + options.getHeaders().getAdmin().size()); + for (Map.Entry header : options.getHeaders().getAdmin().entrySet()) { + clientOptions.addAdminAdditionalHeader(header.getKey(), header.getValue()); + } + } + } + + // Observers + if (options.getObservers() != null && !options.getObservers().isEmpty()) { + LOGGER.debug("+ Observers configured: {} observer(s)", options.getObservers().size()); + for (DataAPIClientProperties.Observer observer : options.getObservers()) { + if (observer.getEnabled() != null && observer.getEnabled()) { + if ("logging".equalsIgnoreCase(observer.getType())) { + clientOptions.addObserver(observer.getName(), + new com.datastax.astra.internal.command.LoggingCommandObserver( + DataAPIClient.class)); + } + // Custom observers can be added here if className is provided + } + } + } + + // Serialization/Deserialization Options + if (options.getSerdes() != null) { + DataAPIClientProperties.Serdes serdes = options.getSerdes(); + if (serdes.getEncodeDurationAsISO8601() != null) { + LOGGER.debug("+ Encode Duration as ISO8601: {}", serdes.getEncodeDurationAsISO8601()); + // This would need to be set on the options if the API supports it + } + if (serdes.getEncodeDataApiVectorsAsBase64() != null) { + LOGGER.debug("+ Encode DataAPIVectors as Base64: {}", + serdes.getEncodeDataApiVectorsAsBase64()); + // This would need to be set on the options if the API supports it + } + } + } + + // Create and return the DataAPIClient + if (Utils.hasLength(dataAPIClientProperties.getToken())) { + LOGGER.info("DataAPIClient initialized with token"); + return new DataAPIClient(dataAPIClientProperties.getToken(), clientOptions); + } else { + LOGGER.info("DataAPIClient initialized without default token"); + return new DataAPIClient(clientOptions); + } + } + + /** + * Creates a Database bean if endpoint-url is provided in configuration. + * This allows direct injection of Database instance in Spring components. + * If a keyspace is specified, it will be used for database operations. + * + * @param dataAPIClient the DataAPIClient bean + * @return Database instance configured with the endpoint URL and optional keyspace + */ + @Bean + @ConditionalOnMissingBean + public Database database(DataAPIClient dataAPIClient) { + if (Utils.hasLength(dataAPIClientProperties.getEndpointUrl())) { + if (Utils.hasLength(dataAPIClientProperties.getKeyspace())) { + LOGGER.info("Setup of Database from endpoint-url: {} with keyspace: {}", + dataAPIClientProperties.getEndpointUrl(), + dataAPIClientProperties.getKeyspace()); + return dataAPIClient.getDatabase( + dataAPIClientProperties.getEndpointUrl(), + dataAPIClientProperties.getKeyspace()); + } else { + LOGGER.info("Setup of Database from endpoint-url: {}", dataAPIClientProperties.getEndpointUrl()); + return dataAPIClient.getDatabase(dataAPIClientProperties.getEndpointUrl()); + } + } else { + LOGGER.warn("No endpoint-url provided in configuration. Database bean will not be created."); + return null; + } + } +} diff --git a/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java new file mode 100644 index 00000000..82366d4e --- /dev/null +++ b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java @@ -0,0 +1,242 @@ +package com.datastax.astra.boot.autoconfigure; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; +import java.util.Map; + +/** + * Configuration properties for DataAPI Client. + * Maps to 'astra.data-api' prefix in application.yaml + * + * @author Cedrick LUNVEN (@clunven) + */ +@Data +@ConfigurationProperties(prefix = "astra.data-api") +public class DataAPIClientProperties { + + /** + * Authentication token for DataAPI access + */ + private String token; + + /** + * DataAPI endpoint URL + */ + private String endpointUrl; + + /** + * Keyspace name for database operations + */ + private String keyspace; + + /** + * Destination type (ASTRA, DSE, HCD, etc.) + */ + private String destination; + + /** + * Enable request logging + */ + private Boolean logRequest; + + /** + * Advanced options for DataAPI client + */ + private Options options; + + @Data + public static class Options { + /** + * API key for embedding services + */ + private String embeddingApiKey; + + /** + * API key for reranking services + */ + private String rerankApiKey; + + /** + * HTTP client configuration + */ + private Http http; + + /** + * HTTP proxy configuration + */ + private Proxy proxy; + + /** + * Caller tracking information + */ + private List callers; + + /** + * Timeout configuration + */ + private Timeout timeout; + + /** + * Additional headers + */ + private Headers headers; + + /** + * Observer configuration + */ + private List observers; + + /** + * Serialization/Deserialization options + */ + private Serdes serdes; + } + + @Data + public static class Http { + /** + * Number of retry attempts + */ + private Integer retryCount; + + /** + * Delay between retries in milliseconds + */ + private Integer retryDelay; + + /** + * HTTP protocol version (HTTP_1_1, HTTP_2) + */ + private String version; + + /** + * HTTP redirect policy (NEVER, ALWAYS, NORMAL) + */ + private String redirect; + } + + @Data + public static class Proxy { + /** + * Proxy username + */ + private String username; + + /** + * Proxy password + */ + private String password; + + /** + * Proxy hostname + */ + private String hostname; + + /** + * Proxy port + */ + private Integer port; + } + + @Data + public static class Caller { + /** + * Caller name + */ + private String name; + + /** + * Caller version + */ + private String version; + } + + @Data + public static class Timeout { + /** + * HTTP connection timeout in milliseconds + */ + private Integer connect; + + /** + * HTTP request timeout in milliseconds + */ + private Integer request; + + /** + * General data operation timeout in milliseconds + */ + private Integer general; + + /** + * Database admin operation timeout in milliseconds + */ + private Integer dbAdmin; + + /** + * Keyspace admin operation timeout in milliseconds + */ + private Integer keyspaceAdmin; + + /** + * Collection admin operation timeout in milliseconds + */ + private Integer collectionAdmin; + + /** + * Table admin operation timeout in milliseconds + */ + private Integer tableAdmin; + } + + @Data + public static class Headers { + /** + * Database-level headers + */ + private Map db; + + /** + * Admin-level headers + */ + private Map admin; + } + + @Data + public static class Observer { + /** + * Observer type (logging, custom, etc.) + */ + private String type; + + /** + * Observer name + */ + private String name; + + /** + * Custom observer class name + */ + private String className; + + /** + * Whether observer is enabled + */ + private Boolean enabled; + } + + @Data + public static class Serdes { + /** + * Encode Duration objects as ISO8601 strings + */ + private Boolean encodeDurationAsISO8601; + + /** + * Encode DataAPIVector objects as Base64 + */ + private Boolean encodeDataApiVectorsAsBase64; + } +} diff --git a/integrations/data-api-spring-boot-3x-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..777b9255 --- /dev/null +++ b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.datastax.astra.boot.autoconfigure.DataAPIAutoConfiguration diff --git a/integrations/data-api-spring-boot-3x-starter/pom.xml b/integrations/data-api-spring-boot-3x-starter/pom.xml new file mode 100644 index 00000000..be329272 --- /dev/null +++ b/integrations/data-api-spring-boot-3x-starter/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + com.datastax.astra + integrations + 2.2.1-SNAPSHOT + + + data-api-spring-boot-3x-starter + + astra-spring-boot-3x-starter + jar + Spring Boot 3.x Starter for DataAPI Client + + + 3.5.13 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + com.datastax.astra + data-api-spring-boot-3x-autoconfigure + ${project.version} + + + + org.springframework.data + spring-data-commons + + + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + + diff --git a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java new file mode 100644 index 00000000..bfb00a17 --- /dev/null +++ b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java @@ -0,0 +1,74 @@ +package com.datastax.astra.spring; + +import com.datastax.astra.client.collections.Collection; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public abstract class DataApiCollectionCrudRepository implements CrudRepository { + + Collection dataAPICollection; + + public Collection getCollection() { + return dataAPICollection; + } + + @Override + public S save(S entity) { + return null; + } + + @Override + public Iterable saveAll(Iterable entities) { + return null; + } + + @Override + public Optional findById(T t) { + return Optional.empty(); + } + + @Override + public boolean existsById(T t) { + return false; + } + + @Override + public Iterable findAll() { + return null; + } + + @Override + public Iterable findAllById(Iterable ts) { + return null; + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(T t) { + + } + + @Override + public void delete(RECORD entity) { + // TODO: Implement delete logic + } + + @Override + public void deleteAllById(Iterable ts) { + // TODO: Implement deleteAllById logic + } + + @Override + public void deleteAll(Iterable entities) { + } + + @Override + public void deleteAll() { + getCollection().deleteAll(); + } +} diff --git a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java new file mode 100644 index 00000000..a10b5459 --- /dev/null +++ b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java @@ -0,0 +1,4 @@ +package com.datastax.astra.spring; + +public class DataApiTableCrudRepository { +} diff --git a/langchain4j-astradb/pom.xml b/integrations/langchain4j-astradb/pom.xml similarity index 100% rename from langchain4j-astradb/pom.xml rename to integrations/langchain4j-astradb/pom.xml diff --git a/langchain4j-astradb/src/license/apache2/header.txt b/integrations/langchain4j-astradb/src/license/apache2/header.txt similarity index 100% rename from langchain4j-astradb/src/license/apache2/header.txt rename to integrations/langchain4j-astradb/src/license/apache2/header.txt diff --git a/langchain4j-astradb/src/license/apache2/license.txt b/integrations/langchain4j-astradb/src/license/apache2/license.txt similarity index 100% rename from langchain4j-astradb/src/license/apache2/license.txt rename to integrations/langchain4j-astradb/src/license/apache2/license.txt diff --git a/langchain4j-astradb/src/license/licenses.properties b/integrations/langchain4j-astradb/src/license/licenses.properties similarity index 100% rename from langchain4j-astradb/src/license/licenses.properties rename to integrations/langchain4j-astradb/src/license/licenses.properties diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeContentRetriever.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeContentRetriever.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeContentRetriever.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeContentRetriever.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeIngestor.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeIngestor.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeIngestor.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/rag/AstraVectorizeIngestor.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbEmbeddingStore.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbEmbeddingStore.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbEmbeddingStore.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbEmbeddingStore.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbFilterMapper.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbFilterMapper.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbFilterMapper.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/AstraDbFilterMapper.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/EmbeddingSearchRequestAstra.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/EmbeddingSearchRequestAstra.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/EmbeddingSearchRequestAstra.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/EmbeddingSearchRequestAstra.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/package-info.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/package-info.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/package-info.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/embedding/package-info.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemory.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemory.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemory.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemory.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemoryStore.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemoryStore.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemoryStore.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMemoryStore.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMessage.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMessage.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMessage.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbChatMessage.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbContent.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbContent.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbContent.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/AstraDbContent.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/package-info.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/package-info.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/package-info.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/package-info.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDBTableChatMessage.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDBTableChatMessage.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDBTableChatMessage.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDBTableChatMessage.java diff --git a/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDbTableChatMemory.java b/integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDbTableChatMemory.java similarity index 100% rename from langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDbTableChatMemory.java rename to integrations/langchain4j-astradb/src/main/java/com/datastax/astra/langchain4j/store/memory/tables/AstraDbTableChatMemory.java diff --git a/langchain4j-astradb/src/test/java/com/datastax/astra/langchain4j/Assistant.java b/integrations/langchain4j-astradb/src/test/java/com/datastax/astra/langchain4j/Assistant.java similarity index 100% rename from langchain4j-astradb/src/test/java/com/datastax/astra/langchain4j/Assistant.java rename to integrations/langchain4j-astradb/src/test/java/com/datastax/astra/langchain4j/Assistant.java diff --git a/langchain4j-astradb/src/test/java/com/datastax/astra/langchain4j/AstraDBTestSupport.java b/integrations/langchain4j-astradb/src/test/java/com/datastax/astra/langchain4j/AstraDBTestSupport.java similarity index 100% rename from langchain4j-astradb/src/test/java/com/datastax/astra/langchain4j/AstraDBTestSupport.java rename to integrations/langchain4j-astradb/src/test/java/com/datastax/astra/langchain4j/AstraDBTestSupport.java diff --git a/langchain4j-astradb/src/test/java/dev/langchain4j/store/CultzymeFixTestIT.java b/integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/CultzymeFixTestIT.java similarity index 100% rename from langchain4j-astradb/src/test/java/dev/langchain4j/store/CultzymeFixTestIT.java rename to integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/CultzymeFixTestIT.java diff --git a/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/AstraDbEmbeddingStoreIT.java b/integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/AstraDbEmbeddingStoreIT.java similarity index 100% rename from langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/AstraDbEmbeddingStoreIT.java rename to integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/AstraDbEmbeddingStoreIT.java diff --git a/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideTestIT.java b/integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideTestIT.java similarity index 100% rename from langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideTestIT.java rename to integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideTestIT.java diff --git a/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideVectorizedTestIT.java b/integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideVectorizedTestIT.java similarity index 100% rename from langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideVectorizedTestIT.java rename to integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/embedding/astradb/GettingStartedGuideVectorizedTestIT.java diff --git a/langchain4j-astradb/src/test/java/dev/langchain4j/store/memory/chat/astradb/AstraDbChatMemoryIT.java b/integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/memory/chat/astradb/AstraDbChatMemoryIT.java similarity index 100% rename from langchain4j-astradb/src/test/java/dev/langchain4j/store/memory/chat/astradb/AstraDbChatMemoryIT.java rename to integrations/langchain4j-astradb/src/test/java/dev/langchain4j/store/memory/chat/astradb/AstraDbChatMemoryIT.java diff --git a/langchain4j-astradb/src/test/resources/johnny.txt b/integrations/langchain4j-astradb/src/test/resources/johnny.txt similarity index 100% rename from langchain4j-astradb/src/test/resources/johnny.txt rename to integrations/langchain4j-astradb/src/test/resources/johnny.txt diff --git a/langchain4j-astradb/src/test/resources/logback-test.xml b/integrations/langchain4j-astradb/src/test/resources/logback-test.xml similarity index 100% rename from langchain4j-astradb/src/test/resources/logback-test.xml rename to integrations/langchain4j-astradb/src/test/resources/logback-test.xml diff --git a/langchain4j-astradb/src/test/resources/shadow.txt b/integrations/langchain4j-astradb/src/test/resources/shadow.txt similarity index 100% rename from langchain4j-astradb/src/test/resources/shadow.txt rename to integrations/langchain4j-astradb/src/test/resources/shadow.txt diff --git a/integrations/pom.xml b/integrations/pom.xml new file mode 100644 index 00000000..4e5a9459 --- /dev/null +++ b/integrations/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + com.datastax.astra + astra-db-java-parent + 2.2.1-SNAPSHOT + + + integrations + Integrations + pom + Integration modules for various frameworks and libraries + + + langchain4j-astradb + data-api-spring-boot-3x-autoconfigure + data-api-spring-boot-3x-starter + + + diff --git a/pom.xml b/pom.xml index 4cd9a63e..98c1e2d8 100644 --- a/pom.xml +++ b/pom.xml @@ -12,11 +12,10 @@ astra-db-java - astra-db-java-tools - langchain4j-astradb astra-sdk-devops + tools + integrations samples - @@ -25,7 +24,7 @@ 2.0.17 1.5.29 - 2.21.0 + 2.21.2 1.18.42 0.15.0 4.3.0 diff --git a/samples/pom.xml b/samples/pom.xml index bcbe7eb1..29b2eef0 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -9,13 +9,14 @@ astra-db-java-samples-parent - Data API Client - Samples + Samples pom sample-demo sample-hcd - sample-astra-spring-boot3x + sample-spring-boot3x + sample-openrag-api diff --git a/samples/sample-astra-spring-boot3x/src/main/resources/application.properties b/samples/sample-astra-spring-boot3x/src/main/resources/application.properties deleted file mode 100644 index 989945a3..00000000 --- a/samples/sample-astra-spring-boot3x/src/main/resources/application.properties +++ /dev/null @@ -1,3 +0,0 @@ -spring.application.name=data-api-starter-spring-boot - - diff --git a/samples/sample-openrag-api/README.MD b/samples/sample-openrag-api/README.MD new file mode 100644 index 00000000..88c4df46 --- /dev/null +++ b/samples/sample-openrag-api/README.MD @@ -0,0 +1,31 @@ + +### Specification + + +1. Chat (/api/v1/chat): + - POST: Create chat (streaming/non-streaming) + - GET: List conversations + - GET /{chat_id}: Get conversation details + - DELETE /{chat_id}: Delete conversation + +2. Search (/api/v1/search): + - POST: Semantic search query + +3. Documents (/api/v1/documents): + - POST /ingest: Ingest document + - DELETE: Delete document + - GET /api/v1/tasks/{task_id}: Get ingestion task status + +4. Settings (/api/v1/settings): + - GET: Get settings + - POST: Update settings + +5. Models (/api/v1/models/{provider}): + - GET: List models for provider + +6. Knowledge Filters (/api/v1/knowledge-filters): + - POST: Create filter + - POST /search: Search filters + - GET /{filter_id}: Get filter + - PUT /{filter_id}: Update filter + - DELETE /{filter_id}: Delete filter \ No newline at end of file diff --git a/samples/sample-openrag-api/pom.xml b/samples/sample-openrag-api/pom.xml new file mode 100644 index 00000000..2694ef72 --- /dev/null +++ b/samples/sample-openrag-api/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + com.datastax.astra + astra-db-java-samples-parent + 2.2.1-SNAPSHOT + + + sample-openrag-api + + sample open rag + + + 21 + 21 + + + + + com.datastax.astra + astra-db-java + + + + com.datastax.astra + langchain4j-astradb + + + + dev.langchain4j + langchain4j-open-ai-official + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + \ No newline at end of file diff --git a/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java b/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java new file mode 100644 index 00000000..a22330e5 --- /dev/null +++ b/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java @@ -0,0 +1,657 @@ +package com.ibm.openrag; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.time.Duration; + +/** + * Java SDK Client for OpenRAG API + * + * This client provides access to all OpenRAG API endpoints: + * + *

Available Endpoints:

+ * + *

1. Chat Operations (/api/v1/chat)

+ *
    + *
  • POST /api/v1/chat - Send a chat message (streaming or non-streaming) + *
      + *
    • Parameters: message, chat_id (optional), filters (optional), limit, score_threshold, filter_id (optional), stream
    • + *
    • Returns: ChatResponse with response text, chat_id, and sources
    • + *
    + *
  • + *
  • GET /api/v1/chat - List all conversations + *
      + *
    • Returns: List of conversations with metadata (id, title, created_at, last_activity, message_count)
    • + *
    + *
  • + *
  • GET /api/v1/chat/{chat_id} - Get conversation details with full message history + *
      + *
    • Returns: ConversationDetail with all messages
    • + *
    + *
  • + *
  • DELETE /api/v1/chat/{chat_id} - Delete a conversation + *
      + *
    • Returns: Success status
    • + *
    + *
  • + *
+ * + *

2. Search Operations (/api/v1/search)

+ *
    + *
  • POST /api/v1/search - Perform semantic search on documents + *
      + *
    • Parameters: query, filters (optional), limit, score_threshold, filter_id (optional)
    • + *
    • Returns: SearchResponse with list of SearchResult (content, score, metadata)
    • + *
    + *
  • + *
+ * + *

3. Document Operations (/api/v1/documents)

+ *
    + *
  • POST /api/v1/documents/ingest - Ingest a document into knowledge base + *
      + *
    • Parameters: file (multipart upload)
    • + *
    • Returns: IngestResponse with task_id for tracking
    • + *
    + *
  • + *
  • GET /api/v1/tasks/{task_id} - Get ingestion task status + *
      + *
    • Returns: IngestTaskStatus (status: pending/processing/completed/failed)
    • + *
    + *
  • + *
  • DELETE /api/v1/documents - Delete a document by filename + *
      + *
    • Parameters: filename
    • + *
    • Returns: DeleteDocumentResponse with deleted chunk count
    • + *
    + *
  • + *
+ * + *

4. Settings Operations (/api/v1/settings)

+ *
    + *
  • GET /api/v1/settings - Get current OpenRAG configuration + *
      + *
    • Returns: SettingsResponse with agent and knowledge settings
    • + *
    + *
  • + *
  • POST /api/v1/settings - Update OpenRAG configuration + *
      + *
    • Parameters: Settings object with fields to update
    • + *
    • Returns: SettingsUpdateResponse with success message
    • + *
    + *
  • + *
+ * + *

5. Models Operations (/api/v1/models)

+ *
    + *
  • GET /api/v1/models/{provider} - List available models for a provider + *
      + *
    • Parameters: provider (openai, anthropic, ollama, watsonx)
    • + *
    • Returns: ModelsResponse with language_models and embedding_models lists
    • + *
    + *
  • + *
+ * + *

6. Knowledge Filter Operations (/api/v1/knowledge-filters)

+ *
    + *
  • POST /api/v1/knowledge-filters - Create a new knowledge filter + *
      + *
    • Parameters: name, description, queryData (JSON string with query, filters, limit, score_threshold)
    • + *
    • Returns: CreateKnowledgeFilterResponse with filter ID
    • + *
    + *
  • + *
  • POST /api/v1/knowledge-filters/search - Search for knowledge filters + *
      + *
    • Parameters: query (optional), limit
    • + *
    • Returns: List of matching KnowledgeFilter objects
    • + *
    + *
  • + *
  • GET /api/v1/knowledge-filters/{filter_id} - Get a specific knowledge filter + *
      + *
    • Returns: KnowledgeFilter object
    • + *
    + *
  • + *
  • PUT /api/v1/knowledge-filters/{filter_id} - Update a knowledge filter + *
      + *
    • Parameters: name, description, queryData (fields to update)
    • + *
    • Returns: Success status
    • + *
    + *
  • + *
  • DELETE /api/v1/knowledge-filters/{filter_id} - Delete a knowledge filter + *
      + *
    • Returns: Success status
    • + *
    + *
  • + *
+ * + *

Authentication:

+ *
    + *
  • API Key via X-API-Key header (from OPENRAG_API_KEY environment variable)
  • + *
  • Or custom headers (X-Username, X-Api-Key) for IBM auth mode
  • + *
+ * + *

Configuration:

+ *
    + *
  • Base URL: Default http://localhost:3000 (from OPENRAG_URL environment variable)
  • + *
  • Timeout: Configurable request timeout
  • + *
+ * + * @author OpenRAG SDK + * @version 1.0.0 + */ +public class OpenRagClient { + + private final String baseUrl; + private final String apiKey; + private final HttpClient httpClient; + private final Duration timeout; + + /** + * Creates a new OpenRAG client with default configuration. + * Uses OPENRAG_URL and OPENRAG_API_KEY environment variables. + */ + public OpenRagClient() { + this( + System.getenv().getOrDefault("OPENRAG_URL", "http://localhost:3000"), + System.getenv("OPENRAG_API_KEY"), + Duration.ofSeconds(30) + ); + } + + /** + * Creates a new OpenRAG client with custom configuration. + * + * @param baseUrl Base URL for the OpenRAG API + * @param apiKey API key for authentication + * @param timeout Request timeout duration + */ + public OpenRagClient(String baseUrl, String apiKey, Duration timeout) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + this.apiKey = apiKey; + this.timeout = timeout; + this.httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(timeout) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + } + + // ==================== CHAT OPERATIONS ==================== + + /** + * Send a chat message (non-streaming). + * + * @param message The message to send + * @return JSON response as String + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String sendChatMessage(String message) throws IOException, InterruptedException { + return sendChatMessage(message, null, null, 10, 0.0, null); + } + + /** + * Send a chat message with full options (non-streaming). + * + * @param message The message to send + * @param chatId Optional conversation ID to continue + * @param filters Optional search filters (JSON string) + * @param limit Maximum number of search results + * @param scoreThreshold Minimum search score threshold + * @param filterId Optional knowledge filter ID + * @return JSON response as String + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String sendChatMessage(String message, String chatId, String filters, + int limit, double scoreThreshold, String filterId) + throws IOException, InterruptedException { + + StringBuilder jsonBody = new StringBuilder("{\"message\":\"" + escapeJson(message) + "\""); + jsonBody.append(",\"stream\":false"); + jsonBody.append(",\"limit\":").append(limit); + jsonBody.append(",\"score_threshold\":").append(scoreThreshold); + + if (chatId != null) { + jsonBody.append(",\"chat_id\":\"").append(escapeJson(chatId)).append("\""); + } + if (filters != null) { + jsonBody.append(",\"filters\":").append(filters); + } + if (filterId != null) { + jsonBody.append(",\"filter_id\":\"").append(escapeJson(filterId)).append("\""); + } + jsonBody.append("}"); + + return executeRequest("POST", "/api/v1/chat", jsonBody.toString()); + } + + /** + * List all conversations. + * + * @return JSON response with conversation list + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String listConversations() throws IOException, InterruptedException { + return executeRequest("GET", "/api/v1/chat", null); + } + + /** + * Get a specific conversation with full message history. + * + * @param chatId The conversation ID + * @return JSON response with conversation details + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String getConversation(String chatId) throws IOException, InterruptedException { + return executeRequest("GET", "/api/v1/chat/" + chatId, null); + } + + /** + * Delete a conversation. + * + * @param chatId The conversation ID to delete + * @return JSON response with success status + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String deleteConversation(String chatId) throws IOException, InterruptedException { + return executeRequest("DELETE", "/api/v1/chat/" + chatId, null); + } + + // ==================== SEARCH OPERATIONS ==================== + + /** + * Perform semantic search on documents. + * + * @param query The search query text + * @return JSON response with search results + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String search(String query) throws IOException, InterruptedException { + return search(query, null, 10, 0.0, null); + } + + /** + * Perform semantic search with full options. + * + * @param query The search query text + * @param filters Optional search filters (JSON string) + * @param limit Maximum number of results + * @param scoreThreshold Minimum score threshold + * @param filterId Optional knowledge filter ID + * @return JSON response with search results + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String search(String query, String filters, int limit, + double scoreThreshold, String filterId) + throws IOException, InterruptedException { + + StringBuilder jsonBody = new StringBuilder("{\"query\":\"" + escapeJson(query) + "\""); + jsonBody.append(",\"limit\":").append(limit); + jsonBody.append(",\"score_threshold\":").append(scoreThreshold); + + if (filters != null) { + jsonBody.append(",\"filters\":").append(filters); + } + if (filterId != null) { + jsonBody.append(",\"filter_id\":\"").append(escapeJson(filterId)).append("\""); + } + jsonBody.append("}"); + + return executeRequest("POST", "/api/v1/search", jsonBody.toString()); + } + + // ==================== DOCUMENT OPERATIONS ==================== + + /** + * Ingest a document into the knowledge base. + * + * @param filePath Path to the file to ingest + * @return JSON response with task_id + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String ingestDocument(String filePath) throws IOException, InterruptedException { + File file = new File(filePath); + if (!file.exists()) { + throw new IOException("File not found: " + filePath); + } + + String boundary = "----WebKitFormBoundary" + System.currentTimeMillis(); + byte[] fileContent = Files.readAllBytes(file.toPath()); + + StringBuilder body = new StringBuilder(); + body.append("--").append(boundary).append("\r\n"); + body.append("Content-Disposition: form-data; name=\"file\"; filename=\"") + .append(file.getName()).append("\"\r\n"); + body.append("Content-Type: application/octet-stream\r\n\r\n"); + + // Note: This is a simplified version. For production, use a proper multipart library + String bodyStr = body.toString(); + byte[] bodyBytes = bodyStr.getBytes(); + byte[] endBoundary = ("\r\n--" + boundary + "--\r\n").getBytes(); + + byte[] fullBody = new byte[bodyBytes.length + fileContent.length + endBoundary.length]; + System.arraycopy(bodyBytes, 0, fullBody, 0, bodyBytes.length); + System.arraycopy(fileContent, 0, fullBody, bodyBytes.length, fileContent.length); + System.arraycopy(endBoundary, 0, fullBody, bodyBytes.length + fileContent.length, endBoundary.length); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/documents/ingest")) + .timeout(timeout) + .header("X-API-Key", apiKey) + .header("Content-Type", "multipart/form-data; boundary=" + boundary) + .POST(HttpRequest.BodyPublishers.ofByteArray(fullBody)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return response.body(); + } + + /** + * Get the status of an ingestion task. + * + * @param taskId The task ID from ingest response + * @return JSON response with task status + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String getTaskStatus(String taskId) throws IOException, InterruptedException { + return executeRequest("GET", "/api/v1/tasks/" + taskId, null); + } + + /** + * Delete a document from the knowledge base. + * + * @param filename Name of the file to delete + * @return JSON response with deleted chunk count + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String deleteDocument(String filename) throws IOException, InterruptedException { + String jsonBody = "{\"filename\":\"" + escapeJson(filename) + "\"}"; + return executeRequest("DELETE", "/api/v1/documents", jsonBody); + } + + // ==================== SETTINGS OPERATIONS ==================== + + /** + * Get current OpenRAG configuration. + * + * @return JSON response with settings + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String getSettings() throws IOException, InterruptedException { + return executeRequest("GET", "/api/v1/settings", null); + } + + /** + * Update OpenRAG configuration. + * + * @param settingsJson JSON string with settings to update + * @return JSON response with success message + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String updateSettings(String settingsJson) throws IOException, InterruptedException { + return executeRequest("POST", "/api/v1/settings", settingsJson); + } + + // ==================== MODELS OPERATIONS ==================== + + /** + * List available models for a provider. + * + * @param provider Provider name (openai, anthropic, ollama, watsonx) + * @return JSON response with language_models and embedding_models + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String listModels(String provider) throws IOException, InterruptedException { + return executeRequest("GET", "/api/v1/models/" + provider, null); + } + + // ==================== KNOWLEDGE FILTER OPERATIONS ==================== + + /** + * Create a new knowledge filter. + * + * @param name Filter name + * @param description Filter description + * @param queryDataJson JSON string with query data + * @return JSON response with filter ID + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String createKnowledgeFilter(String name, String description, String queryDataJson) + throws IOException, InterruptedException { + + String jsonBody = String.format( + "{\"name\":\"%s\",\"description\":\"%s\",\"queryData\":%s}", + escapeJson(name), escapeJson(description), queryDataJson + ); + return executeRequest("POST", "/api/v1/knowledge-filters", jsonBody); + } + + /** + * Search for knowledge filters. + * + * @param query Search query (optional, empty string for all) + * @param limit Maximum number of results + * @return JSON response with matching filters + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String searchKnowledgeFilters(String query, int limit) + throws IOException, InterruptedException { + + String jsonBody = String.format("{\"query\":\"%s\",\"limit\":%d}", escapeJson(query), limit); + return executeRequest("POST", "/api/v1/knowledge-filters/search", jsonBody); + } + + /** + * Get a specific knowledge filter. + * + * @param filterId The filter ID + * @return JSON response with filter details + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String getKnowledgeFilter(String filterId) throws IOException, InterruptedException { + return executeRequest("GET", "/api/v1/knowledge-filters/" + filterId, null); + } + + /** + * Update a knowledge filter. + * + * @param filterId The filter ID + * @param updateJson JSON string with fields to update + * @return JSON response with success status + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String updateKnowledgeFilter(String filterId, String updateJson) + throws IOException, InterruptedException { + return executeRequest("PUT", "/api/v1/knowledge-filters/" + filterId, updateJson); + } + + /** + * Delete a knowledge filter. + * + * @param filterId The filter ID to delete + * @return JSON response with success status + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String deleteKnowledgeFilter(String filterId) throws IOException, InterruptedException { + return executeRequest("DELETE", "/api/v1/knowledge-filters/" + filterId, null); + } + + // ==================== HELPER METHODS ==================== + + /** + * Execute an HTTP request. + * + * @param method HTTP method (GET, POST, PUT, DELETE) + * @param path API endpoint path + * @param body Request body (null for GET/DELETE without body) + * @return Response body as String + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + private String executeRequest(String method, String path, String body) + throws IOException, InterruptedException { + + String fullUrl = baseUrl + path; + System.out.println("DEBUG: Making " + method + " request to: " + fullUrl); + System.out.println("DEBUG: API Key: " + (apiKey != null ? apiKey.substring(0, Math.min(10, apiKey.length())) + "..." : "null")); + + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(fullUrl)) + .timeout(timeout) + .header("X-API-Key", apiKey); + + // Only add Content-Type for requests with body + if (body != null && !method.equalsIgnoreCase("GET")) { + requestBuilder.header("Content-Type", "application/json"); + } + + switch (method.toUpperCase()) { + case "GET": + requestBuilder.GET(); + break; + case "POST": + requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body != null ? body : "{}")); + break; + case "PUT": + requestBuilder.PUT(HttpRequest.BodyPublishers.ofString(body != null ? body : "{}")); + break; + case "DELETE": + if (body != null) { + requestBuilder.method("DELETE", HttpRequest.BodyPublishers.ofString(body)); + } else { + requestBuilder.DELETE(); + } + break; + default: + throw new IllegalArgumentException("Unsupported HTTP method: " + method); + } + + HttpRequest request = requestBuilder.build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("DEBUG: Response status: " + response.statusCode()); + + if (response.statusCode() >= 400) { + throw new IOException("HTTP " + response.statusCode() + ": " + response.body()); + } + + return response.body(); + } catch (IOException e) { + System.err.println("DEBUG: IOException occurred: " + e.getClass().getName() + ": " + e.getMessage()); + System.err.println("DEBUG: Full URL was: " + fullUrl); + throw e; + } + } + + /** + * Escape JSON string values. + * + * @param value String to escape + * @return Escaped string + */ + private String escapeJson(String value) { + if (value == null) return ""; + return value.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + /** + * Wait for an ingestion task to complete. + * + * @param taskId The task ID to wait for + * @param maxWaitSeconds Maximum seconds to wait + * @return Final task status JSON + * @throws IOException If request fails + * @throws InterruptedException If request is interrupted + */ + public String waitForIngestion(String taskId, int maxWaitSeconds) + throws IOException, InterruptedException { + int elapsed = 0; + int pollInterval = 2; // seconds + + while (elapsed < maxWaitSeconds) { + String status = getTaskStatus(taskId); + + // Check if completed or failed + if (status.contains("\"status\":\"completed\"") || status.contains("\"status\":\"failed\"")) { + return status; + } + + Thread.sleep(pollInterval * 1000); + elapsed += pollInterval; + System.out.println("Waiting for ingestion... (" + elapsed + "s)"); + } + + throw new IOException("Ingestion timeout after " + maxWaitSeconds + " seconds"); + } + + /** + * Extract task_id from ingest response JSON. + * + * @param json JSON response string + * @return task_id or null if not found + */ + public String extractTaskId(String json) { + // Simple JSON parsing for task_id + int taskIdIndex = json.indexOf("\"task_id\""); + if (taskIdIndex == -1) return null; + + int colonIndex = json.indexOf(":", taskIdIndex); + int quoteStart = json.indexOf("\"", colonIndex); + int quoteEnd = json.indexOf("\"", quoteStart + 1); + + if (quoteStart != -1 && quoteEnd != -1) { + return json.substring(quoteStart + 1, quoteEnd); + } + return null; + } + + /** + * Extract response text from chat response JSON. + * + * @param json JSON response string + * @return response text or the full JSON if parsing fails + */ + public String extractChatResponse(String json) { + // Simple JSON parsing for response field + int responseIndex = json.indexOf("\"response\""); + if (responseIndex == -1) return json; + + int colonIndex = json.indexOf(":", responseIndex); + int quoteStart = json.indexOf("\"", colonIndex); + int quoteEnd = json.indexOf("\"", quoteStart + 1); + + if (quoteStart != -1 && quoteEnd != -1) { + return json.substring(quoteStart + 1, quoteEnd) + .replace("\\n", "\n") + .replace("\\\"", "\""); + } + return json; + } +} \ No newline at end of file diff --git a/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClientDemo.java b/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClientDemo.java new file mode 100644 index 00000000..3a77c0ac --- /dev/null +++ b/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClientDemo.java @@ -0,0 +1,165 @@ +package com.ibm.openrag; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.time.Duration; +import java.util.Properties; + +public class OpenRagClientDemo { + + // ==================== DEMO MAIN METHOD ==================== + + /** + * Demo usage of the OpenRAG client with document ingestion and question answering. + * + * This example demonstrates: + * 1. Loading configuration from application.properties + * 2. Creating a sample document + * 3. Ingesting it into OpenRAG + * 4. Waiting for ingestion to complete + * 5. Asking a question about the document + * 6. Performing a semantic search + */ + public static void main(String[] args) { + try { + // Load configuration from application.properties + System.out.println("📋 Loading configuration from application.properties..."); + Properties props = new Properties(); + try (InputStream input = OpenRagClientDemo.class.getClassLoader() + .getResourceAsStream("application.properties")) { + if (input == null) { + System.err.println("❌ Unable to find application.properties"); + return; + } + props.load(input); + } + + String apiKey = props.getProperty("openrag.apikey"); + String url = props.getProperty("openrag.url", "http://localhost:3000"); + + if (apiKey == null || apiKey.isEmpty()) { + System.err.println("❌ Error: openrag.apikey not found in application.properties"); + return; + } + + System.out.println("✓ Configuration loaded:"); + System.out.println(" - URL: " + url); + System.out.println(" - API Key: " + apiKey.substring(0, 10) + "..." + "\n"); + + // Initialize client with properties + OpenRagClient client = new OpenRagClient(url, apiKey, Duration.ofSeconds(30)); + + System.out.println("╔════════════════════════════════════════════════════════════╗"); + System.out.println("║ OpenRAG Java SDK - Complete Demo ║"); + System.out.println("╚════════════════════════════════════════════════════════════╝\n"); + + // Step 0: Verify connection by listing conversations + System.out.println("🔌 Step 0: Verifying connection to OpenRAG..."); + try { + String conversations = client.listConversations(); + System.out.println("✓ Connection successful!"); + System.out.println("Existing conversations: " + conversations + "\n"); + } catch (Exception e) { + System.err.println("❌ Connection failed: " + e.getMessage()); + System.err.println("Please verify your openrag.url and openrag.apikey in application.properties\n"); + return; + } + + // Step 1: Create a sample document + System.out.println("📄 Step 1: Creating sample document..."); + String sampleContent = """ + # Machine Learning Guide + + ## What is Machine Learning? + Machine Learning (ML) is a subset of artificial intelligence that enables + systems to learn and improve from experience without being explicitly programmed. + + ## Types of Machine Learning + 1. **Supervised Learning**: Learning from labeled data + 2. **Unsupervised Learning**: Finding patterns in unlabeled data + 3. **Reinforcement Learning**: Learning through trial and error + + ## Popular Algorithms + - Linear Regression + - Decision Trees + - Neural Networks + - Support Vector Machines + + ## Applications + Machine learning is used in: + - Image recognition + - Natural language processing + - Recommendation systems + - Autonomous vehicles + """; + + File tempFile = File.createTempFile("ml-guide-", ".md"); + Files.writeString(tempFile.toPath(), sampleContent); + System.out.println("✓ Created: " + tempFile.getName() + "\n"); + + // Step 2: Ingest the document + System.out.println("📤 Step 2: Ingesting document into OpenRAG..."); + String ingestResponse = client.ingestDocument(tempFile.getAbsolutePath()); + System.out.println("Response: " + ingestResponse); + + String taskId = client.extractTaskId(ingestResponse); + if (taskId != null) { + System.out.println("Task ID: " + taskId + "\n"); + + // Step 3: Wait for ingestion to complete + System.out.println("⏳ Step 3: Waiting for ingestion to complete..."); + String finalStatus = client.waitForIngestion(taskId, 60); + System.out.println("✓ Ingestion completed!"); + System.out.println("Status: " + finalStatus + "\n"); + } + + // Give the system a moment to index + Thread.sleep(2000); + + // Step 4: Ask a question about the document + System.out.println("💬 Step 4: Asking question about the document..."); + String question = "What are the three types of machine learning?"; + System.out.println("Question: " + question); + + String chatResponse = client.sendChatMessage(question); + String answer = client.extractChatResponse(chatResponse); + System.out.println("\n📝 Answer:"); + System.out.println(answer); + System.out.println("\nFull Response: " + chatResponse + "\n"); + + // Step 5: Perform a semantic search + System.out.println("🔍 Step 5: Performing semantic search..."); + String searchQuery = "neural networks applications"; + System.out.println("Search Query: " + searchQuery); + + String searchResponse = client.search(searchQuery); + System.out.println("\n📊 Search Results:"); + System.out.println(searchResponse + "\n"); + + // Step 6: List conversations + System.out.println("📋 Step 6: Listing conversations..."); + String conversations = client.listConversations(); + System.out.println(conversations + "\n"); + + // Step 7: Get settings + System.out.println("⚙️ Step 7: Getting OpenRAG settings..."); + String settings = client.getSettings(); + System.out.println(settings + "\n"); + + // Cleanup + System.out.println("🧹 Cleanup: Deleting sample document..."); + String deleteResponse = client.deleteDocument(tempFile.getName()); + System.out.println(deleteResponse); + tempFile.delete(); + + System.out.println("\n╔════════════════════════════════════════════════════════════╗"); + System.out.println("║ ✓ Demo completed successfully! ║"); + System.out.println("╚════════════════════════════════════════════════════════════╝"); + + } catch (Exception e) { + System.err.println("\n❌ Error: " + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/samples/sample-openrag-api/src/main/resources/application.properties b/samples/sample-openrag-api/src/main/resources/application.properties new file mode 100644 index 00000000..5ba19e9a --- /dev/null +++ b/samples/sample-openrag-api/src/main/resources/application.properties @@ -0,0 +1,8 @@ + + +# Connecting to Open RAG API +openrag.apikey=orag_Xayv9xKl_6Dc4NAuUpUWCRRjK1Rivpvg7NXnmrnFPHE +openrag.url=http://localhost:3000 + + + diff --git a/samples/sample-openrag-api/src/main/resources/mcp-definition.json b/samples/sample-openrag-api/src/main/resources/mcp-definition.json new file mode 100644 index 00000000..d455bbcf --- /dev/null +++ b/samples/sample-openrag-api/src/main/resources/mcp-definition.json @@ -0,0 +1,17 @@ +{ + "mcpServers": { + "openrag": { + "command": "uv", + "args": [ + "run", + "--directory", + "/path/to/openrag/sdks/mcp", + "openrag-mcp" + ], + "env": { + "OPENRAG_URL": "http://localhost:3000", + "OPENRAG_API_KEY": "orag_Xayv9xKl_6Dc4NAuUpUWCRRjK1Rivpvg7NXnmrnFPHE" + } + } + } +} \ No newline at end of file diff --git a/samples/sample-astra-spring-boot3x/pom.xml b/samples/sample-spring-boot3x/pom.xml similarity index 78% rename from samples/sample-astra-spring-boot3x/pom.xml rename to samples/sample-spring-boot3x/pom.xml index 35692165..17b9316d 100644 --- a/samples/sample-astra-spring-boot3x/pom.xml +++ b/samples/sample-spring-boot3x/pom.xml @@ -9,8 +9,8 @@ 2.2.1-SNAPSHOT - sample-astra-spring-boot3x - + sample Spring Boot 3x + sample-spring-boot3x + + sample spring-boot-3x 21 @@ -33,14 +33,16 @@ + com.datastax.astra - astra-db-java + data-api-spring-boot-3x-starter + ${project.version} org.springframework.boot - spring-boot-starter-data-jpa + spring-boot-starter-web @@ -48,6 +50,14 @@ spring-boot-starter-test test + + + org.springframework.boot + spring-boot-devtools + runtime + true + + diff --git a/samples/sample-astra-spring-boot3x/src/main/java/com/ibm/api/demo/DataApiStarterSpringBootApplication.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java similarity index 91% rename from samples/sample-astra-spring-boot3x/src/main/java/com/ibm/api/demo/DataApiStarterSpringBootApplication.java rename to samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java index 08ab5732..508fd73d 100644 --- a/samples/sample-astra-spring-boot3x/src/main/java/com/ibm/api/demo/DataApiStarterSpringBootApplication.java +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java @@ -1,4 +1,4 @@ -package com.ibm.api.demo; +package com.ibm.astra.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -10,4 +10,6 @@ public static void main(String[] args) { SpringApplication.run(DataApiStarterSpringBootApplication.class, args); } + + } diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java new file mode 100644 index 00000000..cac23ff2 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java @@ -0,0 +1,102 @@ +package com.ibm.astra.demo.books; + +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.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.Map; +import java.util.Set; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@DataApiCollection("c_book") +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/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java new file mode 100644 index 00000000..983677fc --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java @@ -0,0 +1,21 @@ +package com.ibm.astra.demo.books; + +import com.datastax.astra.client.DataAPIClient; +import com.datastax.astra.client.collections.Collection; +import com.datastax.astra.spring.DataApiCollectionCrudRepository; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class BookRepository extends DataApiCollectionCrudRepository { + + @Autowired + DataAPIClient dataAPIClient; + + Collection books; + + @PostConstruct + public void init() { + } +} diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/DataSet.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/DataSet.java new file mode 100644 index 00000000..42d49f18 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/DataSet.java @@ -0,0 +1,290 @@ +package com.ibm.astra.demo.books; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Dataset containing 30 sample books for testing and demonstration purposes. + */ +public class DataSet { + + public static final Book BOOK_1 = new Book() + .title("The Midnight Library") + .author("Matt Haig") + .numberOfPages(304) + .genres(Set.of("Fiction", "Fantasy", "Contemporary")) + .description("A library between life and death where every book is a different life you could have lived") + .metadata(Map.of("isbn", "978-0-525-55948-1", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_2 = new Book() + .title("Project Hail Mary") + .author("Andy Weir") + .numberOfPages(496) + .genres(Set.of("Science Fiction", "Adventure", "Thriller")) + .description("A lone astronaut must save Earth from an extinction-level threat") + .metadata(Map.of("isbn", "978-0-593-13520-5", "language", "English", "edition", "First Edition")) + .isCheckedOut(true); + + public static final Book BOOK_3 = new Book() + .title("The Seven Husbands of Evelyn Hugo") + .author("Taylor Jenkins Reid") + .numberOfPages(400) + .genres(Set.of("Historical Fiction", "Romance", "Drama")) + .description("A reclusive Hollywood icon reveals her scandalous life story") + .metadata(Map.of("isbn", "978-1-501-16134-4", "language", "English", "edition", "Reprint")) + .isCheckedOut(false); + + public static final Book BOOK_4 = new Book() + .title("Atomic Habits") + .author("James Clear") + .numberOfPages(320) + .genres(Set.of("Self-Help", "Psychology", "Business")) + .description("An easy and proven way to build good habits and break bad ones") + .metadata(Map.of("isbn", "978-0-735-21129-2", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_5 = new Book() + .title("The Silent Patient") + .author("Alex Michaelides") + .numberOfPages(336) + .genres(Set.of("Thriller", "Mystery", "Psychological")) + .description("A woman shoots her husband and then never speaks another word") + .metadata(Map.of("isbn", "978-1-250-30170-7", "language", "English", "edition", "First Edition")) + .isCheckedOut(true); + + public static final Book BOOK_6 = new Book() + .title("Educated") + .author("Tara Westover") + .numberOfPages(352) + .genres(Set.of("Memoir", "Biography", "Non-Fiction")) + .description("A memoir about a young woman who leaves her survivalist family to pursue education") + .metadata(Map.of("isbn", "978-0-399-59050-4", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_7 = new Book() + .title("The Song of Achilles") + .author("Madeline Miller") + .numberOfPages(378) + .genres(Set.of("Historical Fiction", "Fantasy", "Romance")) + .description("A retelling of the Iliad from Patroclus's perspective") + .metadata(Map.of("isbn", "978-0-062-06032-6", "language", "English", "edition", "Reprint")) + .isCheckedOut(false); + + public static final Book BOOK_8 = new Book() + .title("Dune") + .author("Frank Herbert") + .numberOfPages(688) + .genres(Set.of("Science Fiction", "Adventure", "Epic")) + .description("A sweeping tale of politics, religion, and ecology on the desert planet Arrakis") + .metadata(Map.of("isbn", "978-0-441-17271-9", "language", "English", "edition", "Anniversary Edition")) + .isCheckedOut(true); + + public static final Book BOOK_9 = new Book() + .title("Where the Crawdads Sing") + .author("Delia Owens") + .numberOfPages(384) + .genres(Set.of("Mystery", "Romance", "Coming-of-Age")) + .description("A murder mystery set in the marshlands of North Carolina") + .metadata(Map.of("isbn", "978-0-735-21932-8", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_10 = new Book() + .title("The Invisible Life of Addie LaRue") + .author("V.E. Schwab") + .numberOfPages(448) + .genres(Set.of("Fantasy", "Historical Fiction", "Romance")) + .description("A woman makes a Faustian bargain to live forever but be forgotten by everyone") + .metadata(Map.of("isbn", "978-0-765-38750-0", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_11 = new Book() + .title("Circe") + .author("Madeline Miller") + .numberOfPages(400) + .genres(Set.of("Fantasy", "Mythology", "Historical Fiction")) + .description("The story of the sorceress Circe from Greek mythology") + .metadata(Map.of("isbn", "978-0-316-55633-0", "language", "English", "edition", "First Edition")) + .isCheckedOut(true); + + public static final Book BOOK_12 = new Book() + .title("The Martian") + .author("Andy Weir") + .numberOfPages(369) + .genres(Set.of("Science Fiction", "Adventure", "Thriller")) + .description("An astronaut is stranded on Mars and must survive using science and ingenuity") + .metadata(Map.of("isbn", "978-0-553-41802-6", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_13 = new Book() + .title("The Night Circus") + .author("Erin Morgenstern") + .numberOfPages(400) + .genres(Set.of("Fantasy", "Romance", "Historical Fiction")) + .description("Two young magicians compete in a mysterious circus that appears without warning") + .metadata(Map.of("isbn", "978-0-307-74443-2", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_14 = new Book() + .title("1984") + .author("George Orwell") + .numberOfPages(328) + .genres(Set.of("Dystopian", "Science Fiction", "Political Fiction")) + .description("A totalitarian regime controls every aspect of life in Oceania") + .metadata(Map.of("isbn", "978-0-452-28423-4", "language", "English", "edition", "Centennial Edition")) + .isCheckedOut(true); + + public static final Book BOOK_15 = new Book() + .title("The Alchemist") + .author("Paulo Coelho") + .numberOfPages(208) + .genres(Set.of("Fiction", "Philosophy", "Adventure")) + .description("A shepherd boy's journey to find treasure and discover his personal legend") + .metadata(Map.of("isbn", "978-0-061-12241-5", "language", "English", "edition", "25th Anniversary")) + .isCheckedOut(false); + + public static final Book BOOK_16 = new Book() + .title("The Hobbit") + .author("J.R.R. Tolkien") + .numberOfPages(310) + .genres(Set.of("Fantasy", "Adventure", "Classic")) + .description("Bilbo Baggins embarks on an unexpected journey with dwarves and a wizard") + .metadata(Map.of("isbn", "978-0-547-92822-7", "language", "English", "edition", "75th Anniversary")) + .isCheckedOut(false); + + public static final Book BOOK_17 = new Book() + .title("The Book Thief") + .author("Markus Zusak") + .numberOfPages(552) + .genres(Set.of("Historical Fiction", "War", "Coming-of-Age")) + .description("Death narrates the story of a girl living in Nazi Germany who steals books") + .metadata(Map.of("isbn", "978-0-375-84220-7", "language", "English", "edition", "10th Anniversary")) + .isCheckedOut(true); + + public static final Book BOOK_18 = new Book() + .title("Sapiens") + .author("Yuval Noah Harari") + .numberOfPages(464) + .genres(Set.of("Non-Fiction", "History", "Science")) + .description("A brief history of humankind from the Stone Age to the modern age") + .metadata(Map.of("isbn", "978-0-062-31609-7", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_19 = new Book() + .title("The Hunger Games") + .author("Suzanne Collins") + .numberOfPages(374) + .genres(Set.of("Dystopian", "Science Fiction", "Young Adult")) + .description("Teens fight to the death in a televised competition in a dystopian future") + .metadata(Map.of("isbn", "978-0-439-02348-1", "language", "English", "edition", "First Edition")) + .isCheckedOut(false); + + public static final Book BOOK_20 = new Book() + .title("The Great Gatsby") + .author("F. Scott Fitzgerald") + .numberOfPages(180) + .genres(Set.of("Classic", "Fiction", "Romance")) + .description("The mysterious millionaire Jay Gatsby and his obsession with Daisy Buchanan") + .metadata(Map.of("isbn", "978-0-743-27356-5", "language", "English", "edition", "Scribner")) + .isCheckedOut(true); + + public static final Book BOOK_21 = new Book() + .title("Harry Potter and the Sorcerer's Stone") + .author("J.K. Rowling") + .numberOfPages(309) + .genres(Set.of("Fantasy", "Young Adult", "Adventure")) + .description("A young wizard discovers his magical heritage and attends Hogwarts") + .metadata(Map.of("isbn", "978-0-590-35340-3", "language", "English", "edition", "First American")) + .isCheckedOut(false); + + public static final Book BOOK_22 = new Book() + .title("To Kill a Mockingbird") + .author("Harper Lee") + .numberOfPages(324) + .genres(Set.of("Classic", "Historical Fiction", "Coming-of-Age")) + .description("A lawyer defends a black man accused of rape in 1930s Alabama") + .metadata(Map.of("isbn", "978-0-061-12000-8", "language", "English", "edition", "50th Anniversary")) + .isCheckedOut(false); + + public static final Book BOOK_23 = new Book() + .title("The Catcher in the Rye") + .author("J.D. Salinger") + .numberOfPages(234) + .genres(Set.of("Classic", "Coming-of-Age", "Fiction")) + .description("Holden Caulfield's journey through New York City after being expelled") + .metadata(Map.of("isbn", "978-0-316-76948-0", "language", "English", "edition", "Back Bay Books")) + .isCheckedOut(true); + + public static final Book BOOK_24 = new Book() + .title("Pride and Prejudice") + .author("Jane Austen") + .numberOfPages(432) + .genres(Set.of("Classic", "Romance", "Historical Fiction")) + .description("Elizabeth Bennet navigates love and society in Regency England") + .metadata(Map.of("isbn", "978-0-141-43951-8", "language", "English", "edition", "Penguin Classics")) + .isCheckedOut(false); + + public static final Book BOOK_25 = new Book() + .title("The Lord of the Rings") + .author("J.R.R. Tolkien") + .numberOfPages(1178) + .genres(Set.of("Fantasy", "Adventure", "Epic")) + .description("Frodo Baggins must destroy the One Ring to save Middle-earth") + .metadata(Map.of("isbn", "978-0-544-00341-5", "language", "English", "edition", "50th Anniversary")) + .isCheckedOut(false); + + public static final Book BOOK_26 = new Book() + .title("The Handmaid's Tale") + .author("Margaret Atwood") + .numberOfPages(311) + .genres(Set.of("Dystopian", "Science Fiction", "Feminist")) + .description("A woman's struggle for survival in a totalitarian theocracy") + .metadata(Map.of("isbn", "978-0-385-49081-8", "language", "English", "edition", "Anchor Books")) + .isCheckedOut(true); + + public static final Book BOOK_27 = new Book() + .title("Brave New World") + .author("Aldous Huxley") + .numberOfPages(268) + .genres(Set.of("Dystopian", "Science Fiction", "Classic")) + .description("A futuristic society where humans are genetically engineered and conditioned") + .metadata(Map.of("isbn", "978-0-060-85052-4", "language", "English", "edition", "Harper Perennial")) + .isCheckedOut(false); + + public static final Book BOOK_28 = new Book() + .title("The Kite Runner") + .author("Khaled Hosseini") + .numberOfPages(371) + .genres(Set.of("Historical Fiction", "Drama", "Coming-of-Age")) + .description("A story of friendship and redemption set in Afghanistan") + .metadata(Map.of("isbn", "978-1-594-48000-3", "language", "English", "edition", "Riverhead Books")) + .isCheckedOut(false); + + public static final Book BOOK_29 = new Book() + .title("The Road") + .author("Cormac McCarthy") + .numberOfPages(287) + .genres(Set.of("Post-Apocalyptic", "Fiction", "Drama")) + .description("A father and son journey through a devastated America") + .metadata(Map.of("isbn", "978-0-307-38789-9", "language", "English", "edition", "Vintage")) + .isCheckedOut(true); + + public static final Book BOOK_30 = new Book() + .title("Life of Pi") + .author("Yann Martel") + .numberOfPages(460) + .genres(Set.of("Adventure", "Fantasy", "Philosophical")) + .description("A boy survives 227 days at sea with a Bengal tiger") + .metadata(Map.of("isbn", "978-0-156-02732-2", "language", "English", "edition", "Mariner Books")) + .isCheckedOut(false); + + public static final List BOOKS = List.of( + BOOK_1, BOOK_2, BOOK_3, BOOK_4, BOOK_5, + BOOK_6, BOOK_7, BOOK_8, BOOK_9, BOOK_10, + BOOK_11, BOOK_12, BOOK_13, BOOK_14, BOOK_15, + BOOK_16, BOOK_17, BOOK_18, BOOK_19, BOOK_20, + BOOK_21, BOOK_22, BOOK_23, BOOK_24, BOOK_25, + BOOK_26, BOOK_27, BOOK_28, BOOK_29, BOOK_30 + ); +} \ No newline at end of file diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/config/ApplicationStartupListener.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/config/ApplicationStartupListener.java new file mode 100644 index 00000000..cf7c0384 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/config/ApplicationStartupListener.java @@ -0,0 +1,30 @@ +package com.ibm.astra.demo.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +/** + * Listener to log when the Spring application context is fully ready. + * + * @author Cedrick LUNVEN (@clunven) + */ +@Component +public class ApplicationStartupListener implements ApplicationListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationStartupListener.class); + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + LOGGER.info("=".repeat(80)); + LOGGER.info("🚀 Spring Boot Application Context is READY!"); + LOGGER.info("=".repeat(80)); + LOGGER.info("✅ DataAPI Client configured and available"); + LOGGER.info("✅ Database bean available (if endpoint-url configured)"); + LOGGER.info("📍 API endpoint: http://localhost:{}/api/hello", + event.getApplicationContext().getEnvironment().getProperty("server.port", "8080")); + LOGGER.info("=".repeat(80)); + } +} diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HelloController.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HelloController.java new file mode 100644 index 00000000..beed0be3 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HelloController.java @@ -0,0 +1,36 @@ +package com.ibm.astra.demo.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * Simple REST controller for health check and testing. + * + * @author Cedrick LUNVEN (@clunven) + */ +@RestController +@RequestMapping("/api") +public class HelloController { + + private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class); + + /** + * Simple hello endpoint to verify the application is running. + * + * @return a greeting message + */ + @GetMapping("/hello") + public Map hello() { + LOGGER.info("Hello endpoint called"); + Map response = new HashMap<>(); + response.put("message", "Hello from DataAPI Spring Boot!"); + response.put("status", "running"); + return response; + } +} diff --git a/samples/sample-spring-boot3x/src/main/resources/application.yaml b/samples/sample-spring-boot3x/src/main/resources/application.yaml new file mode 100644 index 00000000..573a86a4 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/resources/application.yaml @@ -0,0 +1,101 @@ +server: + port: 8081 + forward-headers-strategy: framework + +logging: + level: + org.springframework.web: WARN + com.ibm.astra.demo: INFO + root: WARN + +astra: + data-api: + token: "token" + endpoint-url: "url" + keyspace: default_keyspace + destination: "ASTRA" + # Would Create the Keyspace if not exists + schema-action: CREATE_IF_NOT_EXISTS + + log-request: true + options: + # Provide API key for embedding services (OpenAI, Cohere, etc.) + embedding-api-key: "your-embedding-api-key-here" + # Provide API key for reranking services + rerank-api-key: "your-rerank-api-key-here" + + # HTTP Client Options + http: + retry-count: 3 + retry-delay: 100 + # HTTP protocol settings, Default: HTTP_2 + # Options for httpVersion: HTTP_1_1, HTTP_2 + version: "HTTP_2" + # HTTP redirect policy, Default: NORMAL + # Options: NEVER, ALWAYS, NORMAL + redirect: "NORMAL" + + # HTTP Proxy configuration (optional) + #proxy: + # username: "proxy-user" + # password: "proxy-password" + # hostname: "proxy.example.com" + # port: 8080 + + # Tracking + callers: + - name: "sample-astra" + version: "1.0.0" + + # Timeout Options (all values in milliseconds) + timeout: + # Lower level HTTP connection timeout + # Default: 10000ms (10 seconds) + connect: 10000 + # Lower level HTTP request timeout + # Default: 10000ms (10 seconds) + request: 10000 + # Data operation timeout (find*, insert*, update*, delete*) + # Default: 30000ms (30 seconds) + general: 30000 + # Database admin timeout (create, delete, list databases) + # Default: 600000ms (10 minutes) + dbAdmin: 600000 + # Keyspace admin timeout (create, delete, list keyspaces) + # Default: 30000ms (30 seconds) + keyspaceAdmin: 30000 + # Collection admin timeout (create, alter, drop collections) + # Default: 60000ms (1 minute) + collectionAdmin: 60000 + # Table admin timeout (create, alter, drop tables) + # Default: 30000ms (30 seconds) + tableAdmin: 30000 + + # Additional headers + headers: + db: + "X-Custom-Header": "custom-value" + "X-Request-ID": "request-123" + "Feature-Flag-tables": "true" + admin: + "X-Admin-Header": "admin-value" + "X-Tenant-ID": "tenant-123" + + # Observers + observers: + # Built-in logging observer + - type: "logging" + name: "LoggingCommandObserver" + enabled: true + # Custom observer example + - type: "custom" + name: "MetricsObserver" + className: "com.example.MetricsObserver" + enabled: true + + # Serialization/Deserialization Options + serdes: + # Encode Duration objects as ISO8601 strings (Default: true) + encodeDurationAsISO8601: true + # Encode DataAPIVector objects as Base64 (Default: true) + encodeDataApiVectorsAsBase64: true \ No newline at end of file diff --git a/samples/sample-spring-boot3x/src/main/resources/banner.txt b/samples/sample-spring-boot3x/src/main/resources/banner.txt new file mode 100644 index 00000000..a3e8d980 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + + _____ __ ________ + / _ \ _______/ |_____________ \______ \ ____ _____ ____ + / /_\ \ / ___/\ __\_ __ \__ \ | | \_/ __ \ / \ / _ \ +/ | \\___ \ | | | | \// __ \_ | ` \ ___/| Y Y ( <_> ) +\____|__ /____ > |__| |__| (____ / /_______ /\___ >__|_| /\____/ + \/ \/ \/ \/ \/ \/ + diff --git a/samples/sample-spring-boot3x/src/test/java/com/ibm/astra/demo/DataApiStarterSpringBootApplicationTests.java b/samples/sample-spring-boot3x/src/test/java/com/ibm/astra/demo/DataApiStarterSpringBootApplicationTests.java new file mode 100644 index 00000000..6febac51 --- /dev/null +++ b/samples/sample-spring-boot3x/src/test/java/com/ibm/astra/demo/DataApiStarterSpringBootApplicationTests.java @@ -0,0 +1,58 @@ +package com.ibm.astra.demo; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for Spring Boot application startup and REST API. + * + * @author Cedrick LUNVEN (@clunven) + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class DataApiStarterSpringBootApplicationTests { + + private static final Logger LOGGER = LoggerFactory.getLogger(DataApiStarterSpringBootApplicationTests.class); + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + LOGGER.info("=".repeat(80)); + LOGGER.info("✅ Spring Boot Application Context loaded successfully!"); + LOGGER.info("=".repeat(80)); + assertThat(restTemplate).isNotNull(); + } + + @Test + void testHelloEndpoint() { + LOGGER.info("Testing hello endpoint at port: {}", port); + + String url = "http://localhost:" + port + "/api/hello"; + ResponseEntity response = restTemplate.getForEntity(url, Map.class); + + LOGGER.info("Response status: {}", response.getStatusCode()); + LOGGER.info("Response body: {}", response.getBody()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get("message")).isEqualTo("Hello from DataAPI Spring Boot!"); + assertThat(response.getBody().get("status")).isEqualTo("running"); + + LOGGER.info("✅ Hello endpoint test passed!"); + } +} diff --git a/tools/pom.xml b/tools/pom.xml new file mode 100644 index 00000000..85c6f32f --- /dev/null +++ b/tools/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + astra-db-java-tools + Data API Client Tools + pom + + + com.datastax.astra + astra-db-java-parent + 2.2.1-SNAPSHOT + + + + tool-import-csv + tool-import-json + tool-pdf + tool-rag + + + + + + com.datastax.astra + astra-db-java + ${project.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + diff --git a/astra-db-java-tools/pom.xml b/tools/tool-import-csv/pom.xml similarity index 88% rename from astra-db-java-tools/pom.xml rename to tools/tool-import-csv/pom.xml index 824fcfd0..83052c4f 100644 --- a/astra-db-java-tools/pom.xml +++ b/tools/tool-import-csv/pom.xml @@ -1,12 +1,12 @@ 4.0.0 - astra-db-java-tools - Data API Client Tools + tool-import-csv + + tools csv com.datastax.astra - astra-db-java-parent + astra-db-java-tools 2.2.1-SNAPSHOT @@ -20,7 +20,6 @@ ch.qos.logback logback-classic - org.apache.commons commons-csv diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoader.java b/tools/tool-import-csv/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoader.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoader.java rename to tools/tool-import-csv/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoader.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoaderSettings.java b/tools/tool-import-csv/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoaderSettings.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoaderSettings.java rename to tools/tool-import-csv/src/main/java/com/datastax/astra/tool/loader/csv/CsvLoaderSettings.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/csv/CsvRowMapper.java b/tools/tool-import-csv/src/main/java/com/datastax/astra/tool/loader/csv/CsvRowMapper.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/csv/CsvRowMapper.java rename to tools/tool-import-csv/src/main/java/com/datastax/astra/tool/loader/csv/CsvRowMapper.java diff --git a/astra-db-java-tools/src/main/resources/logback.xml b/tools/tool-import-csv/src/main/resources/logback.xml similarity index 100% rename from astra-db-java-tools/src/main/resources/logback.xml rename to tools/tool-import-csv/src/main/resources/logback.xml diff --git a/astra-db-java-tools/src/test/java/com/datastax/astra/samples/CsvCustomerSupport.java b/tools/tool-import-csv/src/test/java/com/datastax/astra/samples/CsvCustomerSupport.java similarity index 100% rename from astra-db-java-tools/src/test/java/com/datastax/astra/samples/CsvCustomerSupport.java rename to tools/tool-import-csv/src/test/java/com/datastax/astra/samples/CsvCustomerSupport.java diff --git a/astra-db-java-tools/src/test/java/com/datastax/astra/samples/CsvLoaderAnoop.java b/tools/tool-import-csv/src/test/java/com/datastax/astra/samples/CsvLoaderAnoop.java similarity index 100% rename from astra-db-java-tools/src/test/java/com/datastax/astra/samples/CsvLoaderAnoop.java rename to tools/tool-import-csv/src/test/java/com/datastax/astra/samples/CsvLoaderAnoop.java diff --git a/astra-db-java-tools/src/test/java/com/datastax/astra/samples/CsvLoaderListing.java b/tools/tool-import-csv/src/test/java/com/datastax/astra/samples/CsvLoaderListing.java similarity index 100% rename from astra-db-java-tools/src/test/java/com/datastax/astra/samples/CsvLoaderListing.java rename to tools/tool-import-csv/src/test/java/com/datastax/astra/samples/CsvLoaderListing.java diff --git a/astra-db-java-tools/src/test/java/com/datastax/astra/samples/CsvPhilosophers.java b/tools/tool-import-csv/src/test/java/com/datastax/astra/samples/CsvPhilosophers.java similarity index 100% rename from astra-db-java-tools/src/test/java/com/datastax/astra/samples/CsvPhilosophers.java rename to tools/tool-import-csv/src/test/java/com/datastax/astra/samples/CsvPhilosophers.java diff --git a/astra-db-java-tools/src/test/resources/customer_support_tickets.csv b/tools/tool-import-csv/src/test/resources/customer_support_tickets.csv similarity index 100% rename from astra-db-java-tools/src/test/resources/customer_support_tickets.csv rename to tools/tool-import-csv/src/test/resources/customer_support_tickets.csv diff --git a/astra-db-java-tools/src/test/resources/philosopher-quotes.csv b/tools/tool-import-csv/src/test/resources/philosopher-quotes.csv similarity index 100% rename from astra-db-java-tools/src/test/resources/philosopher-quotes.csv rename to tools/tool-import-csv/src/test/resources/philosopher-quotes.csv diff --git a/tools/tool-import-json/pom.xml b/tools/tool-import-json/pom.xml new file mode 100644 index 00000000..ec967ad4 --- /dev/null +++ b/tools/tool-import-json/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + tool-import-json + + tools json + + + com.datastax.astra + astra-db-java-tools + 2.2.1-SNAPSHOT + + + + + com.datastax.astra + astra-db-java + ${project.version} + + + ch.qos.logback + logback-classic + + + + diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/json/JsonDocumentLoader.java b/tools/tool-import-json/src/main/java/com/datastax/astra/tool/loader/json/JsonDocumentLoader.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/json/JsonDocumentLoader.java rename to tools/tool-import-json/src/main/java/com/datastax/astra/tool/loader/json/JsonDocumentLoader.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/json/JsonLoaderSettings.java b/tools/tool-import-json/src/main/java/com/datastax/astra/tool/loader/json/JsonLoaderSettings.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/json/JsonLoaderSettings.java rename to tools/tool-import-json/src/main/java/com/datastax/astra/tool/loader/json/JsonLoaderSettings.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/json/JsonRecordMapper.java b/tools/tool-import-json/src/main/java/com/datastax/astra/tool/loader/json/JsonRecordMapper.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/json/JsonRecordMapper.java rename to tools/tool-import-json/src/main/java/com/datastax/astra/tool/loader/json/JsonRecordMapper.java diff --git a/tools/tool-import-json/src/main/resources/logback.xml b/tools/tool-import-json/src/main/resources/logback.xml new file mode 100644 index 00000000..6440fb04 --- /dev/null +++ b/tools/tool-import-json/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + %d{HH:mm:ss.SSS} %magenta(%-5level) %cyan(%-20logger) : %msg%n + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/astra-db-java-tools/src/test/java/com/datastax/astra/samples/JsonLoaderMtgSets.java b/tools/tool-import-json/src/test/java/com/datastax/astra/samples/JsonLoaderMtgSets.java similarity index 100% rename from astra-db-java-tools/src/test/java/com/datastax/astra/samples/JsonLoaderMtgSets.java rename to tools/tool-import-json/src/test/java/com/datastax/astra/samples/JsonLoaderMtgSets.java diff --git a/astra-db-java-tools/src/test/resources/demo-set-list.json b/tools/tool-import-json/src/test/resources/demo-set-list.json similarity index 100% rename from astra-db-java-tools/src/test/resources/demo-set-list.json rename to tools/tool-import-json/src/test/resources/demo-set-list.json diff --git a/tools/tool-pdf/pom.xml b/tools/tool-pdf/pom.xml new file mode 100644 index 00000000..62656941 --- /dev/null +++ b/tools/tool-pdf/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + tool-pdf + + tool pdf + + + com.datastax.astra + astra-db-java-tools + 2.2.1-SNAPSHOT + + + + + com.datastax.astra + astra-db-java + ${project.version} + + + ch.qos.logback + logback-classic + + + + diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/pdf/PdfLoader.java b/tools/tool-pdf/src/main/java/com/datastax/astra/tool/loader/pdf/PdfLoader.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/pdf/PdfLoader.java rename to tools/tool-pdf/src/main/java/com/datastax/astra/tool/loader/pdf/PdfLoader.java diff --git a/tools/tool-pdf/src/main/resources/logback.xml b/tools/tool-pdf/src/main/resources/logback.xml new file mode 100644 index 00000000..6440fb04 --- /dev/null +++ b/tools/tool-pdf/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + %d{HH:mm:ss.SSS} %magenta(%-5level) %cyan(%-20logger) : %msg%n + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/tool-rag/pom.xml b/tools/tool-rag/pom.xml new file mode 100644 index 00000000..f57cb3ec --- /dev/null +++ b/tools/tool-rag/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + tool-rag + + tools rag + + + com.datastax.astra + astra-db-java-tools + 2.2.1-SNAPSHOT + + + + + com.datastax.astra + astra-db-java + ${project.version} + + + ch.qos.logback + logback-classic + + + + diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/RagGenericTest.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/RagGenericTest.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/RagGenericTest.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/RagGenericTest.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/RagRepository.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/RagRepository.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/RagRepository.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/RagRepository.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagEmbeddingsModels.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagEmbeddingsModels.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagEmbeddingsModels.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagEmbeddingsModels.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionConfig.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionConfig.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionConfig.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionConfig.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionJob.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionJob.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionJob.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/ingestion/RagIngestionJob.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagJobStatus.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagJobStatus.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagJobStatus.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagJobStatus.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSource.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSource.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSource.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSource.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceCreationRequest.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceCreationRequest.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceCreationRequest.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceCreationRequest.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceStatus.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceStatus.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceStatus.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSourceStatus.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSources.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSources.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSources.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/sources/RagSources.java diff --git a/astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/stores/RagStore.java b/tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/stores/RagStore.java similarity index 100% rename from astra-db-java-tools/src/main/java/com/datastax/astra/tool/loader/rag/stores/RagStore.java rename to tools/tool-rag/src/main/java/com/datastax/astra/tool/loader/rag/stores/RagStore.java diff --git a/tools/tool-rag/src/main/resources/logback.xml b/tools/tool-rag/src/main/resources/logback.xml new file mode 100644 index 00000000..6440fb04 --- /dev/null +++ b/tools/tool-rag/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + %d{HH:mm:ss.SSS} %magenta(%-5level) %cyan(%-20logger) : %msg%n + + + + + + + + + + + + + + + + \ No newline at end of file From 11e833f26cee87ced4e8a67942777a9ee59529cd Mon Sep 17 00:00:00 2001 From: Cedrick Lunven Date: Fri, 17 Apr 2026 18:42:50 +0200 Subject: [PATCH 02/10] spring boot --- .bob/notes/pending-notes.txt | 159 +++-- AGENT.md | 2 +- CONTRIBUTING.md | 4 +- README_TABLE_REPOSITORY.md | 333 ++++++++++ .../mapping/DataApiCollection.java | 4 +- .../astra/client/databases/Database.java | 377 ++++++++++- .../tables/definition/TableDefinition.java | 45 ++ .../tables/mapping/TablePrimaryKey.java | 76 +++ .../tables/mapping/TablePrimaryKeyClass.java | 78 +++ .../command/AbstractCommandRunner.java | 2 - ...ion.java => CollectionBeanDefinition.java} | 55 +- ...on.java => EntityTableBeanDefinition.java} | 37 +- .../internal/serdes/tables/RowMapper.java | 129 +++- .../integration/AbstractCollectionIT.java | 1 + .../test/integration/AbstractTableIT.java | 75 +++ .../astra/Astra_Collections_01_CrudIT.java | 7 + .../Astra_Collections_04_FindAndRerankIT.java | 81 +++ .../astra/test/integration/model/Book.java | 147 +++++ .../test/integration/model/OrderBean.java | 93 +++ .../test/integration/model/OrderKey.java | 88 +++ .../test/unit/TablePrimaryKeyClassDemo.java | 80 +++ .../test/unit/TablePrimaryKeyClassTest.java | 112 ++++ .../TablePrimaryKeyDeserializationTest.java | 86 +++ .../tables/TableDefinitionEqualsTest.java | 169 +++++ .../test/resources/junit-platform.properties | 4 +- .../src/test/resources/philosopher-quotes.csv | 18 +- ...ig-embedding-providers.properties.template | 2 +- .../src/test/resources/test-config.properties | 4 +- astra-sdk-devops/pom.xml | 4 - .../astra/sdk/utils/HttpClientWrapper.java | 212 +++--- .../com/dtsx/astra/sdk/utils/TestUtils.java | 40 +- .../observability/ApiExecutionInfos.java | 33 +- .../com/dtsx/astra/sdk/db/CdcClientTest.java | 55 +- .../astra/sdk/db/DatabasesClientTest.java | 2 +- .../pom.xml | 2 +- .../DataAPIAutoConfiguration.java | 38 +- .../DataAPIClientProperties.java | 10 + .../boot/autoconfigure/SchemaAction.java | 72 ++ .../data-api-spring-boot-3x-starter/pom.xml | 7 + .../DataApiCollectionCrudRepository.java | 364 ++++++++++- .../spring/DataApiSpringQueryMapper.java | 187 ++++++ .../spring/DataApiTableCrudRepository.java | 618 +++++++++++++++++- pom.xml | 13 +- .../src/main/resources/model.cql | 38 ++ samples/sample-spring-boot3x/README.md | 261 ++++++++ samples/sample-spring-boot3x/pom.xml | 7 + .../DataApiStarterSpringBootApplication.java | 5 +- .../java/com/ibm/astra/demo/books/Book.java | 65 +- .../ibm/astra/demo/books/BookRepository.java | 26 +- .../com/ibm/astra/demo/books/BookService.java | 125 ++++ .../demo/books/BookVectorSearchRequest.java | 15 + .../config/ApplicationStartupListener.java | 30 - .../astra/demo/controller/BookController.java | 122 ++++ .../demo/controller/HelloController.java | 36 - .../astra/demo/controller/HomeController.java | 16 + .../astra/demo/controller/InfoController.java | 87 +++ .../src/main/resources/application.yaml | 75 ++- .../src/main/resources/banner.txt | 16 +- .../src/main/resources/logback-spring.xml | 70 ++ .../src/test/resources/philosopher-quotes.csv | 18 +- 60 files changed, 4461 insertions(+), 476 deletions(-) create mode 100644 README_TABLE_REPOSITORY.md create mode 100644 astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKey.java create mode 100644 astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKeyClass.java rename astra-db-java/src/main/java/com/datastax/astra/internal/reflection/{CollectionRecordDefinition.java => CollectionBeanDefinition.java} (91%) rename astra-db-java/src/main/java/com/datastax/astra/internal/reflection/{EntityBeanDefinition.java => EntityTableBeanDefinition.java} (91%) create mode 100644 astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Book.java create mode 100644 astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderBean.java create mode 100644 astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java create mode 100644 astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassDemo.java create mode 100644 astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java create mode 100644 astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyDeserializationTest.java create mode 100644 astra-db-java/src/test/java/com/datastax/astra/test/unit/tables/TableDefinitionEqualsTest.java create mode 100644 integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/SchemaAction.java create mode 100644 integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java create mode 100644 samples/sample-openrag-api/src/main/resources/model.cql create mode 100644 samples/sample-spring-boot3x/README.md create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookVectorSearchRequest.java delete mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/config/ApplicationStartupListener.java create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java delete mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HelloController.java create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java create mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/InfoController.java create mode 100644 samples/sample-spring-boot3x/src/main/resources/logback-spring.xml diff --git a/.bob/notes/pending-notes.txt b/.bob/notes/pending-notes.txt index e192aecb..56cc2c3d 100644 --- a/.bob/notes/pending-notes.txt +++ b/.bob/notes/pending-notes.txt @@ -1,43 +1,116 @@ -{"id":"086eb265-e894-41bc-982b-311e33732f20","ts":"2026-04-13T15:56:02.753Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/exceptions/CollectionInsertManyException.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"af6db2e4-499e-4bdb-b9e4-6736b6847236","ts":"2026-04-13T15:57:33.358Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"29749d0c-d0bc-4925-8a74-bfca709270f3","ts":"2026-04-13T15:57:46.321Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"20b7a488-0180-4e22-a627-71a95171ee0e","ts":"2026-04-13T15:57:51.822Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"4d3843b4-e92c-497b-8658-ef31332179fc","ts":"2026-04-13T16:01:04.441Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"3c77efd0-e081-43e7-be2a-80e833e19dcc","ts":"2026-04-13T16:07:36.628Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"514c93b4-6dae-40f9-b5ba-ce0a67c76d8c","ts":"2026-04-14T13:06:17.635Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"31b8dcd9-4ee4-432c-a218-377bb8d1d49b","ts":"2026-04-14T13:06:49.780Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"e0ba8d48-a632-49e5-abb5-7feb6a426cce","ts":"2026-04-14T13:41:58.880Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/Collection.java","version":"1.0.0","taskID":"086ee69d-97a3-4479-9b97-037eef16f2d8"} -{"id":"890d99cb-bd46-46b2-af08-455cb6e72b9c","ts":"2026-04-14T13:59:52.748Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} -{"id":"ea38a881-70f7-433e-a182-b6af74dd0286","ts":"2026-04-14T14:01:46.525Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClientDemo.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} -{"id":"8540479c-b0f6-4f6e-88e8-6221a78cd0ee","ts":"2026-04-14T14:02:51.798Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClientDemo.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} -{"id":"5fcfc69e-021f-45ef-a320-34881f5d6702","ts":"2026-04-14T14:05:25.994Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} -{"id":"b25f29bc-2d8d-420a-9824-f409b3ef7311","ts":"2026-04-14T14:08:13.658Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} -{"id":"a8c45b94-aad3-47dc-ab60-78590392ebb0","ts":"2026-04-14T14:09:31.786Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} -{"id":"208796a0-89a9-4bff-b80d-f89e673f2eb5","ts":"2026-04-14T14:10:00.290Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-openrag-api/src/main/java/com/ibm/openrag/OpenRagClient.java","version":"1.0.0","taskID":"3c03f8f0-48c3-428e-a4ba-d25f04b7226a"} -{"id":"6a17898d-5400-410a-830d-74eaa6b077ce","ts":"2026-04-15T13:30:05.367Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} -{"id":"56434bfd-7538-40ef-97f1-dfb28b596972","ts":"2026-04-15T13:30:13.586Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/tool-import-csv/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} -{"id":"97e41698-1904-4f34-934a-d943d51a8b06","ts":"2026-04-15T13:30:19.124Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/tool-import-json/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} -{"id":"1f10ae47-46d4-4f43-af5f-e86c7a4a755f","ts":"2026-04-15T13:30:25.064Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/tool-rag/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} -{"id":"827cac40-7c98-4b78-afde-32f70374d157","ts":"2026-04-15T13:31:34.940Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} -{"id":"f6956f76-c041-43c5-ae03-e6f2083042a7","ts":"2026-04-15T13:32:59.210Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} -{"id":"d7fcbfb0-354e-44fc-81fd-44d631d12067","ts":"2026-04-15T13:36:03.584Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/tool-pdf/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} -{"id":"755ce287-c36c-4920-8dc6-11df85af1906","ts":"2026-04-15T13:36:09.591Z","path":"/Users/cedricklunven/dev/astra-db-java/tools/pom.xml","version":"1.0.0","taskID":"b5b24746-77ef-4912-b559-52336c38f2e9"} -{"id":"63efa172-1290-4da2-975c-81f41c33c48e","ts":"2026-04-15T15:03:45.601Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/pom.xml","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} -{"id":"b9a95084-4702-4a94-bb09-e8bbf1be0a9f","ts":"2026-04-15T15:03:52.269Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} -{"id":"d5da5d21-2825-4a2c-8a58-0b355646bbad","ts":"2026-04-15T15:05:17.901Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} -{"id":"43288406-8a7d-4308-b368-263249ad1fc4","ts":"2026-04-15T15:05:52.818Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} -{"id":"1ec74f02-5409-4635-b8f9-5e4e6ba72ed0","ts":"2026-04-15T15:05:59.064Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} -{"id":"dfd292f5-b392-4b4f-880f-f8e171107de2","ts":"2026-04-15T15:06:17.200Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} -{"id":"d12f3244-8ce3-44cf-b42d-57344f6a04fb","ts":"2026-04-15T15:06:51.294Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/README.md","version":"1.0.0","taskID":"85f12346-40ba-41c1-92d9-394f8b977ceb"} -{"id":"af971520-f282-4b05-82b9-e9deb9950e19","ts":"2026-04-15T15:33:23.923Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"119c45f7-36d4-426c-8a7f-9fbcc94e49c3","ts":"2026-04-15T15:38:22.443Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vectorize.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"c10b914b-8ae6-44ca-b3e6-3c2fac514833","ts":"2026-04-15T15:38:38.323Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"f6de1171-ad13-4889-b2b6-74edfe2f6e85","ts":"2026-04-15T15:43:22.996Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Lexical.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"fbe14c90-3e84-4b32-a921-ccd4f64b9db5","ts":"2026-04-15T15:43:38.369Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"07bb888c-3c39-4cb4-9c7a-7a35427f60c0","ts":"2026-04-15T15:44:56.544Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/Vector.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"c5019869-c249-4fd1-9cd4-c67245843759","ts":"2026-04-15T15:45:16.825Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"b5639047-f9aa-476d-a66f-0f9546443795","ts":"2026-04-15T15:45:54.657Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"863643ef-a8f9-4f13-a129-de26297bad4f","ts":"2026-04-15T15:49:28.185Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"a54dc518-f6c4-43cd-a44d-0fbdc060ac29","ts":"2026-04-15T15:50:14.871Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/DataSet.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"78412107-2cdd-47d1-8950-d3fbac5c3d5f","ts":"2026-04-15T15:58:52.204Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"64c74f4e-d910-4816-899f-0a7a9b660a01","ts":"2026-04-15T15:59:08.980Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionRecordDefinition.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"31554695-6df5-4273-b89b-b17b64b25c53","ts":"2026-04-15T17:04:20.251Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"ec6a8e85-75a5-44d7-8836-fe92551619f6","ts":"2026-04-15T17:04:32.101Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/SchemaAction.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"4596bd4b-5a88-446b-bb7c-3e2bbe8e84c4","ts":"2026-04-15T17:04:38.380Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"69350581-a1f2-474b-bcd0-30617358038f","ts":"2026-04-15T17:05:01.518Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"694eaad5-abd0-4026-a10f-fad0b2b8d5ce","ts":"2026-04-15T17:19:35.629Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} +{"id":"c46457a2-1a2d-44ef-89ad-a6cd1e119053","ts":"2026-04-16T11:15:06.636Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"eb3e1efd-f4a8-46fd-ae85-dc6b1a9a1230","ts":"2026-04-16T11:15:14.086Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"c1087d22-91b0-404e-98b9-3e474b4f3213","ts":"2026-04-16T11:15:22.349Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"e39724cc-df69-43a9-a419-6138af7e7b3f","ts":"2026-04-16T11:15:34.879Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"722bda18-6c74-4e67-b202-f4d0c7f68516","ts":"2026-04-16T11:15:48.219Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"566386d4-40b1-423a-888a-f5dd60d2ddae","ts":"2026-04-16T11:15:54.022Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"2a3b092c-59a0-4e2e-9524-d301e583f3bd","ts":"2026-04-16T11:16:05.144Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"53bb9919-a9f4-4f89-a91d-7d24b862c2a5","ts":"2026-04-16T11:16:16.029Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"c727eb97-2f4b-43b7-bfe8-7fc9ebd5ef1b","ts":"2026-04-16T11:16:30.223Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"06eb1c48-29c0-49e2-a8d1-9b49891f5803","ts":"2026-04-16T11:16:38.681Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"ae68cae8-e712-4958-bac1-93273e8b541a","ts":"2026-04-16T11:16:45.924Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"48270324-7736-4d50-9785-ffbe89fafa14","ts":"2026-04-16T11:16:51.898Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"75632aa0-993f-44ba-8b25-5289bc32f7bb","ts":"2026-04-16T11:19:42.271Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Book.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"82bbcdd2-1d64-4e5a-aeff-cebfe64ed010","ts":"2026-04-16T11:19:58.352Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"764d2974-32f8-49f9-b89f-9d4284c93f23","ts":"2026-04-16T11:20:17.509Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"77eac65f-de02-4930-b9a7-3266376c9e6b","ts":"2026-04-16T11:26:21.956Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"db9be7a6-6dcd-4c82-b50b-83f3fc151a8a","ts":"2026-04-16T11:26:52.514Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"dc083945-6b27-47c0-a4e5-06145329065b","ts":"2026-04-16T11:50:46.227Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"216b0bfa-6ab6-49c0-b5b3-b3cb9081a528","ts":"2026-04-16T11:51:07.121Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"f29a3189-719e-400e-a9b8-3c3817472412","ts":"2026-04-16T11:51:29.140Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"f464d7a7-bc82-4964-87e5-501aeacdd953","ts":"2026-04-16T11:51:45.420Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"3a5fe801-3f62-4b38-b9f3-7d1e480de616","ts":"2026-04-16T11:51:58.614Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"669b7e5d-ea45-4ff3-90c3-e5f09e05cbac","ts":"2026-04-16T11:52:25.469Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/pom.xml","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"4a9e15dd-f591-4eb6-8b71-2f72b034e761","ts":"2026-04-16T12:30:51.060Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"3eef00d6-0613-4dec-9213-4ec6ca29454a","ts":"2026-04-16T12:32:24.973Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"35f3fd69-851e-4302-a5f6-96f3a75d2343","ts":"2026-04-16T12:32:48.380Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"b9499f4a-3c14-4b03-9d9d-11a24647c2af","ts":"2026-04-16T12:33:07.817Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"c926c73d-4487-4362-b964-18ef2cd0feab","ts":"2026-04-16T12:33:18.445Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"c5a0c13f-eecf-4f07-99c1-8e3e7702d3b4","ts":"2026-04-16T12:33:40.714Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"ee87ec6f-cb03-4acc-b2a8-6e664d93aca3","ts":"2026-04-16T12:41:55.971Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKeyClass.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"18441a47-53a8-49d5-930e-f3c1d96cd009","ts":"2026-04-16T12:42:06.537Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"647388bd-e86d-4bb0-95fc-399b05dcf940","ts":"2026-04-16T12:43:13.525Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"b0c8de3f-b527-4a61-9fda-9594496cbe1c","ts":"2026-04-16T12:43:59.284Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/README_TABLE_REPOSITORY.md","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"d761d550-dfa0-459d-af6e-42610fc17649","ts":"2026-04-16T12:51:26.448Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"93a4798c-5b1e-4dcb-92a3-7dfd99a7d7e3","ts":"2026-04-16T12:51:40.592Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"7911f8e4-9c19-4f89-ac56-b6996aceb0db","ts":"2026-04-16T12:52:14.328Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"cc9c21b1-ebf2-4222-bf64-5b66ad557fdb","ts":"2026-04-16T12:52:26.188Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"c261a008-d2d8-43cd-880b-ef524408d4aa","ts":"2026-04-16T12:52:43.684Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"62e18708-ac34-423d-8bee-49e6893037ff","ts":"2026-04-16T12:52:49.039Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"250dfe52-9ce5-4a42-8d31-ef9c987f3f30","ts":"2026-04-16T12:53:30.033Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"efe636ef-e47b-456f-9839-96ff7e3c4b99","ts":"2026-04-16T12:54:01.893Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"f7203a10-02f4-45dc-b189-989643f720c0","ts":"2026-04-16T12:54:42.799Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"e2a898cf-5e5f-449c-84e1-e426585b476e","ts":"2026-04-16T12:54:54.535Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"6b43ec04-aca0-429f-abea-c902deb0737e","ts":"2026-04-16T12:55:00.094Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"b23fe5c5-f27a-4081-a860-dc0908ca1b32","ts":"2026-04-16T12:55:48.969Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Order.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"73520d00-7bef-4a76-8504-e4c2317ae3ae","ts":"2026-04-16T12:56:57.564Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"58427a01-6d1a-4379-989b-e874dd756911","ts":"2026-04-16T12:57:35.709Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"515eb92f-8d01-4fba-9f3f-83ef103caf13","ts":"2026-04-16T12:57:40.996Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Order.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"c276caf1-0314-4625-8f8c-b4926b5bffba","ts":"2026-04-16T12:58:02.655Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"7b5ab564-35d8-4964-b598-d9d9f2b5bc5c","ts":"2026-04-16T12:58:08.414Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"8e36afcb-70df-4a7c-82e9-74fa4de90b05","ts":"2026-04-16T12:58:26.033Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"38ad9160-5ba3-4cdb-b154-9fb4d7708c1e","ts":"2026-04-16T12:58:46.487Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"fdc960d9-c834-4cd6-89dc-e4aa70ec4391","ts":"2026-04-16T12:59:15.984Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassDemo.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"a003915c-1259-4365-b343-791f2d57252a","ts":"2026-04-16T12:59:46.853Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"aeac3ac3-f12d-43ff-a5a4-f4dfe622b3a6","ts":"2026-04-16T12:59:58.026Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"7418bf02-3ec1-4234-9809-77342452aa44","ts":"2026-04-16T13:00:12.361Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} +{"id":"04305034-4def-44cd-ac00-a98427ea70aa","ts":"2026-04-16T15:03:33.703Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"48bca368-4ce4-4069-aa6c-007fc115c401","ts":"2026-04-16T15:12:20.229Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyDeserializationTest.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"08cb66e0-d5b4-4473-9ffb-39d09330ba91","ts":"2026-04-16T15:12:51.436Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyDeserializationTest.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"463647cc-5f9f-48e2-8391-b56be83f4bdb","ts":"2026-04-16T15:13:55.329Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"5979af38-8b04-4abc-b486-b2c9b9c1a623","ts":"2026-04-16T15:14:34.146Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"30fdd1eb-cdd2-498b-91b3-ca822d8e74cc","ts":"2026-04-16T16:52:39.827Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"3943ce35-7e42-44c0-9737-7218d0ebc39e","ts":"2026-04-16T16:54:49.970Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/tables/definition/TableDefinition.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"f77a3302-0a27-441d-8f34-3c3ce6fcac78","ts":"2026-04-16T16:55:17.594Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/tables/TableDefinitionEqualsTest.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"a1845daa-db62-42d3-9f17-8f1dfdf253a4","ts":"2026-04-16T17:00:30.661Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"ff69dd92-3eb5-4039-ba37-1a0fb224585d","ts":"2026-04-16T17:01:01.508Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"4bfeeb12-ca4b-4289-afa7-74a3c4cad8cc","ts":"2026-04-16T17:01:21.794Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"cec37b99-e5a9-43c1-b143-199c8acbe34f","ts":"2026-04-16T17:17:14.576Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"2229fcfa-3c9d-4060-89a4-1441e7d60a30","ts":"2026-04-16T17:17:27.199Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"c24b6fca-2959-41c0-b374-6cfc75cb02c8","ts":"2026-04-16T17:20:53.390Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"7338119c-99cd-4d60-a8d9-ce030c15c4e2","ts":"2026-04-16T17:25:39.144Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/DatabaseInfoController.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"8ef79ebe-43e2-4282-a03d-70b909df6802","ts":"2026-04-16T17:25:53.150Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"078a7a4d-b4ef-4a38-93d8-442826ed8e6b","ts":"2026-04-16T17:26:15.851Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/DatabaseInfoController.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"db49d022-a3d3-4d82-b21c-9ade55d439e3","ts":"2026-04-16T17:26:55.052Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/README.md","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} +{"id":"a699d28f-0040-412e-a8bd-787c754f9a82","ts":"2026-04-17T11:59:00.638Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} +{"id":"61c1af0b-ca1c-4f20-9bb5-0be3101ccf3c","ts":"2026-04-17T11:59:05.182Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} +{"id":"6443efac-e92a-4c64-a27e-bb66be27c146","ts":"2026-04-17T12:10:05.152Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} +{"id":"4d03836c-4fa4-4783-937f-48bbd08ade0f","ts":"2026-04-17T12:10:30.967Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} +{"id":"92a2f77a-2dc2-4c9e-a12d-1c1290d1c2c7","ts":"2026-04-17T12:11:15.838Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} +{"id":"ea5fdc47-f6fa-4dc8-8f7e-2c490e11cc8f","ts":"2026-04-17T12:13:21.027Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} +{"id":"47f1b4ab-d3e8-44fe-95a0-90ee9ece6e94","ts":"2026-04-17T12:17:24.308Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} +{"id":"fecd24c0-a581-44cc-8745-3d3284967a99","ts":"2026-04-17T12:22:10.879Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} +{"id":"50a4ef32-9da7-4852-9f2f-7f365719039f","ts":"2026-04-17T13:13:14.062Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"8480e585-16dd-4e3f-8d11-3b4ff31ef32d","ts":"2026-04-17T13:13:42.087Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"263b9fe3-a1db-48c9-a8db-591a0f8aaa22","ts":"2026-04-17T13:14:56.542Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"11faa163-7a35-4ee4-8e45-367721484848","ts":"2026-04-17T13:18:31.584Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/HttpClientWrapper.java","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"7ca1502a-75f0-4567-939a-e759ec9aba9c","ts":"2026-04-17T13:19:00.643Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/observability/ApiExecutionInfos.java","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"87f5771a-422a-4aaa-a9f3-7937c567ee39","ts":"2026-04-17T13:19:07.829Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"b578777d-ad9f-4412-8e58-a69bbad1401e","ts":"2026-04-17T13:19:41.290Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/TestUtils.java","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"c25f2dd5-4356-4365-83cb-0cddc6c791d9","ts":"2026-04-17T13:20:44.475Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"714f5165-5ec3-4260-8748-dd2cbc60b3d6","ts":"2026-04-17T13:21:02.677Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"b013d198-3d3b-4056-9512-d434d5e58de7","ts":"2026-04-17T13:23:43.279Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/CdcClientTest.java","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} +{"id":"956bd2a1-edaa-4d25-899a-2c0e45ca79ea","ts":"2026-04-17T13:32:08.857Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/pom.xml","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"31aeba2c-0298-41f5-91b1-2680c88a9264","ts":"2026-04-17T13:32:17.201Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"e86ca3da-30f1-413a-a4fc-999e24e91f74","ts":"2026-04-17T13:32:29.038Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"88d9c020-c2d8-4a4e-87c7-ab82862646bb","ts":"2026-04-17T13:32:39.226Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"8c26b9c4-1a61-41aa-ac93-88aaaa762c1f","ts":"2026-04-17T13:32:57.317Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/DatabaseInfoController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"c1897810-7087-4dc8-a1dc-6b1110e76670","ts":"2026-04-17T13:34:25.654Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/DatabaseInfoController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"7ecc8260-a5bf-4c01-b843-ca459140813e","ts":"2026-04-17T13:34:42.669Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"5161c44a-e7c3-45dc-9510-a7ba529fb119","ts":"2026-04-17T13:39:50.039Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"19745f68-7f14-4c0b-b7db-1642c06a986a","ts":"2026-04-17T13:40:49.612Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/application.yaml","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"280106ec-575e-4ea4-9d34-111832b03a48","ts":"2026-04-17T13:42:18.124Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"a6e098cd-f84c-4ea1-95f9-a79c1718262d","ts":"2026-04-17T13:45:31.055Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/banner.txt","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"ad0fa13a-477e-46e6-9b77-fc39df3b0e0f","ts":"2026-04-17T13:53:16.814Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"73a9e1eb-132f-4fcb-9677-27d6519dc5e1","ts":"2026-04-17T14:03:00.644Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"9996940e-7b29-4e39-8f83-fe856f42da57","ts":"2026-04-17T14:03:06.720Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"9939310d-178d-4232-ac7d-415b7b9d8d60","ts":"2026-04-17T14:06:18.675Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"8d62b81e-3fdb-423d-a9af-2adc5697ca08","ts":"2026-04-17T14:06:33.241Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"cd009b14-44af-4cc2-bdb1-4fc2bfed50be","ts":"2026-04-17T14:11:14.403Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"f9559078-6a7b-473e-b319-413810ae6644","ts":"2026-04-17T14:25:45.649Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookVectorSearchRequest.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} +{"id":"dcb595ed-ca97-4992-bcbe-ab47f9e43855","ts":"2026-04-17T14:42:21.809Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java","version":"1.0.0","taskID":"e52187da-c542-42a0-9c51-3c63c37571f1"} diff --git a/AGENT.md b/AGENT.md index 93bb956e..4d8a527a 100644 --- a/AGENT.md +++ b/AGENT.md @@ -264,7 +264,7 @@ Files in `astra-db-java/src/test/resources/`: | `junit-platform.properties` | JUnit 5 config (ordered execution, sequential) | | `logback-test.xml` | Test logging | -**Priority order:** Environment variables > System properties > Config files. +**Priority orderBean:** Environment variables > System properties > Config files. ### Custom test annotations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e976fb99..ed1267b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,11 +97,11 @@ mvn clean install -DskipTests ### Test Configuration System -Tests are configured through a layered properties system. Values are resolved in priority order: +Tests are configured through a layered properties system. Values are resolved in priority orderBean: 1. **Environment variables** (e.g., `ASTRA_DB_APPLICATION_TOKEN`) 2. **System properties** (e.g., `-Dastra.token=...`) -3. **Config files** (loaded in order, later files override earlier ones): +3. **Config files** (loaded in orderBean, later files override earlier ones): - `test-config.properties` — defaults (committed) - `test-config-local.properties` — local HCD/DSE settings (committed) - `test-config-astra.properties` — Astra credentials (**gitignored**) diff --git a/README_TABLE_REPOSITORY.md b/README_TABLE_REPOSITORY.md new file mode 100644 index 00000000..6f367808 --- /dev/null +++ b/README_TABLE_REPOSITORY.md @@ -0,0 +1,333 @@ +# DataApiTableCrudRepository Usage Guide + +This guide demonstrates the three patterns for using `DataApiTableCrudRepository` with Astra DB Tables in Spring Boot applications. + +## Pattern 1: Single Partition Key + +Use when your table has a single partition key column. + +### Entity Definition +```java +import com.datastax.astra.client.tables.mapping.Column; +import com.datastax.astra.client.tables.mapping.EntityTable; +import com.datastax.astra.client.tables.mapping.PartitionBy; +import java.util.UUID; + +@EntityTable("users") +public class User { + @PartitionBy(1) + @Column("user_id") + private UUID userId; + + @Column("name") + private String name; + + @Column("email") + private String email; + + // Constructors, getters, setters +} +``` + +### Repository Definition +```java +import com.datastax.astra.spring.DataApiTableCrudRepository; +import org.springframework.stereotype.Repository; +import java.util.UUID; + +@Repository +public interface UserRepository extends DataApiTableCrudRepository { +} +``` + +### Usage +```java +@Service +public class UserService { + @Autowired + private UserRepository userRepository; + + public void example() { + // Create + User user = new User(UUID.randomUUID(), "John Doe", "john@example.com"); + userRepository.save(user); + + // Read + UUID userId = user.getUserId(); + Optional found = userRepository.findById(userId); + + // Delete + userRepository.deleteById(userId); + } +} +``` + +## Pattern 2: Composite Key with Map + +Use when your table has multiple partition keys or clustering columns, and you prefer a flexible Map-based approach. + +### Entity Definition +```java +import com.datastax.astra.client.tables.mapping.Column; +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.PartitionSortOrder; +import java.math.BigDecimal; +import java.time.LocalDate; + +@EntityTable("orders") +public class Order { + @PartitionBy(1) + @Column("customer_id") + private String customerId; + + @PartitionSort(position = 1, orderBean = PartitionSortOrder.ASC) + @Column("order_date") + private LocalDate orderDate; + + @Column("order_id") + private String orderId; + + @Column("amount") + private BigDecimal amount; + + // Constructors, getters, setters +} +``` + +### Repository Definition +```java +import com.datastax.astra.spring.DataApiTableCrudRepository; +import org.springframework.stereotype.Repository; +import java.util.Map; + +@Repository +public interface OrderRepository extends DataApiTableCrudRepository> { +} +``` + +### Usage +```java +@Service +public class OrderService { + @Autowired + private OrderRepository orderRepository; + + public void example() { + // Create + Order orderBean = new Order("CUST123", LocalDate.now(), "ORD001", new BigDecimal("99.99")); + orderRepository.save(orderBean); + + // Read - construct primary key as Map + Map primaryKey = Map.of( + "customer_id", "CUST123", + "order_date", LocalDate.now() + ); + Optional found = orderRepository.findById(primaryKey); + + // Delete + orderRepository.deleteById(primaryKey); + } +} +``` + +## Pattern 3: Composite Key with @TablePrimaryKeyClass (Recommended) + +Use when your table has multiple partition keys or clustering columns, and you want type-safe, object-oriented primary key handling. This pattern is similar to Spring Data Cassandra's `@PrimaryKeyClass`. + +### Primary Key Class Definition +```java +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.PartitionSortOrder; +import com.datastax.astra.client.tables.mapping.TablePrimaryKeyClass; +import java.time.LocalDate; +import java.util.Objects; + +@TablePrimaryKeyClass +public class OrderKey { + @PartitionBy(1) + @Column("customer_id") + private String customerId; + + @PartitionSort(position = 1, orderBean = PartitionSortOrder.ASC) + @Column("order_date") + private LocalDate orderDate; + + // Default constructor + public OrderKey() {} + + // Constructor + public OrderKey(String customerId, LocalDate orderDate) { + this.customerId = customerId; + this.orderDate = orderDate; + } + + // Getters and setters + 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; } + + // equals and hashCode are REQUIRED for proper key comparison + @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 + "}"; + } +} +``` + +### Entity Definition +```java +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; + +@EntityTable("orders") +public class Order { + @TablePrimaryKey + private OrderKey key; + + @Column("order_id") + private String orderId; + + @Column("amount") + private BigDecimal amount; + + @Column("status") + private String status; + + // Constructors + public Order() {} + + public Order(OrderKey key, String orderId, BigDecimal amount, String status) { + this.key = key; + this.orderId = orderId; + this.amount = amount; + this.status = status; + } + + // Getters and setters + 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; } +} +``` + +### Repository Definition +```java +import com.datastax.astra.spring.DataApiTableCrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface OrderRepository extends DataApiTableCrudRepository { +} +``` + +### Usage +```java +@Service +public class OrderService { + @Autowired + private OrderRepository orderRepository; + + public void example() { + // Create - type-safe primary key + OrderKey key = new OrderKey("CUST123", LocalDate.now()); + Order orderBean = new Order(key, "ORD001", new BigDecimal("99.99"), "PENDING"); + orderRepository.save(orderBean); + + // Read - type-safe lookup + Optional found = orderRepository.findById(key); + + // Update + found.ifPresent(o -> { + o.setStatus("COMPLETED"); + orderRepository.save(o); + }); + + // Delete - type-safe deletion + orderRepository.deleteById(key); + + // Delete by entity (extracts key automatically) + orderRepository.delete(orderBean); + } +} +``` + +## Comparison of Patterns + +| Feature | Single Key | Map Key | @TablePrimaryKeyClass | +|---------|-----------|---------|----------------------| +| Type Safety | ✅ High | ❌ Low | ✅ High | +| Readability | ✅ Excellent | ⚠️ Fair | ✅ Excellent | +| Refactoring | ✅ Easy | ❌ Difficult | ✅ Easy | +| IDE Support | ✅ Full | ⚠️ Limited | ✅ Full | +| Boilerplate | ✅ Minimal | ✅ Minimal | ⚠️ Moderate | +| Best For | Single partition key | Quick prototypes | Production code with composite keys | + +## Recommendations + +1. **Use Pattern 1** for tables with a single partition key +2. **Use Pattern 3** (@TablePrimaryKeyClass) for tables with composite keys in production code +3. **Use Pattern 2** (Map) only for quick prototypes or when key structure is highly dynamic + +## Spring Boot Configuration + +Add to your `application.yml`: + +```yaml +astra: + data-api: + token: ${ASTRA_DB_TOKEN} + endpoint-url: ${ASTRA_DB_ENDPOINT} + keyspace: ${ASTRA_DB_KEYSPACE:default_keyspace} +``` + +## Additional Features + +All patterns support the full `CrudRepository` interface: + +```java +// Batch operations +List orders = Arrays.asList(order1, order2, order3); +orderRepository.saveAll(orders); + +// Existence check +boolean exists = orderRepository.existsById(key); + +// Count +long count = orderRepository.count(); + +// Find all +Iterable allOrders = orderRepository.findAll(); + +// Delete all +orderRepository.deleteAll(); +``` diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java index 10ab8df3..acebc62a 100644 --- a/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java +++ b/astra-db-java/src/main/java/com/datastax/astra/client/collections/mapping/DataApiCollection.java @@ -72,7 +72,7 @@ * * @return the collection name */ - String value() default ""; + String name() default ""; // --------------------- // DefaultId Options @@ -164,7 +164,7 @@ * * @return true to enable lexical search, false to disable */ - boolean lexicalEnabled() default true; + boolean lexicalEnabled() default false; /** * Analyzer type for lexical search. diff --git a/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java b/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java index c4c889eb..4aa72836 100644 --- a/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java +++ b/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java @@ -33,6 +33,7 @@ import com.datastax.astra.client.collections.definition.CollectionDefinition; import com.datastax.astra.client.collections.definition.CollectionDescriptor; import com.datastax.astra.client.collections.definition.documents.Document; +import com.datastax.astra.client.collections.mapping.DataApiCollection; import com.datastax.astra.client.core.commands.Command; import com.datastax.astra.client.core.options.DataAPIClientOptions; import com.datastax.astra.client.databases.definition.DatabaseInfo; @@ -59,7 +60,8 @@ import com.datastax.astra.internal.api.AstraApiEndpoint; import com.datastax.astra.internal.command.AbstractCommandRunner; import com.datastax.astra.internal.command.CommandObserver; -import com.datastax.astra.internal.reflection.EntityBeanDefinition; +import com.datastax.astra.internal.reflection.CollectionBeanDefinition; +import com.datastax.astra.internal.reflection.EntityTableBeanDefinition; import com.datastax.astra.internal.utils.Assert; import com.dtsx.astra.sdk.utils.Utils; import lombok.Getter; @@ -69,7 +71,7 @@ import java.util.Map; import java.util.UUID; -import static com.datastax.astra.internal.reflection.EntityBeanDefinition.createTableCommand; +import static com.datastax.astra.internal.reflection.EntityTableBeanDefinition.createTableCommand; import static com.datastax.astra.internal.utils.Assert.hasLength; import static com.datastax.astra.internal.utils.Assert.notNull; @@ -639,6 +641,85 @@ public Collection getCollection(String collectionName) { return getCollection(collectionName, Document.class); } + /** + * Retrieves the collection name from a class annotated with {@link DataApiCollection}. + *

+ * This method extracts the collection name from the {@code name} attribute of the + * {@link DataApiCollection} 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 document class + * @param documentClass the class representing the document type; must not be null and must be + * annotated with {@link DataApiCollection} + * @return the collection name as specified in the {@link DataApiCollection} annotation + * @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 {
+     *     // fields
+     * }
+     *
+     * String collectionName = database.getCollectionName(User.class);
+     * // Returns: "users"
+     * }
+     * 
+ */ + public String getCollectionName(Class documentClass) { + notNull(documentClass, "documentClass"); + DataApiCollection ann = documentClass.getAnnotation(DataApiCollection.class); + if (ann == null) { + throw new IllegalArgumentException("Class " + documentClass.getName() + " is not annotated with "+ DataApiCollection.class.getName()); + } + if (!Utils.hasLength(ann.name())) { + throw new IllegalArgumentException("Annotation @DataApiCollection on class " + documentClass.getName() + " has no name"); + } + return ann.name(); + } + + /** + * Retrieves the collection definition from a class annotated with {@link DataApiCollection}. + *

+ * This method builds a {@link CollectionDefinition} based on the metadata extracted from + * the provided class. The class must be annotated with {@link DataApiCollection}, and the + * method uses reflection to analyze the class structure and generate the appropriate + * collection schema definition. + *

+ * + * @param documentClass the class representing the document type; must not be null and must be + * annotated with {@link DataApiCollection} + * @return a {@link CollectionDefinition} object representing the schema definition for the collection + * @throws IllegalArgumentException if the class is not annotated with {@link DataApiCollection} + * @throws NullPointerException if {@code documentClass} is null + * + *

Example usage:

+ *
+     * {@code
+     * @DataApiCollection(name = "products")
+     * public class Product {
+     *     private String id;
+     *     private String name;
+     *     // other fields
+     * }
+     *
+     * CollectionDefinition definition = database.getCollectionDefinition(Product.class);
+     * }
+     * 
+ */ + public CollectionDefinition getCollectionDefinition(Class documentClass) { + notNull(documentClass, "documentClass"); + DataApiCollection ann = documentClass.getAnnotation(DataApiCollection.class); + if (ann == null) { + throw new IllegalArgumentException("Class " + documentClass.getName() + " is not annotated with "+ DataApiCollection.class.getName()); + } + return new CollectionBeanDefinition<>(documentClass).buildCollectionDefinition(); + } + /** * Retrieves a {@link Collection} object for the specified collection name, with the ability to * customize the collection behavior using the specified {@link CollectionOptions}. @@ -677,6 +758,40 @@ public Collection getCollection(String collectionName, Class documentC return getCollection(collectionName, documentClass, defaultCollectionOptions()); } + /** + * Retrieves a {@link Collection} object for a class annotated with {@link DataApiCollection}. + *

+ * This method provides a convenient way to obtain a {@link Collection} instance by using + * the collection name extracted from the {@link DataApiCollection} annotation on the provided + * class. The collection is configured 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 a {@link Collection} object representing the collection associated with the annotated class + * @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.getCollection(User.class);
+     * }
+     * 
+ */ + public Collection getCollection(Class 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
- org.apache.httpcomponents.client5 - httpclient5 - - org.projectlombok lombok diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/HttpClientWrapper.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/HttpClientWrapper.java index c6ebe214..24509fd0 100644 --- a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/HttpClientWrapper.java +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/HttpClientWrapper.java @@ -4,32 +4,16 @@ import com.dtsx.astra.sdk.utils.observability.ApiExecutionInfos; import com.dtsx.astra.sdk.utils.observability.ApiRequestObserver; import com.dtsx.astra.sdk.utils.observability.CompletableFutures; -import org.apache.hc.client5.http.auth.StandardAuthScheme; -import org.apache.hc.client5.http.classic.methods.HttpDelete; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.classic.methods.HttpHead; -import org.apache.hc.client5.http.classic.methods.HttpPatch; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.classic.methods.HttpPut; -import org.apache.hc.client5.http.classic.methods.HttpTrace; -import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; -import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.cookie.StandardCookieSpec; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.Method; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.apache.hc.core5.http.io.entity.StringEntity; -import org.apache.hc.core5.util.TimeValue; -import org.apache.hc.core5.util.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.HttpURLConnection; -import java.util.Arrays; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; @@ -39,7 +23,7 @@ import java.util.stream.Collectors; /** - * Helper to forge Http Requests to interact with Devops API. + * Helper to forge Http Requests to interact with Devops API using JDK11+ HttpClient. */ public class HttpClientWrapper { @@ -82,8 +66,8 @@ public class HttpClientWrapper { /** Singleton pattern. */ private static HttpClientWrapper _instance = null; - /** HttpComponent5. */ - protected CloseableHttpClient httpClient = null; + /** JDK11+ HttpClient. */ + protected HttpClient httpClient = null; /** Observers. */ protected static Map observers = new LinkedHashMap<>(); @@ -91,15 +75,6 @@ public class HttpClientWrapper { /** Observers. */ protected String operationName= "n/a"; - /** Default request configuration. */ - protected static RequestConfig requestConfig = RequestConfig.custom() - .setCookieSpec(StandardCookieSpec.STRICT) - .setExpectContinueEnabled(true) - .setConnectionRequestTimeout(Timeout.ofSeconds(DEFAULT_TIMEOUT_REQUEST)) - .setConnectTimeout(Timeout.ofSeconds(DEFAULT_TIMEOUT_CONNECT)) - .setTargetPreferredAuthSchemes(Arrays.asList(StandardAuthScheme.NTLM, StandardAuthScheme.DIGEST)) - .build(); - // ------------------------------------------- // ----------------- Singleton --------------- // ------------------------------------------- @@ -118,11 +93,11 @@ private HttpClientWrapper() {} private static synchronized HttpClientWrapper getInstance() { if (_instance == null) { _instance = new HttpClientWrapper(); - final PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); - connManager.setValidateAfterInactivity(TimeValue.ofSeconds(10)); - connManager.setMaxTotal(100); - connManager.setDefaultMaxPerRoute(10); - _instance.httpClient = HttpClients.custom().setConnectionManager(connManager).build(); + _instance.httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(DEFAULT_TIMEOUT_CONNECT)) + .build(); } return _instance; } @@ -138,11 +113,11 @@ private static synchronized HttpClientWrapper getInstance() { public static synchronized HttpClientWrapper getInstance(String operation) { if (_instance == null) { _instance = new HttpClientWrapper(); - final PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); - connManager.setValidateAfterInactivity(TimeValue.ofSeconds(10)); - connManager.setMaxTotal(100); - connManager.setDefaultMaxPerRoute(10); - _instance.httpClient = HttpClients.custom().setConnectionManager(connManager).build(); + _instance.httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(DEFAULT_TIMEOUT_CONNECT)) + .build(); } _instance.operationName = operation; return _instance; @@ -163,7 +138,7 @@ public static synchronized HttpClientWrapper getInstance(String operation) { * http request */ public ApiResponseHttp GET(String url, String token) { - return executeHttp(Method.GET, url, token, null, CONTENT_TYPE_JSON, false); + return executeHttp("GET", url, token, null, CONTENT_TYPE_JSON, false); } /** @@ -181,9 +156,9 @@ public ApiResponseHttp GET(String url, String token) { * http request */ public ApiResponseHttp GET_PULSAR(String url, String token, String pulsarCluster, String organizationId) { - HttpUriRequestBase request = buildRequest(Method.GET, url, token, null, CONTENT_TYPE_JSON); - updatePulsarHttpRequest(request, token, pulsarCluster, organizationId); - return executeHttp(request, false); + HttpRequest.Builder requestBuilder = buildRequest("GET", url, token, null, CONTENT_TYPE_JSON); + updatePulsarHttpRequest(requestBuilder, token, pulsarCluster, organizationId); + return executeHttp(requestBuilder.build(), false); } /** @@ -203,9 +178,9 @@ public ApiResponseHttp GET_PULSAR(String url, String token, String pulsarCluster * http request */ public ApiResponseHttp POST_PULSAR(String url, String token, String body, String pulsarCluster, String organizationId) { - HttpUriRequestBase request = buildRequest(Method.POST, url, token, body, CONTENT_TYPE_JSON); - updatePulsarHttpRequest(request, token, pulsarCluster, organizationId); - return executeHttp(request, false); + HttpRequest.Builder requestBuilder = buildRequest("POST", url, token, body, CONTENT_TYPE_JSON); + updatePulsarHttpRequest(requestBuilder, token, pulsarCluster, organizationId); + return executeHttp(requestBuilder.build(), false); } /** @@ -225,16 +200,16 @@ public ApiResponseHttp POST_PULSAR(String url, String token, String body, String * http request */ public ApiResponseHttp DELETE_PULSAR(String url, String token, String body, String pulsarCluster, String organizationId) { - HttpUriRequestBase request = buildRequest(Method.DELETE, url, token, body, CONTENT_TYPE_JSON); - updatePulsarHttpRequest(request, token, pulsarCluster, organizationId); - return executeHttp(request, false); + HttpRequest.Builder requestBuilder = buildRequest("DELETE", url, token, body, CONTENT_TYPE_JSON); + updatePulsarHttpRequest(requestBuilder, token, pulsarCluster, organizationId); + return executeHttp(requestBuilder.build(), false); } /** * Add item for a pulsar request. * - * @param request - * current request + * @param requestBuilder + * current request builder * @param pulsarToken * pulsar token * @param pulsarCluster @@ -242,10 +217,10 @@ public ApiResponseHttp DELETE_PULSAR(String url, String token, String body, Stri * @param organizationId * organization */ - private void updatePulsarHttpRequest(HttpUriRequestBase request, String pulsarToken, String pulsarCluster, String organizationId) { - request.addHeader(HEADER_AUTHORIZATION, "Bearer " + pulsarToken); - request.addHeader(HEADER_CURRENT_ORG, organizationId); - request.addHeader(HEADER_CURRENT_PULSAR_CLUSTER, pulsarCluster); + private void updatePulsarHttpRequest(HttpRequest.Builder requestBuilder, String pulsarToken, String pulsarCluster, String organizationId) { + requestBuilder.header(HEADER_AUTHORIZATION, "Bearer " + pulsarToken); + requestBuilder.header(HEADER_CURRENT_ORG, organizationId); + requestBuilder.header(HEADER_CURRENT_PULSAR_CLUSTER, pulsarCluster); } /** @@ -259,7 +234,7 @@ private void updatePulsarHttpRequest(HttpUriRequestBase request, String pulsarTo * http request */ public ApiResponseHttp HEAD(String url, String token) { - return executeHttp(Method.HEAD, url, token, null, CONTENT_TYPE_JSON, false); + return executeHttp("HEAD", url, token, null, CONTENT_TYPE_JSON, false); } /** @@ -273,7 +248,7 @@ public ApiResponseHttp HEAD(String url, String token) { * http request */ public ApiResponseHttp POST(String url, String token) { - return executeHttp(Method.POST, url, token, null, CONTENT_TYPE_JSON, true); + return executeHttp("POST", url, token, null, CONTENT_TYPE_JSON, true); } /** @@ -289,7 +264,7 @@ public ApiResponseHttp POST(String url, String token) { * http request */ public ApiResponseHttp POST(String url, String token, String body) { - return executeHttp(Method.POST, url, token, body, CONTENT_TYPE_JSON, true); + return executeHttp("POST", url, token, body, CONTENT_TYPE_JSON, true); } /** @@ -301,7 +276,7 @@ public ApiResponseHttp POST(String url, String token, String body) { * authentication token */ public void DELETE(String url, String token) { - executeHttp(Method.DELETE, url, token, null, CONTENT_TYPE_JSON, true); + executeHttp("DELETE", url, token, null, CONTENT_TYPE_JSON, true); } /** @@ -315,7 +290,7 @@ public void DELETE(String url, String token) { * request body */ public void PUT(String url, String token, String body) { - executeHttp(Method.PUT, url, token, body, CONTENT_TYPE_JSON, false); + executeHttp("PUT", url, token, body, CONTENT_TYPE_JSON, false); } /** @@ -329,7 +304,7 @@ public void PUT(String url, String token, String body) { * request body */ public void PATCH(String url, String token, String body) { - executeHttp(Method.PATCH, url, token, body, CONTENT_TYPE_JSON, false); + executeHttp("PATCH", url, token, body, CONTENT_TYPE_JSON, false); } /** @@ -350,8 +325,9 @@ public void PATCH(String url, String token, String body) { * @return * basic request */ - public ApiResponseHttp executeHttp(final Method method, final String url, final String token, String reqBody, String contentType, boolean mandatory) { - return executeHttp(buildRequest(method, url, token, reqBody, contentType), mandatory); + public ApiResponseHttp executeHttp(final String method, final String url, final String token, String reqBody, String contentType, boolean mandatory) { + HttpRequest request = buildRequest(method, url, token, reqBody, contentType).build(); + return executeHttp(request, mandatory); } /** @@ -364,28 +340,28 @@ public ApiResponseHttp executeHttp(final Method method, final String url, final * @return * api response */ - public ApiResponseHttp executeHttp(HttpUriRequestBase req, boolean mandatory) { + public ApiResponseHttp executeHttp(HttpRequest req, boolean mandatory) { // Execution Infos ApiExecutionInfos.ApiExecutionInfoBuilder executionInfo = ApiExecutionInfos.builder() .withOperationName(operationName) .withHttpRequest(req); - try(CloseableHttpResponse response = httpClient.execute(req)) { + try { + HttpResponse response = httpClient.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + ApiResponseHttp res; if (response == null) { res = new ApiResponseHttp("Response is empty, please check url", HttpURLConnection.HTTP_UNAVAILABLE, null); } else { // Mapping response - String body = null; - if (null != response.getEntity()) { - body = EntityUtils.toString(response.getEntity()); - EntityUtils.consume(response.getEntity()); - } - Map headers = new HashMap<>(); - Arrays.stream(response.getHeaders()).forEach(h -> headers.put(h.getName(), h.getValue())); - res = new ApiResponseHttp(body, response.getCode(), headers); + String body = response.body(); + Map headers = new HashMap<>(); + response.headers().map().forEach((key, values) -> + headers.put(key, String.join(", ", values)) + ); + res = new ApiResponseHttp(body, response.statusCode(), headers); } // Error management @@ -393,12 +369,9 @@ public ApiResponseHttp executeHttp(HttpUriRequestBase req, boolean mandatory) { return res; } if (res.getCode() >= 300) { - String entity = "n/a"; - if (req.getEntity() != null) { - entity = EntityUtils.toString(req.getEntity()); - } + String entity = req.bodyPublisher().map(bp -> "body present").orElse("n/a"); LOGGER.error("Error for request, url={}, method={}, body={}", - req.getUri().toString(), req.getMethod(), entity); + req.uri().toString(), req.method(), entity); LOGGER.error("Response code={}, body={}", res.getCode(), res.getBody()); processErrors(res, mandatory); LOGGER.error("An HTTP Error occurred. The HTTP CODE Return is {}", res.getCode()); @@ -425,39 +398,62 @@ public ApiResponseHttp executeHttp(HttpUriRequestBase req, boolean mandatory) { * target URL * @param token * current token + * @param body + * request body + * @param contentType + * content type * @return - * default http with header + * default http request builder with headers */ - private HttpUriRequestBase buildRequest(final Method method, final String url, final String token, String body, String contentType) { - HttpUriRequestBase req; - switch(method) { - case GET: req = new HttpGet(url); break; - case POST: req = new HttpPost(url); break; - case PUT: req = new HttpPut(url); break; - case DELETE: req = new HttpDelete(url); break; - case PATCH: req = new HttpPatch(url); break; - case HEAD: req = new HttpHead(url); break; - case TRACE: req = new HttpTrace(url); break; - case OPTIONS: - case CONNECT: - default:throw new IllegalArgumentException("Invalid HTTP Method"); - } - req.addHeader(HEADER_CONTENT_TYPE, contentType); - req.addHeader(HEADER_ACCEPT, CONTENT_TYPE_JSON); - req.addHeader(HEADER_USER_AGENT, REQUEST_WITH); - req.addHeader(HEADER_REQUESTED_WITH, REQUEST_WITH); - req.addHeader(HEADER_AUTHORIZATION, "Bearer " + token); - req.setConfig(requestConfig); - if (null != body) { - req.setEntity(new StringEntity(body, ContentType.TEXT_PLAIN)); + private HttpRequest.Builder buildRequest(final String method, final String url, final String token, String body, String contentType) { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(DEFAULT_TIMEOUT_REQUEST)) + .header(HEADER_CONTENT_TYPE, contentType) + .header(HEADER_ACCEPT, CONTENT_TYPE_JSON) + .header(HEADER_USER_AGENT, REQUEST_WITH) + .header(HEADER_REQUESTED_WITH, REQUEST_WITH) + .header(HEADER_AUTHORIZATION, "Bearer " + token); + + // Set method and body + HttpRequest.BodyPublisher bodyPublisher = body != null + ? HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8) + : HttpRequest.BodyPublishers.noBody(); + + switch(method.toUpperCase()) { + case "GET": + builder.GET(); + break; + case "POST": + builder.POST(bodyPublisher); + break; + case "PUT": + builder.PUT(bodyPublisher); + break; + case "DELETE": + builder.method("DELETE", bodyPublisher); + break; + case "PATCH": + builder.method("PATCH", bodyPublisher); + break; + case "HEAD": + builder.method("HEAD", HttpRequest.BodyPublishers.noBody()); + break; + case "TRACE": + builder.method("TRACE", HttpRequest.BodyPublishers.noBody()); + break; + default: + throw new IllegalArgumentException("Invalid HTTP Method: " + method); } - return req; + + return builder; } /** * Process ERRORS.Anything above code 300 can be marked as an error Still something * 404 is expected and should not result in throwing exception (=not find) * @param res HttpResponse + * @param mandatory mandatory flag */ private void processErrors(ApiResponseHttp res, boolean mandatory) { String body = res.getBody(); @@ -568,4 +564,4 @@ private void notifyASync(Consumer lambda, Collection> requestHttpHeaders; /** - * HTTP Request in + * HTTP Request method */ - private final Method requestHttpMethod; + private final String requestHttpMethod; /** * Request URL @@ -124,7 +120,7 @@ public static ApiExecutionInfoBuilder builder() { public static class ApiExecutionInfoBuilder { private String operationName; private Object payload; - private Method requestHttpMethod; + private String requestHttpMethod; private ApiResponse response; private long executionTime; private int responseHttpCode; @@ -175,19 +171,12 @@ public ApiExecutionInfoBuilder withOperationName(String operationName) { * @return * current reference */ - public ApiExecutionInfoBuilder withHttpRequest(HttpUriRequestBase req) { - this.requestHttpMethod = Method.valueOf(req.getMethod()); - this.requestHttpHeaders = Arrays.stream(req.getHeaders()).collect - (Collectors.toMap(NameValuePair::getName, - h -> Collections.singletonList(h.getValue()))); - try { - this.requestUrl = req.getUri().toString(); - } catch (Exception e) {} - if (req.getEntity() != null) { - try { - this.payload = EntityUtils.toString(req.getEntity()); - } catch (Exception e) {} - } + public ApiExecutionInfoBuilder withHttpRequest(HttpRequest req) { + this.requestHttpMethod = req.method(); + this.requestHttpHeaders = req.headers().map(); + this.requestUrl = req.uri().toString(); + // Note: HttpRequest doesn't provide direct access to body content after creation + // Body content would need to be tracked separately if needed return this; } @@ -216,4 +205,4 @@ public ApiExecutionInfos build() { } -} +} \ No newline at end of file diff --git a/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/CdcClientTest.java b/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/CdcClientTest.java index a3095e1c..0dc9c1ea 100644 --- a/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/CdcClientTest.java +++ b/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/CdcClientTest.java @@ -6,12 +6,6 @@ import com.dtsx.astra.sdk.streaming.domain.CdcDefinition; import com.dtsx.astra.sdk.streaming.domain.CreateTenant; import com.dtsx.astra.sdk.utils.TestUtils; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.io.entity.StringEntity; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; @@ -21,6 +15,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Optional; /** @@ -60,7 +60,13 @@ public void shouldCreateDbAndTenant() throws Exception { Database db = dc.get(); LOGGER.info("+ Using db id={}, region={}", db.getId(), db.getInfo().getRegion()); TestUtils.waitForDbStatus(dc, DatabaseStatusType.ACTIVE, 500); - // Create Table + + // Create Table using JDK HttpClient + HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.ofSeconds(20)) + .build(); + String urlCreateTable = "https://" + dc.get().getId() + "-" + dc.get().getInfo().getRegion() @@ -81,16 +87,31 @@ public void shouldCreateDbAndTenant() throws Exception { " \"primaryKey\": { \"partitionKey\":[\"col1\"] }," + " \"tableOptions\": { \"defaultTimeToLive\":0 }" + " }"; - HttpUriRequestBase req = new HttpPost(urlCreateTable); - req.addHeader("Content-Type", "application/json"); - req.addHeader("Accept", "application/json"); - req.addHeader("X-Cassandra-Token", getToken()); - req.setEntity(new StringEntity(body, ContentType.TEXT_PLAIN)); - CloseableHttpResponse res = HttpClients.createDefault().execute(req); - LOGGER.info("+ Table creation status={}", res.getCode()); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(urlCreateTable)) + .timeout(Duration.ofSeconds(20)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("X-Cassandra-Token", getToken()) + .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + LOGGER.info("+ Table creation status={}", response.statusCode()); + body = body.replaceAll("table1", "table2"); - req.setEntity(new StringEntity(body, ContentType.TEXT_PLAIN)); - LOGGER.info("+ Table creation status={}", HttpClients.createDefault().execute(req).getCode()); + request = HttpRequest.newBuilder() + .uri(URI.create(urlCreateTable)) + .timeout(Duration.ofSeconds(20)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("X-Cassandra-Token", getToken()) + .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) + .build(); + + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + LOGGER.info("+ Table creation status={}", response.statusCode()); } @Test @@ -190,4 +211,4 @@ public void shouldDeleteCdcWithTenant() { } -} +} \ No newline at end of file diff --git a/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/DatabasesClientTest.java b/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/DatabasesClientTest.java index 77898b07..84d36b09 100644 --- a/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/DatabasesClientTest.java +++ b/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/DatabasesClientTest.java @@ -52,7 +52,7 @@ public void shouldCreateServerlessDb() { Assertions.assertNotNull(getDatabasesClient().database(dbId).get()); Assertions.assertTrue(getDatabasesClient().findByName(SDK_TEST_DB_NAME).count() > 0); // When - TestUtils.waitForDbStatus(getDatabasesClient().database(dbId), DatabaseStatusType.ACTIVE, 500); + TestUtils.waitForDbStatus(getDatabasesClient().database(dbId), DatabaseStatusType.ACTIVE, 5000); // Then Database db = getDatabasesClient().database(dbId).get(); Assertions.assertEquals(DatabaseStatusType.ACTIVE, db.getStatus()); diff --git a/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml b/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml index d0f5fe6a..4f992594 100644 --- a/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml +++ b/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml @@ -65,4 +65,4 @@
- + \ No newline at end of file diff --git a/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java index 51b44683..c448a2d3 100644 --- a/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java +++ b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java @@ -1,6 +1,7 @@ package com.datastax.astra.boot.autoconfigure; import com.datastax.astra.client.DataAPIClient; +import com.datastax.astra.client.admin.DatabaseAdmin; import com.datastax.astra.client.core.options.DataAPIClientOptions; import com.datastax.astra.client.core.options.TimeoutOptions; import com.datastax.astra.client.core.http.HttpClientOptions; @@ -248,21 +249,54 @@ public DataAPIClient dataAPIClient() { @Bean @ConditionalOnMissingBean public Database database(DataAPIClient dataAPIClient) { + if (Utils.hasLength(dataAPIClientProperties.getEndpointUrl())) { + Database database; if (Utils.hasLength(dataAPIClientProperties.getKeyspace())) { LOGGER.info("Setup of Database from endpoint-url: {} with keyspace: {}", dataAPIClientProperties.getEndpointUrl(), dataAPIClientProperties.getKeyspace()); - return dataAPIClient.getDatabase( + database = dataAPIClient.getDatabase( dataAPIClientProperties.getEndpointUrl(), dataAPIClientProperties.getKeyspace()); } else { LOGGER.info("Setup of Database from endpoint-url: {}", dataAPIClientProperties.getEndpointUrl()); - return dataAPIClient.getDatabase(dataAPIClientProperties.getEndpointUrl()); + database = dataAPIClient.getDatabase(dataAPIClientProperties.getEndpointUrl()); + } + + // list keyspaces from Data API Client + SchemaAction schemaAction = dataAPIClientProperties.getSchemaAction(); + DatabaseAdmin dbAdmin = database.getDatabaseAdmin(); + if (!dbAdmin.listKeyspaceNames().contains(database.getKeyspace())) { + LOGGER.info("Schema action configured: {}", schemaAction); + if (SchemaAction.CREATE_IF_NOT_EXISTS.equals(schemaAction)) { + dbAdmin.createKeyspace(database.getKeyspace()); + } else if (SchemaAction.VALIDATE.equals(schemaAction)) { + throw new IllegalArgumentException("Keyspace '" + + database.getKeyspace() + "' has not been found, create the keyspace or " + + "set astra.data-api.schema-action to CREATE_IF_NOT_EXISTS"); + } } + + // if expected keyspace does not exists + + + + + + return database; } else { LOGGER.warn("No endpoint-url provided in configuration. Database bean will not be created."); return null; } } + + /** + * Gets the configured schema action. + * + * @return the schema action + */ + public SchemaAction getSchemaAction() { + return dataAPIClientProperties.getSchemaAction(); + } } diff --git a/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java index 82366d4e..a4912179 100644 --- a/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java +++ b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java @@ -41,6 +41,16 @@ public class DataAPIClientProperties { */ private Boolean logRequest; + /** + * Schema action for collection/table management. + * Similar to JPA's ddl-auto property. + * Possible values: + * - CREATE_IF_NOT_EXISTS: Create collections/tables if they don't exist (default) + * - VALIDATE: Validate that collections/tables exist but don't create them + * - NONE: Do nothing, assume collections/tables already exist + */ + private SchemaAction schemaAction = SchemaAction.CREATE_IF_NOT_EXISTS; + /** * Advanced options for DataAPI client */ diff --git a/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/SchemaAction.java b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/SchemaAction.java new file mode 100644 index 00000000..a1d0a893 --- /dev/null +++ b/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/SchemaAction.java @@ -0,0 +1,72 @@ +package com.datastax.astra.boot.autoconfigure; + +/*- + * #%L + * Data API Spring Boot 3.x Autoconfigure + * -- + * 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% + */ + +/** + * Defines the schema management strategy for collections and tables. + * Similar to JPA's ddl-auto property, this controls how the application + * handles collection/table creation and validation at startup. + * + * @author Cedrick LUNVEN (@clunven) + */ +public enum SchemaAction { + + /** + * Create collections/tables if they don't exist. + * This is the default and safest option for most applications. + * If a collection/table already exists, it will be used as-is. + */ + CREATE_IF_NOT_EXISTS, + + /** + * Validate that collections/tables exist but don't create them. + * The application will fail to start if required collections/tables are missing. + * Use this in production environments where schema should be managed separately. + */ + VALIDATE, + + /** + * Do nothing - assume collections/tables already exist. + * No validation or creation will be performed. + * Use this when you want complete manual control over schema management. + */ + NONE; + + /** + * Parse a string value to SchemaAction enum. + * + * @param value the string value (case-insensitive) + * @return the corresponding SchemaAction + * @throws IllegalArgumentException if the value is not a valid SchemaAction + */ + public static SchemaAction fromString(String value) { + if (value == null || value.trim().isEmpty()) { + return CREATE_IF_NOT_EXISTS; // default + } + try { + return SchemaAction.valueOf(value.toUpperCase().replace("-", "_")); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Invalid schema-action value: '" + value + "'. " + + "Valid values are: CREATE_IF_NOT_EXISTS, VALIDATE, NONE", e); + } + } +} diff --git a/integrations/data-api-spring-boot-3x-starter/pom.xml b/integrations/data-api-spring-boot-3x-starter/pom.xml index be329272..a0185ced 100644 --- a/integrations/data-api-spring-boot-3x-starter/pom.xml +++ b/integrations/data-api-spring-boot-3x-starter/pom.xml @@ -33,6 +33,13 @@ + + + com.datastax.astra + astra-db-java + ${project.version} + + com.datastax.astra diff --git a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java index bfb00a17..b64aed81 100644 --- a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java +++ b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java @@ -1,74 +1,388 @@ package com.datastax.astra.spring; +import com.datastax.astra.boot.autoconfigure.DataAPIClientProperties; +import com.datastax.astra.boot.autoconfigure.SchemaAction; import com.datastax.astra.client.collections.Collection; +import com.datastax.astra.client.collections.commands.results.CollectionInsertManyResult; +import com.datastax.astra.client.collections.commands.results.CollectionInsertOneResult; +import com.datastax.astra.client.collections.definition.CollectionDefinition; +import com.datastax.astra.client.collections.mapping.DataApiCollection; +import com.datastax.astra.client.core.query.Filter; +import com.datastax.astra.client.databases.Database; +import com.datastax.astra.internal.reflection.CollectionBeanDefinition; +import com.datastax.astra.internal.utils.BetaPreview; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.lang.NonNull; +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; -public abstract class DataApiCollectionCrudRepository implements CrudRepository { +/** + * Abstract base class for Spring Data CRUD repositories backed by DataStax Astra DB Collections. + *

+ * This class provides a complete implementation of Spring's {@link CrudRepository} interface + * using the DataStax Astra DB Java SDK's {@link Collection} API. It automatically discovers + * and initializes collections based on the {@link DataApiCollection} annotation on the document class. + *

+ * + *

Usage Example:

+ *
+ * {@code
+ * @DataApiCollection(name = "products")
+ * public class Product {
+ *     @DocumentId
+ *     private String id;
+ *
+ *     private String name;
+ *     private BigDecimal price;
+ *     // getters and setters
+ * }
+ *
+ * @Repository
+ * public interface ProductRepository extends DataApiCollectionCrudRepository {}
+ * }
+ * 
+ * + * @param the document type, should be annotated with {@link DataApiCollection} + * @param the document ID type (must match the type of the field annotated with @DocumentId) + */ +@Slf4j +@BetaPreview +public abstract class DataApiCollectionCrudRepository + implements CrudRepository, QueryByExampleExecutor { - Collection dataAPICollection; + /** + * Injected Database instance from Spring context. + */ + @Autowired + @Getter + protected Database database; + /** + * Injected configuration properties. + */ + @Autowired + @Getter + protected DataAPIClientProperties yamlConfig; - public Collection getCollection() { - return dataAPICollection; + /** + * The underlying DataStax Astra DB Collection instance. + */ + @Getter + protected Collection collection; + + /** + * The document class type. + */ + @Getter + protected Class documentClass; + + /** + * The document ID class type. + */ + @Getter + protected Class idClass; + + /** + * Bean definition for the document class. + */ + @Getter + protected CollectionBeanDefinition beanDefinition; + + /** + * Initializes the repository after dependency injection. + *

+ * This method discovers the document class from the generic type parameters, + * validates that it's annotated with {@link DataApiCollection}, and initializes + * the underlying collection based on the configured schema action. + *

+ * + * @throws IllegalStateException if the document class is not properly annotated + */ + @PostConstruct + @SuppressWarnings("unchecked") + protected void init() { + // Extract the document and ID classes from generic type parameters + ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass(); + this.documentClass = (Class) genericSuperclass.getActualTypeArguments()[0]; + this.idClass = (Class) genericSuperclass.getActualTypeArguments()[1]; + + // Create bean definition + this.beanDefinition = new CollectionBeanDefinition<>(documentClass); + + // Validate that the document class is annotated with @DataApiCollection + DataApiCollection annotation = documentClass.getAnnotation(DataApiCollection.class); + if (annotation == null) { + throw new IllegalStateException(String.format( + "Document class '%s' must be annotated with @DataApiCollection", + documentClass.getName())); + } + + // Get the collection name from annotation or use class name + String collectionName = annotation.name(); + if (collectionName == null || collectionName.isEmpty()) { + collectionName = documentClass.getSimpleName().toLowerCase(); + } + + // Handle schema actions + if (SchemaAction.CREATE_IF_NOT_EXISTS.equals(yamlConfig.getSchemaAction())) { + log.info("Detected schema action CREATE_IF_NOT_EXISTS, ensuring collection {} exists...", collectionName); + if (!database.collectionExists(collectionName)) { + log.info("Collection '{}' does not exist, creating it...", collectionName); + CollectionDefinition expected = database.getCollection(collectionName, documentClass).getDefinition(); + database.createCollection(collectionName, expected, documentClass); + log.info("Collection '{}' created successfully", collectionName); + } else { + log.info("Collection '{}' already exists", collectionName); + } + } else if (SchemaAction.VALIDATE.equals(yamlConfig.getSchemaAction())) { + log.info("Detected schema action VALIDATE, validating collection {}...", collectionName); + if (!database.collectionExists(collectionName)) { + throw new IllegalArgumentException("Collection '" + collectionName + "' does not exist"); + } else { + CollectionDefinition existing = database.getCollection(collectionName).getDefinition(); + CollectionDefinition expected = database.getCollection(collectionName, documentClass).getDefinition(); + + // Compare collection definitions + if (!existing.equals(expected)) { + throw new IllegalStateException(String.format( + "Collection '%s' schema mismatch. Existing collection definition does not match document class '%s' definition. " + + "Expected: %s, Found: %s", + collectionName, documentClass.getName(), expected, existing)); + } + log.info("Collection '{}' schema validated successfully", collectionName); + } + } + + // Get the collection instance + this.collection = database.getCollection(collectionName, documentClass); } + /** + * Extracts the document ID from a document instance. + * + * @param document the document + * @return the document ID + */ + @SuppressWarnings("unchecked") + protected ID extractDocumentId(RECORD document) { + if (document == null) { + throw new IllegalArgumentException("Document must not be null"); + } + Object id = beanDefinition.getId(document); + if (id == null) { + throw new IllegalStateException("Document ID is null"); + } + return (ID) id; + } + + // ==================== CrudRepository Implementation ==================== + @Override - public S save(S entity) { - return null; + @NonNull + public S save(@NonNull S entity) { + CollectionInsertOneResult res = collection.insertOne(entity); + if (res.getInsertedId() != null && beanDefinition.canSetId()) { + beanDefinition.setId(entity, res.getInsertedId()); + } + return entity; } @Override - public Iterable saveAll(Iterable entities) { - return null; + @NonNull + public Iterable saveAll(@NonNull Iterable entities) { + List batch = new ArrayList<>(); + for (S entity : entities) { + batch.add(entity); + } + if (batch.isEmpty()) { + return batch; + } + + CollectionInsertManyResult result = collection.insertMany(batch); + if (beanDefinition.canSetId()) { + List insertedIds = result.getInsertedIds(); + int size = Math.min(batch.size(), insertedIds.size()); + for (int i = 0; i < size; i++) { + if (beanDefinition.getId(batch.get(i)) == null && insertedIds.get(i) != null) { + beanDefinition.setId(batch.get(i), insertedIds.get(i)); + } + } + } + return batch; } @Override - public Optional findById(T t) { - return Optional.empty(); + @NonNull + public Optional findById(@NonNull ID id) { + return collection.findById(id); } @Override - public boolean existsById(T t) { - return false; + public boolean existsById(@NonNull ID id) { + return findById(id).isPresent(); } @Override + @NonNull public Iterable findAll() { - return null; + return collection.findAll().toList(); + } + + /** + * Finds all entities matching the provided Data API filter. + * + * @param filter + * filter to apply + * @return + * matching entities + */ + @NonNull + public Iterable findAll(Filter filter) { + return collection.find(filter).toList(); + } + + /** + * Finds all entities matching the provided Data API filter and Spring sort. + * + * @param filter + * filter to apply + * @param sort + * spring sort to map + * @return + * matching entities + */ + @NonNull + public Iterable findAll(com.datastax.astra.client.core.query.Filter filter, @NonNull Sort sort) { + com.datastax.astra.client.collections.commands.options.CollectionFindOptions options = + new com.datastax.astra.client.collections.commands.options.CollectionFindOptions(); + com.datastax.astra.client.core.query.Sort[] mappedSort = DataApiSpringQueryMapper.mapSort(sort); + if (mappedSort.length > 0) { + options.sort(mappedSort); + } + return collection.find(filter, options).toList(); + } + + /** + * Finds all entities matching the provided Data API filter and pageable settings. + * + * @param filter + * filter to apply + * @param pageable + * spring pageable to map + * @return + * matching entities + */ + @NonNull + public Iterable findAll(com.datastax.astra.client.core.query.Filter filter, @NonNull Pageable pageable) { + return collection.find(filter, DataApiSpringQueryMapper.mapPageable(pageable)).toList(); } @Override - public Iterable findAllById(Iterable ts) { - return null; + @NonNull + public Iterable findAllById(@NonNull Iterable ids) { + com.datastax.astra.client.core.query.Filter idFilter = DataApiSpringQueryMapper.mapIdIn(ids); + if (idFilter == null) { + return List.of(); + } + return collection.find(idFilter).toList(); } @Override public long count() { - return 0; + return collection.countDocuments(Integer.MAX_VALUE); } @Override - public void deleteById(T t) { - + public void deleteById(@NonNull ID id) { + collection.deleteOne(com.datastax.astra.client.core.query.Filters.id(id)); } @Override - public void delete(RECORD entity) { - // TODO: Implement delete logic + public void delete(@NonNull RECORD entity) { + deleteById(extractDocumentId(entity)); } @Override - public void deleteAllById(Iterable ts) { - // TODO: Implement deleteAllById logic + public void deleteAllById(@NonNull Iterable ids) { + for (ID id : ids) { + deleteById(id); + } } @Override - public void deleteAll(Iterable entities) { + public void deleteAll(@NonNull Iterable entities) { + for (RECORD entity : entities) { + delete(entity); + } } @Override public void deleteAll() { - getCollection().deleteAll(); + collection.deleteAll(); + } + + @Override + @NonNull + public Optional findOne(@NonNull Example example) { + com.datastax.astra.client.core.query.Filter filter = DataApiSpringQueryMapper.mapExample(example, new CollectionBeanDefinition<>(example.getProbeType())); + return collection.findOne(filter).map(example.getProbeType()::cast); + } + + @Override + @NonNull + public Iterable findAll(@NonNull Example example) { + com.datastax.astra.client.core.query.Filter filter = DataApiSpringQueryMapper.mapExample(example, new CollectionBeanDefinition<>(example.getProbeType())); + return collection.find(filter, new com.datastax.astra.client.collections.commands.options.CollectionFindOptions(), example.getProbeType()).toList(); + } + + @Override + @NonNull + public Iterable findAll(@NonNull Example example, @NonNull Sort sort) { + com.datastax.astra.client.core.query.Filter filter = DataApiSpringQueryMapper.mapExample(example, new CollectionBeanDefinition<>(example.getProbeType())); + com.datastax.astra.client.collections.commands.options.CollectionFindOptions options = + new com.datastax.astra.client.collections.commands.options.CollectionFindOptions(); + com.datastax.astra.client.core.query.Sort[] mappedSort = DataApiSpringQueryMapper.mapSort(sort); + if (mappedSort.length > 0) { + options.sort(mappedSort); + } + return collection.find(filter, options, example.getProbeType()).toList(); + } + + @Override + @NonNull + public org.springframework.data.domain.Page findAll(@NonNull Example example, @NonNull Pageable pageable) { + com.datastax.astra.client.core.query.Filter filter = DataApiSpringQueryMapper.mapExample(example, new CollectionBeanDefinition<>(example.getProbeType())); + com.datastax.astra.client.collections.commands.options.CollectionFindOptions options = DataApiSpringQueryMapper.mapPageable(pageable); + java.util.List content = collection.find(filter, options, example.getProbeType()).toList(); + return new org.springframework.data.domain.PageImpl<>(content, pageable, content.size()); + } + + @Override + public long count(@NonNull Example example) { + com.datastax.astra.client.core.query.Filter filter = DataApiSpringQueryMapper.mapExample(example, new CollectionBeanDefinition<>(example.getProbeType())); + return collection.countDocuments(filter, Integer.MAX_VALUE); + } + + @Override + public boolean exists(@NonNull Example example) { + com.datastax.astra.client.core.query.Filter filter = DataApiSpringQueryMapper.mapExample(example, new CollectionBeanDefinition<>(example.getProbeType())); + return collection.findOne(filter).isPresent(); + } + + @Override + public R findBy( + @NonNull Example example, + @NonNull java.util.function.Function, R> queryFunction) { + throw new UnsupportedOperationException("QueryByExampleExecutor#findBy is not implemented yet for Data API repositories"); } -} +} \ No newline at end of file diff --git a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java new file mode 100644 index 00000000..d5043951 --- /dev/null +++ b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java @@ -0,0 +1,187 @@ +package com.datastax.astra.spring; + +import com.datastax.astra.client.collections.commands.options.CollectionFindOptions; +import com.datastax.astra.client.core.query.Filter; +import com.datastax.astra.client.core.query.Filters; +import com.datastax.astra.client.core.query.Sort; +import com.datastax.astra.internal.reflection.CollectionBeanDefinition; +import com.datastax.astra.internal.reflection.EntityFieldDefinition; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.Pageable; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Maps Spring Data query abstractions to Data API query objects. + */ +public final class DataApiSpringQueryMapper { + + private DataApiSpringQueryMapper() { + } + + /** + * Maps a Spring {@link Example} to a Data API {@link Filter}. + * + * @param example + * example to map + * @param beanDefinition + * entity metadata + * @param + * entity type + * @return + * mapped filter, or {@code null} when the example produces no predicate + */ + public static Filter mapExample(Example example, CollectionBeanDefinition beanDefinition) { + if (example == null) { + return null; + } + if (beanDefinition == null) { + throw new IllegalArgumentException("Bean definition must not be null"); + } + + T probe = example.getProbe(); + if (probe == null) { + return null; + } + + ExampleMatcher matcher = example.getMatcher(); + List filters = new ArrayList<>(); + + for (EntityFieldDefinition field : beanDefinition.getFields().values()) { + String fieldName = field.getName(); + if (matcher.isIgnoredPath(fieldName)) { + continue; + } + Object value = readValue(probe, field); + if (value == null) { + continue; + } + + if (value instanceof String stringValue) { + String normalized = stringValue; + if (matcher.isIgnoreCaseEnabled()) { + normalized = stringValue.toLowerCase(); + } + ExampleMatcher.StringMatcher stringMatcher = resolveStringMatcher(matcher, fieldName); + switch (stringMatcher) { + case DEFAULT: + case EXACT: + filters.add(Filters.eq(fieldName, normalized)); + break; + case STARTING: + filters.add(Filters.match(fieldName, normalized + "*")); + break; + case ENDING: + filters.add(Filters.match(fieldName, "*" + normalized)); + break; + case CONTAINING: + filters.add(Filters.match(fieldName, "*" + normalized + "*")); + break; + case REGEX: + filters.add(Filters.match(fieldName, normalized)); + break; + default: + filters.add(Filters.eq(fieldName, normalized)); + break; + } + } else { + filters.add(Filters.eq(fieldName, value)); + } + } + + if (filters.isEmpty()) { + return null; + } + return filters.size() == 1 ? filters.get(0) : Filters.and(filters); + } + + /** + * Maps a Spring {@link org.springframework.data.domain.Sort} to Data API sorts. + * + * @param springSort + * spring sort + * @return + * mapped sort array, empty if no sort + */ + public static Sort[] mapSort(org.springframework.data.domain.Sort springSort) { + if (springSort == null || springSort.isUnsorted()) { + return new Sort[0]; + } + List sorts = new ArrayList<>(); + for (org.springframework.data.domain.Sort.Order order : springSort) { + sorts.add(order.isAscending() + ? Sort.ascending(order.getProperty()) + : Sort.descending(order.getProperty())); + } + return sorts.toArray(new Sort[0]); + } + + /** + * Maps a Spring {@link Pageable} to collection find options. + * + * @param pageable + * spring pageable + * @return + * mapped find options + */ + public static CollectionFindOptions mapPageable(Pageable pageable) { + CollectionFindOptions options = new CollectionFindOptions(); + if (pageable == null || pageable.isUnpaged()) { + return options; + } + options.skip((int) pageable.getOffset()); + options.limit(pageable.getPageSize()); + Sort[] sorts = mapSort(pageable.getSort()); + if (sorts.length > 0) { + options.sort(sorts); + } + return options; + } + + /** + * Creates an id-in filter from an iterable of ids. + * + * @param ids + * ids to map + * @return + * filter or null when no ids + */ + public static Filter mapIdIn(Iterable ids) { + if (ids == null) { + return null; + } + List values = new ArrayList<>(); + ids.forEach(values::add); + if (values.isEmpty()) { + return null; + } + return Filters.in("_id", values.toArray()); + } + + private static Object readValue(T probe, EntityFieldDefinition field) { + Method getter = field.getGetter(); + if (getter == null) { + return null; + } + try { + return getter.invoke(probe); + } catch (Exception e) { + throw new IllegalStateException("Cannot read field '" + field.getName() + "' from example probe", e); + } + } + + private static ExampleMatcher.StringMatcher resolveStringMatcher(ExampleMatcher matcher, String fieldName) { + Optional propertySpecifier = + matcher.getPropertySpecifiers().hasSpecifierForPath(fieldName) + ? Optional.of(matcher.getPropertySpecifiers().getForPath(fieldName)) + : Optional.empty(); + + return propertySpecifier + .map(ExampleMatcher.PropertySpecifier::getStringMatcher) + .orElseGet(matcher::getDefaultStringMatcher); + } +} diff --git a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java index a10b5459..1b674f14 100644 --- a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java +++ b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java @@ -1,4 +1,620 @@ package com.datastax.astra.spring; -public class DataApiTableCrudRepository { +import com.datastax.astra.boot.autoconfigure.DataAPIClientProperties; +import com.datastax.astra.boot.autoconfigure.SchemaAction; +import com.datastax.astra.client.core.query.Filter; +import com.datastax.astra.client.core.query.Filters; +import com.datastax.astra.client.databases.Database; +import com.datastax.astra.client.tables.Table; +import com.datastax.astra.client.tables.definition.TableDefinition; +import com.datastax.astra.client.tables.exceptions.TooManyRowsToCountException; +import com.datastax.astra.client.tables.mapping.Column; +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.datastax.astra.internal.reflection.EntityTableBeanDefinition; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.lang.NonNull; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Abstract base class for Spring Data CRUD repositories backed by DataStax Astra DB Tables. + *

+ * This class provides a complete implementation of Spring's {@link CrudRepository} interface + * using the DataStax Astra DB Java SDK's {@link Table} API. It automatically discovers + * and initializes tables based on the {@link EntityTable} annotation on the entity class. + *

+ * + *

Primary Key Patterns:

+ *

The repository supports three patterns for defining primary keys:

+ *
    + *
  1. Single Partition Key: Use the field type directly (e.g., UUID, String)
  2. + *
  3. Composite Key with Map: Use Map<String, Object> where keys are column names
  4. + *
  5. Composite Key with @TablePrimaryKeyClass: Use a dedicated class annotated with @TablePrimaryKeyClass
  6. + *
+ * + *

Pattern 1: Single Partition Key

+ *
+ * {@code
+ * @EntityTable("users")
+ * public class User {
+ *     @PartitionBy(1)
+ *     @Column("user_id")
+ *     private UUID userId;
+ *     
+ *     @Column("name")
+ *     private String name;
+ * }
+ *
+ * @Repository
+ * public interface UserRepository extends DataApiTableCrudRepository {}
+ * }
+ * 
+ * + *

Pattern 2: Composite Key with Map

+ *
+ * {@code
+ * @EntityTable("orders")
+ * public class Order {
+ *     @PartitionBy(1)
+ *     @Column("customer_id")
+ *     private String customerId;
+ *     
+ *     @PartitionBy(2)
+ *     @Column("order_date")
+ *     private LocalDate orderDate;
+ *     
+ *     @Column("amount")
+ *     private BigDecimal amount;
+ * }
+ *
+ * @Repository
+ * public interface OrderRepository extends DataApiTableCrudRepository> {}
+ * 
+ * // Usage:
+ * Map pk = Map.of("customer_id", "CUST123", "order_date", LocalDate.now());
+ * Optional order = orderRepository.findById(pk);
+ * }
+ * 
+ * + *

Pattern 3: Composite Key with @TablePrimaryKeyClass (Recommended)

+ *
+ * {@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;
+ * }
+ *
+ * @Repository
+ * public interface OrderRepository extends DataApiTableCrudRepository {}
+ * 
+ * // Usage:
+ * OrderKey key = new OrderKey("CUST123", LocalDate.now());
+ * Optional order = orderRepository.findById(key);
+ * }
+ * 
+ * + * @param the entity type, must be annotated with {@link EntityTable} + * @param the primary key type (single field, Map<String, Object>, or @TablePrimaryKeyClass annotated class) + */ +@Slf4j +public abstract class DataApiTableCrudRepository implements CrudRepository { + + /** + * The underlying DataStax Astra DB Table instance. + */ + protected Table dataAPITable; + + /** + * The entity class type. + */ + protected Class entityClass; + + /** + * The primary key class type. + */ + protected Class primaryKeyClass; + + /** + * Cached list of partition key column names in order. + */ + protected List partitionKeyColumns; + + /** + * Cached list of clustering column names in order. + */ + protected List clusteringColumns; + + /** + * Whether the primary key is a @TablePrimaryKeyClass annotated class. + */ + protected boolean isPrimaryKeyClass; + + /** + * Field in the entity annotated with @TablePrimaryKey (if using @TablePrimaryKeyClass pattern). + */ + protected Field primaryKeyField; + + /** + * Injected Database instance from Spring context. + */ + @Autowired + protected Database database; + + @Autowired + protected DataAPIClientProperties yamlConfig; + + /** + * Initializes the repository after dependency injection. + *

+ * This method discovers the entity class from the generic type parameters, + * validates that it's annotated with {@link EntityTable}, and initializes + * the underlying table. + *

+ * + * @throws IllegalStateException if the entity class is not properly annotated + */ + @PostConstruct + @SuppressWarnings("unchecked") + protected void init() { + // Extract the entity and primary key classes from generic type parameters + ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass(); + this.entityClass = (Class) genericSuperclass.getActualTypeArguments()[0]; + this.primaryKeyClass = (Class) genericSuperclass.getActualTypeArguments()[1]; + + // Validate that the entity class is annotated with @EntityTable + EntityTable annotation = entityClass.getAnnotation(EntityTable.class); + if (annotation == null) { + throw new IllegalStateException(String.format( + "Entity class '%s' must be annotated with @EntityTable", + entityClass.getName())); + } + + // Check if using @TablePrimaryKeyClass pattern + this.isPrimaryKeyClass = primaryKeyClass.isAnnotationPresent(TablePrimaryKeyClass.class); + + if (isPrimaryKeyClass) { + // Find the field annotated with @TablePrimaryKey in the entity + this.primaryKeyField = findPrimaryKeyField(entityClass); + if (primaryKeyField == null) { + throw new IllegalStateException(String.format( + "Entity class '%s' must have a field annotated with @TablePrimaryKey when using @TablePrimaryKeyClass", + entityClass.getName())); + } + // Extract partition keys from the primary key class + this.partitionKeyColumns = extractPartitionKeyColumns(primaryKeyClass); + this.clusteringColumns = extractClusteringColumns(primaryKeyClass); + } else { + // Extract partition keys from the entity class directly + this.partitionKeyColumns = extractPartitionKeyColumns(entityClass); + this.clusteringColumns = extractClusteringColumns(entityClass); + } + + if (partitionKeyColumns.isEmpty()) { + throw new IllegalStateException(String.format( + "Entity class '%s' must have at least one field annotated with @PartitionBy", + entityClass.getName())); + } + + // Get the table name from annotation or use class name + String tableName = annotation.value(); + if (tableName == null || tableName.isEmpty()) { + tableName = entityClass.getSimpleName().toLowerCase(); + } + + // Check the schema action + if (yamlConfig.getSchemaAction() != null) { + if (SchemaAction.CREATE_IF_NOT_EXISTS.equals(yamlConfig.getSchemaAction())) { + log.info("Detected schema action CREATE_IF_NOT_EXISTS, creating table " + tableName + "..."); + database.createTable(entityClass); + log.info("Table '{}' has been created", tableName); + } + } else if (SchemaAction.VALIDATE.equals(yamlConfig.getSchemaAction())) { + log.info("Detected schema action VALIDATE, validating table " + tableName + "..."); + if (!database.tableExists(tableName)) { + log.info("Table '{}' does not exist", tableName); + throw new IllegalArgumentException("Table '" + tableName + "' does not exist"); + } else { + TableDefinition existing = database.getTable(tableName).getDefinition(); + TableDefinition settings = database.getTable(tableName, entityClass).getDefinition(); + + // Compare table definitions + if (!existing.equals(settings)) { + throw new IllegalStateException(String.format( + "Table '%s' schema mismatch. Existing table definition does not match entity class '%s' definition. " + + "Expected: %s, Found: %s", + tableName, entityClass.getName(), settings, existing)); + } + log.info("Table '{}' schema validated successfully", tableName); + } + } + + // Get the table instance + this.dataAPITable = database.getTable(tableName, entityClass); + } + + /** + * Finds the field annotated with @TablePrimaryKey in the entity class. + * + * @param clazz the entity class + * @return the primary key field, or null if not found + */ + protected Field findPrimaryKeyField(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(TablePrimaryKey.class)) { + field.setAccessible(true); + return field; + } + } + return null; + } + + /** + * Extracts partition key column names from a class. + * Fields annotated with @PartitionBy are sorted by their position value. + * + * @param clazz the class to inspect + * @return ordered list of partition key column names + */ + protected List extractPartitionKeyColumns(Class clazz) { + List partitionKeys = new ArrayList<>(); + + for (Field field : clazz.getDeclaredFields()) { + PartitionBy partitionBy = field.getAnnotation(PartitionBy.class); + if (partitionBy != null) { + Column column = field.getAnnotation(Column.class); + String columnName = (column != null && column.name() != null && !column.name().isEmpty()) + ? column.name() + : field.getName(); + partitionKeys.add(new PartitionKeyInfo(partitionBy.value(), columnName)); + } + } + + // Sort by position and extract column names + return partitionKeys.stream() + .sorted(Comparator.comparingInt(PartitionKeyInfo::position)) + .map(PartitionKeyInfo::columnName) + .collect(Collectors.toList()); + } + + /** + * Extracts clustering column names from a class. + * Fields annotated with @PartitionSort are sorted by their position value. + * + * @param clazz the class to inspect + * @return ordered list of clustering column names + */ + protected List extractClusteringColumns(Class clazz) { + List clusteringKeys = new ArrayList<>(); + + for (Field field : clazz.getDeclaredFields()) { + PartitionSort partitionSort = field.getAnnotation(PartitionSort.class); + if (partitionSort != null) { + Column column = field.getAnnotation(Column.class); + String columnName = (column != null && column.name() != null && !column.name().isEmpty()) + ? column.name() + : field.getName(); + clusteringKeys.add(new ClusteringKeyInfo(partitionSort.position(), columnName)); + } + } + + // Sort by position and extract column names + return clusteringKeys.stream() + .sorted(Comparator.comparingInt(ClusteringKeyInfo::position)) + .map(ClusteringKeyInfo::columnName) + .collect(Collectors.toList()); + } + + /** + * Helper record to store partition key information. + */ + protected record PartitionKeyInfo(int position, String columnName) {} + + /** + * Helper record to store clustering key information. + */ + protected record ClusteringKeyInfo(int position, String columnName) {} + + /** + * Gets the underlying DataStax Astra DB Table instance. + * + * @return the table instance + */ + public Table getTable() { + return dataAPITable; + } + + /** + * Converts a primary key to a Map representation. + *

+ * Handles three cases: + *

    + *
  • Single partition key: creates a map with one entry
  • + *
  • Map primary key: returns as-is
  • + *
  • @TablePrimaryKeyClass: extracts fields using reflection
  • + *
+ *

+ * + * @param pk the primary key + * @return Map representation of the primary key + */ + @SuppressWarnings("unchecked") + protected Map primaryKeyToMap(PK pk) { + if (pk == null) { + throw new IllegalArgumentException("Primary key must not be null"); + } + + // If already a Map, return it + if (pk instanceof Map) { + return (Map) pk; + } + + Map pkMap = new HashMap<>(); + + // If using @TablePrimaryKeyClass, extract fields from the primary key object + if (isPrimaryKeyClass) { + try { + List allKeyColumns = new ArrayList<>(partitionKeyColumns); + allKeyColumns.addAll(clusteringColumns); + + for (String columnName : allKeyColumns) { + Field field = findFieldByColumnName(primaryKeyClass, columnName); + if (field != null) { + field.setAccessible(true); + Object value = field.get(pk); + if (value != null) { + pkMap.put(columnName, value); + } + } + } + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to extract primary key fields", e); + } + } else { + // Single partition key - create a map with one entry + if (partitionKeyColumns.size() == 1 && clusteringColumns.isEmpty()) { + pkMap.put(partitionKeyColumns.get(0), pk); + } else { + throw new IllegalArgumentException( + "For composite primary keys, use Map or @TablePrimaryKeyClass"); + } + } + + return pkMap; + } + + /** + * Extracts the primary key from an entity. + * + * @param entity the entity + * @return the primary key + */ + @SuppressWarnings("unchecked") + protected PK extractPrimaryKey(ROW entity) { + if (entity == null) { + throw new IllegalArgumentException("Entity must not be null"); + } + + try { + if (isPrimaryKeyClass && primaryKeyField != null) { + // Extract the @TablePrimaryKey field from the entity + return (PK) primaryKeyField.get(entity); + } else { + // Extract partition key values directly from entity fields + Map pkValues = new HashMap<>(); + List allKeyColumns = new ArrayList<>(partitionKeyColumns); + allKeyColumns.addAll(clusteringColumns); + + for (String columnName : allKeyColumns) { + Field field = findFieldByColumnName(entityClass, columnName); + if (field != null) { + field.setAccessible(true); + Object value = field.get(entity); + if (value != null) { + pkValues.put(columnName, value); + } + } + } + + // Return single value or map + if (allKeyColumns.size() == 1) { + return (PK) pkValues.values().iterator().next(); + } else { + return (PK) pkValues; + } + } + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to extract primary key from entity", e); + } + } + + /** + * Creates a filter for the primary key. + * + * @param pk the primary key value + * @return a Filter for the primary key + */ + protected Filter createPrimaryKeyFilter(PK pk) { + Map pkMap = primaryKeyToMap(pk); + + List filters = new ArrayList<>(); + List allKeyColumns = new ArrayList<>(partitionKeyColumns); + allKeyColumns.addAll(clusteringColumns); + + for (String keyColumn : allKeyColumns) { + Object value = pkMap.get(keyColumn); + if (value == null) { + throw new IllegalArgumentException(String.format( + "Primary key must contain value for column '%s'", keyColumn)); + } + filters.add(Filters.eq(keyColumn, value)); + } + + return filters.size() == 1 ? filters.get(0) : Filters.and(filters.toArray(new Filter[0])); + } + + /** + * Finds a field by its column name or field name. + * + * @param clazz the class to search + * @param columnName the column name to find + * @return the field, or null if not found + */ + protected Field findFieldByColumnName(Class clazz, String columnName) { + for (Field field : clazz.getDeclaredFields()) { + Column column = field.getAnnotation(Column.class); + String fieldColumnName = (column != null && column.name() != null && !column.name().isEmpty()) + ? column.name() + : field.getName(); + if (fieldColumnName.equals(columnName)) { + return field; + } + } + return null; + } + + // ==================== CrudRepository Implementation ==================== + + @Override + @NonNull + public S save(@NonNull S entity) { + if (entity == null) { + throw new IllegalArgumentException("Entity must not be null"); + } + dataAPITable.insertOne(entity); + return entity; + } + + @Override + @NonNull + public Iterable saveAll(@NonNull Iterable entities) { + if (entities == null) { + throw new IllegalArgumentException("Entities must not be null"); + } + List result = new ArrayList<>(); + for (S entity : entities) { + result.add(save(entity)); + } + return result; + } + + @Override + @NonNull + public Optional findById(@NonNull PK pk) { + if (pk == null) { + throw new IllegalArgumentException("Primary key must not be null"); + } + Filter filter = createPrimaryKeyFilter(pk); + return dataAPITable.findOne(filter); + } + + @Override + public boolean existsById(@NonNull PK pk) { + if (pk == null) { + throw new IllegalArgumentException("Primary key must not be null"); + } + return findById(pk).isPresent(); + } + + @Override + @NonNull + public Iterable findAll() { + return dataAPITable.findAll().toList(); + } + + @Override + @NonNull + public Iterable findAllById(@NonNull Iterable pks) { + if (pks == null) { + throw new IllegalArgumentException("Primary keys must not be null"); + } + List result = new ArrayList<>(); + for (PK pk : pks) { + findById(pk).ifPresent(result::add); + } + return result; + } + + @Override + public long count() { + try { + return dataAPITable.countRows(Integer.MAX_VALUE); + } catch (TooManyRowsToCountException e) { + return Integer.MAX_VALUE; + } + } + + @Override + public void deleteById(@NonNull PK pk) { + if (pk == null) { + throw new IllegalArgumentException("Primary key must not be null"); + } + Filter filter = createPrimaryKeyFilter(pk); + dataAPITable.deleteOne(filter); + } + + @Override + public void delete(@NonNull ROW entity) { + if (entity == null) { + throw new IllegalArgumentException("Entity must not be null"); + } + PK pk = extractPrimaryKey(entity); + deleteById(pk); + } + + @Override + public void deleteAllById(@NonNull Iterable pks) { + if (pks == null) { + throw new IllegalArgumentException("Primary keys must not be null"); + } + for (PK pk : pks) { + deleteById(pk); + } + } + + @Override + public void deleteAll(@NonNull Iterable entities) { + if (entities == null) { + throw new IllegalArgumentException("Entities must not be null"); + } + for (ROW entity : entities) { + delete(entity); + } + } + + @Override + public void deleteAll() { + dataAPITable.deleteAll(); + } } diff --git a/pom.xml b/pom.xml index 98c1e2d8..4f1556a3 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,6 @@ 4.3.0 5.3.2 5.2.0 - 5.6 1.11.0 @@ -75,17 +74,7 @@ pom - - - org.apache.httpcomponents.client5 - httpclient5 - ${httpclient.version} - - - org.apache.httpcomponents.client5 - httpclient5-fluent - ${httpclient.version} - + diff --git a/samples/sample-openrag-api/src/main/resources/model.cql b/samples/sample-openrag-api/src/main/resources/model.cql new file mode 100644 index 00000000..88be838e --- /dev/null +++ b/samples/sample-openrag-api/src/main/resources/model.cql @@ -0,0 +1,38 @@ + +-- 1 backend, multiple stores +CREATE TABLE vector_stores_by_backend ( + backend text, + vector_store text, + embedding_provider text, + embedding_model text, + embedding_endpoint text, + embedding_dimension int, + embedding_api_key text, + lexical_enabled text, + lexical_analyzer text, + lexical_options map, + reranking_provider text, + reranking_model text, + reranking_endpoint text, + reranking_api_key text, + PRIMARY KEY ((backend), vector_store) +); + +-- list of document ingested +CREATE TABLE documents_by_vector_store ( + backend text, + vector_store text, + source_doc_id text, + source_filename text, + file_type text, + file_size bigint, + -- ingestion + md5_hash text, + chunk_total int, + ingested_at timestamp, + status text, + metadata map, + PRIMARY KEY ((backend, vector_store), source_doc_id) +); + + diff --git a/samples/sample-spring-boot3x/README.md b/samples/sample-spring-boot3x/README.md new file mode 100644 index 00000000..6722369b --- /dev/null +++ b/samples/sample-spring-boot3x/README.md @@ -0,0 +1,261 @@ +# Sample Spring Boot 3.x Application with DataStax Astra DB + +This sample demonstrates how to use the DataStax Astra DB Java SDK with Spring Boot 3.x, including the new Spring Data CRUD repositories for both Collections and Tables. + +## Prerequisites + +- Java 21 or higher +- Maven 3.6+ +- DataStax Astra DB account with a database created +- Application token with appropriate permissions + +## Configuration + +### 1. Set Environment Variable + +Set your Astra DB application token as an environment variable: + +```bash +export ASTRA_DB_APPLICATION_TOKEN="your-token-here" +``` + +### 2. Update application.yaml + +Edit `src/main/resources/application.yaml` and update the following: + +```yaml +astra: + data-api: + token: ${ASTRA_DB_APPLICATION_TOKEN} + endpoint-url: https://your-database-id-region.apps.astra.datastax.com + keyspace: default_keyspace + schema-action: CREATE_IF_NOT_EXISTS +``` + +**Configuration Options:** + +- `endpoint-url`: Your Astra DB API endpoint (found in Astra DB console) +- `keyspace`: The keyspace to use (default: `default_keyspace`) +- `schema-action`: + - `CREATE_IF_NOT_EXISTS`: Auto-create collections/tables if they don't exist + - `VALIDATE`: Validate that collections/tables match entity definitions + - `NONE`: Assume collections/tables already exist + +## Running the Application + +### Using Maven + +```bash +# From the project root +cd samples/sample-spring-boot3x + +# Run the application +mvn spring-boot:run +``` + +### Using Java + +```bash +# Build the project +mvn clean package + +# Run the JAR +java -jar target/sample-spring-boot3x-2.2.1-SNAPSHOT.jar +``` + +The application will start on port **8081** (configured in `application.yaml`). + +## Available REST API Endpoints + +### Health Check + +```bash +# Simple hello endpoint +curl http://localhost:8081/api/hello +``` + +**Response:** +```json +{ + "message": "Hello from DataAPI Spring Boot!", + "status": "running" +} +``` + +### Database Information Endpoints + +#### Get Complete Database Info + +```bash +curl http://localhost:8081/api/database/info +``` + +**Response:** +```json +{ + "keyspace": "default_keyspace", + "collections": ["c_book_auto"], + "collectionsCount": 1, + "tables": [], + "tablesCount": 0, + "types": [], + "typesCount": 0, + "status": "success" +} +``` + +#### Get Current Keyspace + +```bash +curl http://localhost:8081/api/database/keyspace +``` + +**Response:** +```json +{ + "keyspace": "default_keyspace", + "status": "success" +} +``` + +#### List Collections + +```bash +curl http://localhost:8081/api/database/collections +``` + +**Response:** +```json +{ + "collections": ["c_book_auto"], + "count": 1, + "status": "success" +} +``` + +#### List Tables + +```bash +curl http://localhost:8081/api/database/tables +``` + +**Response:** +```json +{ + "tables": [], + "count": 0, + "status": "success" +} +``` + +#### List User-Defined Types + +```bash +curl http://localhost:8081/api/database/types +``` + +**Response:** +```json +{ + "types": [], + "count": 0, + "status": "success" +} +``` + +#### Get Collection Details with Schema + +```bash +curl http://localhost:8081/api/database/collections/details +``` + +**Response:** Returns detailed schema information for each collection including vector configuration, indexing options, etc. + +#### Get Table Details with Schema + +```bash +curl http://localhost:8081/api/database/tables/details +``` + +**Response:** Returns detailed schema information for each table including columns, primary keys, etc. + +## Project Structure + +``` +src/main/java/com/ibm/astra/demo/ +├── DataApiStarterSpringBootApplication.java # Main Spring Boot application +├── books/ +│ ├── Book.java # Collection entity with @DataApiCollection +│ ├── BookRepository.java # Spring Data repository for Book collection +│ └── DataSet.java # Sample data loader +├── config/ # Configuration classes +└── controller/ + ├── HelloController.java # Simple health check endpoint + └── DatabaseInfoController.java # Database metadata REST API +``` + +## Key Features Demonstrated + +### 1. Collection Repository Pattern + +The `BookRepository` extends `DataApiCollectionCrudRepository` to provide automatic CRUD operations: + +```java +@Repository +public class BookRepository extends DataApiCollectionCrudRepository { + // No implementation needed - all CRUD methods inherited +} +``` + +### 2. Auto-Schema Creation + +The `Book` entity is annotated with `@DataApiCollection` which automatically creates the collection with: +- Vector search (1024 dimensions, COSINE similarity) +- Vectorization service (NVIDIA NV-Embed-QA) +- Lexical search (STANDARD analyzer) +- Reranking service (NVIDIA llama-3.2-nv-rerankqa-1b-v2) + +### 3. Database Metadata API + +The `DatabaseInfoController` provides REST endpoints to inspect: +- Current keyspace +- Available collections and their schemas +- Available tables and their schemas +- User-defined types + +## Troubleshooting + +### Connection Issues + +If you see connection errors: +1. Verify your `ASTRA_DB_APPLICATION_TOKEN` is set correctly +2. Check that the `endpoint-url` in `application.yaml` matches your database +3. Ensure your token has appropriate permissions + +### Schema Validation Errors + +If you see schema mismatch errors with `schema-action: VALIDATE`: +1. Switch to `CREATE_IF_NOT_EXISTS` to auto-create collections/tables +2. Or manually update your database schema to match entity definitions + +### Port Already in Use + +If port 8081 is already in use, change it in `application.yaml`: + +```yaml +server: + port: 8082 # or any available port +``` + +## Next Steps + +- Explore the Book collection CRUD operations +- Add your own entities and repositories +- Implement custom query methods +- Add table-based entities using `@EntityTable` + +## Documentation + +- [Astra DB Java SDK Documentation](https://docs.datastax.com/en/astra-db-serverless/api-reference/client-sdks.html) +- [Spring Boot Documentation](https://spring.io/projects/spring-boot) +- [Spring Data Documentation](https://spring.io/projects/spring-data) diff --git a/samples/sample-spring-boot3x/pom.xml b/samples/sample-spring-boot3x/pom.xml index 17b9316d..aa31947e 100644 --- a/samples/sample-spring-boot3x/pom.xml +++ b/samples/sample-spring-boot3x/pom.xml @@ -17,6 +17,7 @@ 21 21 3.5.13 + 2.8.13 @@ -45,6 +46,12 @@ spring-boot-starter-web + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + org.springframework.boot spring-boot-starter-test diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java index 508fd73d..61eb617e 100644 --- a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java @@ -9,7 +9,4 @@ public class DataApiStarterSpringBootApplication { public static void main(String[] args) { SpringApplication.run(DataApiStarterSpringBootApplication.class, args); } - - - -} +} \ No newline at end of file diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java index cac23ff2..a9cf10fe 100644 --- a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java @@ -2,12 +2,12 @@ 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.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; import java.util.Map; import java.util.Set; @@ -15,36 +15,49 @@ @Data @NoArgsConstructor @AllArgsConstructor -@DataApiCollection("c_book") +@DataApiCollection( + name = "c_books_vectorize_nvidia", + vectorDimension = 1024, + vectorizeModel = "NV-Embed-QA", + vectorizeProvider = "nvidia" +) +@Schema(name = "Book", description = "Book document stored in Astra DB") public class Book { @DocumentId - String id; - - String title; + @Schema(description = "Unique identifier of the book", example = "book-1984") + private String id; - String author; + @Schema(description = "Title of the book", example = "1984") + private String title; - boolean is_checked_out; - - @Vectorize - String vectorize; + @Schema(description = "Author name", example = "George Orwell") + private String author; - @Lexical - String lexical; + @JsonProperty("checked_out") + @Schema(description = "Whether the book is currently checked out", example = "false") + private boolean checkedOut; @JsonProperty("number_of_pages") - Integer numberOfPages; + @Schema(description = "Number of pages", example = "328") + private Integer numberOfPages; - String genre; + @Schema(description = "Primary genre", example = "Dystopian") + private String genre; - String description; + @Schema(description = "Short description of the book", example = "A dystopian social science fiction novel.") + private String description; - Set genres; + @Vectorize + @Schema(description = "String use for vectorization") + private String vectorize; + + @Schema(description = "List of genres or tags") + private Set genres; - Map metadata; + @Schema(description = "Additional metadata as key-value pairs") + private Map metadata; - // Fluent interface methods public Book id(String id) { this.id = id; return this; @@ -60,18 +73,13 @@ public Book author(String author) { return this; } - public Book isCheckedOut(boolean isCheckedOut) { - this.is_checked_out = isCheckedOut; - return this; - } - - public Book vectorize(String vectorize) { - this.vectorize = vectorize; + public Book isCheckedOut(boolean checkedOut) { + this.checkedOut = checkedOut; return this; } - public Book lexical(String lexical) { - this.lexical = lexical; + public Book checkedOut(boolean checkedOut) { + this.checkedOut = checkedOut; return this; } @@ -87,6 +95,7 @@ public Book genre(String genre) { public Book description(String description) { this.description = description; + this.vectorize = description; return this; } diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java index 983677fc..46e038f5 100644 --- a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java @@ -1,21 +1,25 @@ package com.ibm.astra.demo.books; -import com.datastax.astra.client.DataAPIClient; -import com.datastax.astra.client.collections.Collection; +import com.datastax.astra.client.collections.commands.options.CollectionFindOptions; +import com.datastax.astra.client.core.DataAPIKeywords; +import com.datastax.astra.client.core.query.Filter; +import com.datastax.astra.client.core.query.Projection; +import com.datastax.astra.client.core.query.Sort; import com.datastax.astra.spring.DataApiCollectionCrudRepository; -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public class BookRepository extends DataApiCollectionCrudRepository { - @Autowired - DataAPIClient dataAPIClient; - - Collection books; - - @PostConstruct - public void init() { + public List search(String query, Integer limit) { + return getCollection() + .find(new CollectionFindOptions() + .sort(Sort.vectorize(query)) + .projection(Projection.exclude(DataAPIKeywords.VECTOR.getKeyword())) + .limit(limit)) + .toList(); } + } diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java new file mode 100644 index 00000000..762818f3 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java @@ -0,0 +1,125 @@ +package com.ibm.astra.demo.books; + +import com.datastax.astra.client.core.query.Filters; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.stream.StreamSupport; + +@Service +public class BookService { + + private final BookRepository bookRepository; + + public BookService(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + public List findAll() { + return StreamSupport.stream(bookRepository.findAll().spliterator(), false) + .toList(); + } + + public Optional findById(String id) { + return bookRepository.findById(id); + } + + public Book create(Book book) { + validateBook(book); + if (book.getId() == null || book.getId().isBlank()) { + throw new IllegalArgumentException("Book id must be provided"); + } + if (bookRepository.existsById(book.getId())) { + throw new IllegalArgumentException("Book with id '%s' already exists".formatted(book.getId())); + } + return bookRepository.save(enrichBook(book)); + } + + public void flush() { + bookRepository.deleteAll(); + } + + public Book update(String id, Book book) { + validateBook(book); + Book existing = bookRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("Book with id '%s' was not found".formatted(id))); + + existing.setTitle(book.getTitle()); + existing.setAuthor(book.getAuthor()); + existing.setCheckedOut(book.isCheckedOut()); + existing.setNumberOfPages(book.getNumberOfPages()); + existing.setGenre(book.getGenre()); + existing.setDescription(book.getDescription()); + existing.setGenres(book.getGenres()); + existing.setMetadata(book.getMetadata()); + + bookRepository.getCollection().replaceOne(Filters.id(id), enrichBook(existing)); + return bookRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("Book with id '%s' was not found after update".formatted(id))); + } + + public void delete(String id) { + if (!bookRepository.existsById(id)) { + throw new NoSuchElementException("Book with id '%s' was not found".formatted(id)); + } + bookRepository.deleteById(id); + } + + public int loadDefaultDataSet() { + return insertMany(DataSet.BOOKS); + } + + public int insertMany(List books) { + if (books == null || books.isEmpty()) { + return 0; + } + List booksToInsert = books.stream() + .map(this::enrichBook) + .filter(book -> book.getId() != null && !book.getId().isBlank()) + .filter(book -> !bookRepository.existsById(book.getId())) + .toList(); + + if (!booksToInsert.isEmpty()) { + bookRepository.saveAll(booksToInsert); + } + return booksToInsert.size(); + } + + public List searchBooks(BookVectorSearchRequest query) { + return bookRepository.search(query.getQuery(), query.getLimit()); + } + + private void validateBook(Book book) { + if (book == null) { + throw new IllegalArgumentException("Book payload must be provided"); + } + if (book.getTitle() == null || book.getTitle().isBlank()) { + throw new IllegalArgumentException("Book title must be provided"); + } + if (book.getAuthor() == null || book.getAuthor().isBlank()) { + throw new IllegalArgumentException("Book author must be provided"); + } + } + + private Book enrichBook(Book book) { + if (book.getId() == null || book.getId().isBlank()) { + book.setId(slugify(book.getTitle(), book.getAuthor())); + } + book.setDescription(defaultString(book.getDescription())); + return book; + } + + private String defaultString(String value) { + return value == null ? "" : value; + } + + private String slugify(String title, String author) { + return (title + "-" + author) + .toLowerCase() + .replaceAll("[^a-z0-9]+", "-") + .replaceAll("(^-|-$)", ""); + } + +} diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookVectorSearchRequest.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookVectorSearchRequest.java new file mode 100644 index 00000000..b44c63c1 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookVectorSearchRequest.java @@ -0,0 +1,15 @@ +package com.ibm.astra.demo.books; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(name = "BookVectorSearchRequest", description = "Request payload for vector search on books") +public class BookVectorSearchRequest { + + @Schema(description = "Natural language query to vectorize", example = "space survival and science fiction") + private String query; + + @Schema(description = "Maximum number of books to return", example = "5", defaultValue = "5") + private Integer limit = 5; +} diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/config/ApplicationStartupListener.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/config/ApplicationStartupListener.java deleted file mode 100644 index cf7c0384..00000000 --- a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/config/ApplicationStartupListener.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ibm.astra.demo.config; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; - -/** - * Listener to log when the Spring application context is fully ready. - * - * @author Cedrick LUNVEN (@clunven) - */ -@Component -public class ApplicationStartupListener implements ApplicationListener { - - private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationStartupListener.class); - - @Override - public void onApplicationEvent(ApplicationReadyEvent event) { - LOGGER.info("=".repeat(80)); - LOGGER.info("🚀 Spring Boot Application Context is READY!"); - LOGGER.info("=".repeat(80)); - LOGGER.info("✅ DataAPI Client configured and available"); - LOGGER.info("✅ Database bean available (if endpoint-url configured)"); - LOGGER.info("📍 API endpoint: http://localhost:{}/api/hello", - event.getApplicationContext().getEnvironment().getProperty("server.port", "8080")); - LOGGER.info("=".repeat(80)); - } -} diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java new file mode 100644 index 00000000..b35e555a --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java @@ -0,0 +1,122 @@ +package com.ibm.astra.demo.controller; + +import com.ibm.astra.demo.books.Book; +import com.ibm.astra.demo.books.BookService; +import com.ibm.astra.demo.books.BookVectorSearchRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +@RestController +@RequestMapping("/api/v1/books") +@Tag(name = "Books", description = "CRUD REST API for books") +public class BookController { + + private final BookService bookService; + + public BookController(BookService bookService) { + this.bookService = bookService; + } + + @GetMapping + @Operation(summary = "List all books") + public List findAll() { + return bookService.findAll(); + } + + @GetMapping("/{id}") + @Operation(summary = "Get a book by id") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Book found"), + @ApiResponse(responseCode = "404", description = "Book not found", content = @Content(schema = @Schema())) + }) + public Book findById(@PathVariable String id) { + return bookService.findById(id) + .orElseThrow(() -> new NoSuchElementException("Book with id '%s' was not found".formatted(id))); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Create a new book") + public Book create(@RequestBody Book book) { + return bookService.create(book); + } + + @PostMapping("/load") + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Load the default dataset of books") + public Map loadDefaultDataSet() { + int inserted = bookService.loadDefaultDataSet(); + return Map.of( + "status", "success", + "inserted", inserted, + "datasetSize", 30 + ); + } + + + @DeleteMapping("/books") + @ResponseStatus(HttpStatus.NO_CONTENT) + @ResponseBody + @Operation(summary = "Flush all books from the collection") + public void flushBooks() { + bookService.flush(); + } + + @PostMapping("/vsearch") + @ResponseBody + @Operation(summary = "Vector search for books") + public List vectorSearch(@RequestBody BookVectorSearchRequest request) { + return bookService.searchBooks(request); + } + + @PutMapping("/{id}") + @Operation(summary = "Update an existing book") + public Book update(@PathVariable String id, @RequestBody Book book) { + return bookService.update(id, book); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete a book") + public void delete(@PathVariable String id) { + bookService.delete(id); + } + + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleBadRequest(IllegalArgumentException exception) { + return Map.of( + "status", "error", + "message", exception.getMessage() + ); + } + + @ExceptionHandler(NoSuchElementException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Map handleNotFound(NoSuchElementException exception) { + return Map.of( + "status", "error", + "message", exception.getMessage() + ); + } +} \ No newline at end of file diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HelloController.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HelloController.java deleted file mode 100644 index beed0be3..00000000 --- a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HelloController.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ibm.astra.demo.controller; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.HashMap; -import java.util.Map; - -/** - * Simple REST controller for health check and testing. - * - * @author Cedrick LUNVEN (@clunven) - */ -@RestController -@RequestMapping("/api") -public class HelloController { - - private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class); - - /** - * Simple hello endpoint to verify the application is running. - * - * @return a greeting message - */ - @GetMapping("/hello") - public Map hello() { - LOGGER.info("Hello endpoint called"); - Map response = new HashMap<>(); - response.put("message", "Hello from DataAPI Spring Boot!"); - response.put("status", "running"); - return response; - } -} diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java new file mode 100644 index 00000000..ce70e6e7 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java @@ -0,0 +1,16 @@ +package com.ibm.astra.demo.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +@Tag(name = "Home", description = "Home and utility endpoints") +public class HomeController { + + @GetMapping("/") + public String redirectToSwaggerUi() { + return "redirect:/swagger-ui/index.html"; + } + +} diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/InfoController.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/InfoController.java new file mode 100644 index 00000000..110f4341 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/InfoController.java @@ -0,0 +1,87 @@ +package com.ibm.astra.demo.controller; + +import com.datastax.astra.client.admin.DatabaseAdmin; +import com.datastax.astra.client.databases.Database; +import com.datastax.astra.client.databases.DatabaseOptions; +import com.datastax.astra.client.tables.Table; +import com.datastax.astra.client.tables.definition.indexes.TableIndexDescriptor; +import com.datastax.astra.client.tables.definition.rows.Row; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/infos") +@Tag(name = "Data API", description = "Database metadata endpoints") +public class InfoController { + + private static final Logger LOGGER = LoggerFactory.getLogger(InfoController.class); + + private final Database database; + + public InfoController(Database database) { + this.database = database; + } + + @GetMapping + @Operation(summary = "Get current database metadata") + public Map getDatabaseInfo() { + LOGGER.info("Fetching database metadata"); + + DatabaseOptions options = database.getOptions(); + DatabaseAdmin databaseAdmin = database.getDatabaseAdmin(); + + List tables = database.listTableNames(); + Map> indexesByTable = new LinkedHashMap<>(); + for (String tableName : tables) { + try { + Table table = database.getTable(tableName); + List indexNames = table.listIndexes().stream() + .map(TableIndexDescriptor::getName) + .toList(); + indexesByTable.put(tableName, indexNames); + } catch (Exception exception) { + LOGGER.warn("Unable to list indexes for table {}", tableName, exception); + indexesByTable.put(tableName, List.of()); + } + } + + Map response = new LinkedHashMap<>(); + response.put("databaseName", extractDatabaseName(database)); + response.put("databaseEndpoint", extractEndpoint(database)); + response.put("currentKeyspace", options.getKeyspace()); + response.put("keyspaces", databaseAdmin.listKeyspaceNames()); + response.put("collections", database.listCollectionNames()); + response.put("tables", tables); + response.put("types", database.listTypeNames()); + response.put("indexes", indexesByTable); + return response; + } + + private String extractEndpoint(Database database) { + return database.getRootEndpoint(); + } + + private String extractDatabaseName(Database database) { + try { + URI uri = URI.create(database.getRootEndpoint()); + String host = uri.getHost(); + if (host == null || host.isBlank()) { + return "unknown"; + } + return host.split("\\.")[0]; + } catch (Exception exception) { + LOGGER.warn("Unable to extract database name from endpoint", exception); + return "unknown"; + } + } +} diff --git a/samples/sample-spring-boot3x/src/main/resources/application.yaml b/samples/sample-spring-boot3x/src/main/resources/application.yaml index 573a86a4..f2e0bb89 100644 --- a/samples/sample-spring-boot3x/src/main/resources/application.yaml +++ b/samples/sample-spring-boot3x/src/main/resources/application.yaml @@ -1,3 +1,8 @@ +spring: + output: + ansi: + enabled: ALWAYS + server: port: 8081 forward-headers-strategy: framework @@ -10,30 +15,30 @@ logging: astra: data-api: - token: "token" - endpoint-url: "url" + token: ${ASTRA_DB_APPLICATION_TOKEN} + endpoint-url: https://cad896f4-49e4-4b55-bce3-36dd290e4b9b-us-east-2.apps.astra.datastax.com keyspace: default_keyspace - destination: "ASTRA" - # Would Create the Keyspace if not exists schema-action: CREATE_IF_NOT_EXISTS - log-request: true + + #destination: "ASTRA" + # Would Create the Keyspace if not exists options: # Provide API key for embedding services (OpenAI, Cohere, etc.) - embedding-api-key: "your-embedding-api-key-here" + #embedding-api-key: "your-embedding-api-key-here" # Provide API key for reranking services - rerank-api-key: "your-rerank-api-key-here" + #rerank-api-key: "your-rerank-api-key-here" # HTTP Client Options - http: - retry-count: 3 - retry-delay: 100 + #http: + #retry-count: 3 + #retry-delay: 100 # HTTP protocol settings, Default: HTTP_2 # Options for httpVersion: HTTP_1_1, HTTP_2 - version: "HTTP_2" + #version: "HTTP_2" # HTTP redirect policy, Default: NORMAL # Options: NEVER, ALWAYS, NORMAL - redirect: "NORMAL" + #redirect: "NORMAL" # HTTP Proxy configuration (optional) #proxy: @@ -60,42 +65,42 @@ astra: general: 30000 # Database admin timeout (create, delete, list databases) # Default: 600000ms (10 minutes) - dbAdmin: 600000 + #dbAdmin: 600000 # Keyspace admin timeout (create, delete, list keyspaces) # Default: 30000ms (30 seconds) - keyspaceAdmin: 30000 + #keyspaceAdmin: 30000 # Collection admin timeout (create, alter, drop collections) # Default: 60000ms (1 minute) - collectionAdmin: 60000 + # collectionAdmin: 60000 # Table admin timeout (create, alter, drop tables) # Default: 30000ms (30 seconds) - tableAdmin: 30000 + #tableAdmin: 30000 # Additional headers - headers: - db: - "X-Custom-Header": "custom-value" - "X-Request-ID": "request-123" - "Feature-Flag-tables": "true" - admin: - "X-Admin-Header": "admin-value" - "X-Tenant-ID": "tenant-123" + #headers: + # db: + # "X-Custom-Header": "custom-value" + # "X-Request-ID": "request-123" + # "Feature-Flag-tables": "true" + # admin: + # "X-Admin-Header": "admin-value" + # "X-Tenant-ID": "tenant-123" # Observers - observers: + #observers: # Built-in logging observer - - type: "logging" - name: "LoggingCommandObserver" - enabled: true + # - type: "logging" + # name: "LoggingCommandObserver" + # enabled: true # Custom observer example - - type: "custom" - name: "MetricsObserver" - className: "com.example.MetricsObserver" - enabled: true + # - type: "custom" + # name: "MetricsObserver" + # className: "com.example.MetricsObserver" + # enabled: true # Serialization/Deserialization Options - serdes: + # serdes: # Encode Duration objects as ISO8601 strings (Default: true) - encodeDurationAsISO8601: true + # encodeDurationAsISO8601: true # Encode DataAPIVector objects as Base64 (Default: true) - encodeDataApiVectorsAsBase64: true \ No newline at end of file + # encodeDataApiVectorsAsBase64: true diff --git a/samples/sample-spring-boot3x/src/main/resources/banner.txt b/samples/sample-spring-boot3x/src/main/resources/banner.txt index a3e8d980..d4065fea 100644 --- a/samples/sample-spring-boot3x/src/main/resources/banner.txt +++ b/samples/sample-spring-boot3x/src/main/resources/banner.txt @@ -1,8 +1,10 @@ +${AnsiColor.BRIGHT_CYAN} ______ ______ +${AnsiColor.BRIGHT_CYAN} _/ Y \_ +${AnsiColor.BRIGHT_CYAN} // ~~ ~~ | ~~ ~ \\ CRUD with BOOKS +${AnsiColor.BRIGHT_CYAN} // ~ ~ ~~ | ~~~ ~~ \\ ${AnsiColor.BRIGHT_GREEN}Data API Spring Boot Demo +${AnsiColor.BRIGHT_CYAN} //________.|.________\\ ${AnsiColor.BRIGHT_MAGENTA}Build with ❤️ by @clun +${AnsiColor.BRIGHT_CYAN}`----------`-'----------'${AnsiColor.DEFAULT} - _____ __ ________ - / _ \ _______/ |_____________ \______ \ ____ _____ ____ - / /_\ \ / ___/\ __\_ __ \__ \ | | \_/ __ \ / \ / _ \ -/ | \\___ \ | | | | \// __ \_ | ` \ ___/| Y Y ( <_> ) -\____|__ /____ > |__| |__| (____ / /_______ /\___ >__|_| /\____/ - \/ \/ \/ \/ \/ \/ - +${AnsiColor.BRIGHT_GREEN} 📘 Books API ${AnsiColor.DEFAULT}-> http://localhost:8081/api/v1/books +${AnsiColor.BRIGHT_GREEN} ℹ️ Swagger UI ${AnsiColor.DEFAULT}-> http://localhost:8081/swagger-ui/index.html +${AnsiColor.BRIGHT_GREEN} 📍 OpenAPI ${AnsiColor.DEFAULT}-> http://localhost:8081/v3/api-docs diff --git a/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml b/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..245b7436 --- /dev/null +++ b/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/tool-import-csv/src/test/resources/philosopher-quotes.csv b/tools/tool-import-csv/src/test/resources/philosopher-quotes.csv index 2260ef91..00de4e2d 100644 --- a/tools/tool-import-csv/src/test/resources/philosopher-quotes.csv +++ b/tools/tool-import-csv/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 From cf0c68001a5abf40f524e9ee371449827cebd478 Mon Sep 17 00:00:00 2001 From: Cedrick Lunven Date: Fri, 17 Apr 2026 22:34:03 +0200 Subject: [PATCH 03/10] full --- .bob/notes/pending-notes.txt | 119 +----------------- .../DataAPIAnnotationIntrospector.java | 81 ++++++++++++ .../collections/DocumentSerializer.java | 4 +- .../DataApiCollectionCrudRepository.java | 4 +- .../src/main/resources/application.yaml | 1 + .../src/main/resources/logback-spring.xml | 3 +- 6 files changed, 90 insertions(+), 122 deletions(-) create mode 100644 astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DataAPIAnnotationIntrospector.java diff --git a/.bob/notes/pending-notes.txt b/.bob/notes/pending-notes.txt index 56cc2c3d..7faa1c8f 100644 --- a/.bob/notes/pending-notes.txt +++ b/.bob/notes/pending-notes.txt @@ -1,116 +1,3 @@ -{"id":"31554695-6df5-4273-b89b-b17b64b25c53","ts":"2026-04-15T17:04:20.251Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"ec6a8e85-75a5-44d7-8836-fe92551619f6","ts":"2026-04-15T17:04:32.101Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/SchemaAction.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"4596bd4b-5a88-446b-bb7c-3e2bbe8e84c4","ts":"2026-04-15T17:04:38.380Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIClientProperties.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"69350581-a1f2-474b-bcd0-30617358038f","ts":"2026-04-15T17:05:01.518Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/src/main/java/com/datastax/astra/boot/autoconfigure/DataAPIAutoConfiguration.java","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"694eaad5-abd0-4026-a10f-fad0b2b8d5ce","ts":"2026-04-15T17:19:35.629Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml","version":"1.0.0","taskID":"23d1efe4-b760-42a3-a4f1-2028bfa7b7ae"} -{"id":"c46457a2-1a2d-44ef-89ad-a6cd1e119053","ts":"2026-04-16T11:15:06.636Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"eb3e1efd-f4a8-46fd-ae85-dc6b1a9a1230","ts":"2026-04-16T11:15:14.086Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"c1087d22-91b0-404e-98b9-3e474b4f3213","ts":"2026-04-16T11:15:22.349Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"e39724cc-df69-43a9-a419-6138af7e7b3f","ts":"2026-04-16T11:15:34.879Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"722bda18-6c74-4e67-b202-f4d0c7f68516","ts":"2026-04-16T11:15:48.219Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"566386d4-40b1-423a-888a-f5dd60d2ddae","ts":"2026-04-16T11:15:54.022Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"2a3b092c-59a0-4e2e-9524-d301e583f3bd","ts":"2026-04-16T11:16:05.144Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"53bb9919-a9f4-4f89-a91d-7d24b862c2a5","ts":"2026-04-16T11:16:16.029Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"c727eb97-2f4b-43b7-bfe8-7fc9ebd5ef1b","ts":"2026-04-16T11:16:30.223Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"06eb1c48-29c0-49e2-a8d1-9b49891f5803","ts":"2026-04-16T11:16:38.681Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"ae68cae8-e712-4958-bac1-93273e8b541a","ts":"2026-04-16T11:16:45.924Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"48270324-7736-4d50-9785-ffbe89fafa14","ts":"2026-04-16T11:16:51.898Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/databases/Database.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"75632aa0-993f-44ba-8b25-5289bc32f7bb","ts":"2026-04-16T11:19:42.271Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Book.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"82bbcdd2-1d64-4e5a-aeff-cebfe64ed010","ts":"2026-04-16T11:19:58.352Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"764d2974-32f8-49f9-b89f-9d4284c93f23","ts":"2026-04-16T11:20:17.509Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractCollectionIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"77eac65f-de02-4930-b9a7-3266376c9e6b","ts":"2026-04-16T11:26:21.956Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"db9be7a6-6dcd-4c82-b50b-83f3fc151a8a","ts":"2026-04-16T11:26:52.514Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"dc083945-6b27-47c0-a4e5-06145329065b","ts":"2026-04-16T11:50:46.227Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"216b0bfa-6ab6-49c0-b5b3-b3cb9081a528","ts":"2026-04-16T11:51:07.121Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"f29a3189-719e-400e-a9b8-3c3817472412","ts":"2026-04-16T11:51:29.140Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"f464d7a7-bc82-4964-87e5-501aeacdd953","ts":"2026-04-16T11:51:45.420Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"3a5fe801-3f62-4b38-b9f3-7d1e480de616","ts":"2026-04-16T11:51:58.614Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"669b7e5d-ea45-4ff3-90c3-e5f09e05cbac","ts":"2026-04-16T11:52:25.469Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/pom.xml","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"4a9e15dd-f591-4eb6-8b71-2f72b034e761","ts":"2026-04-16T12:30:51.060Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"3eef00d6-0613-4dec-9213-4ec6ca29454a","ts":"2026-04-16T12:32:24.973Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"35f3fd69-851e-4302-a5f6-96f3a75d2343","ts":"2026-04-16T12:32:48.380Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"b9499f4a-3c14-4b03-9d9d-11a24647c2af","ts":"2026-04-16T12:33:07.817Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"c926c73d-4487-4362-b964-18ef2cd0feab","ts":"2026-04-16T12:33:18.445Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"c5a0c13f-eecf-4f07-99c1-8e3e7702d3b4","ts":"2026-04-16T12:33:40.714Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"ee87ec6f-cb03-4acc-b2a8-6e664d93aca3","ts":"2026-04-16T12:41:55.971Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKeyClass.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"18441a47-53a8-49d5-930e-f3c1d96cd009","ts":"2026-04-16T12:42:06.537Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/tables/mapping/TablePrimaryKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"647388bd-e86d-4bb0-95fc-399b05dcf940","ts":"2026-04-16T12:43:13.525Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"b0c8de3f-b527-4a61-9fda-9594496cbe1c","ts":"2026-04-16T12:43:59.284Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/README_TABLE_REPOSITORY.md","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"d761d550-dfa0-459d-af6e-42610fc17649","ts":"2026-04-16T12:51:26.448Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"93a4798c-5b1e-4dcb-92a3-7dfd99a7d7e3","ts":"2026-04-16T12:51:40.592Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"7911f8e4-9c19-4f89-ac56-b6996aceb0db","ts":"2026-04-16T12:52:14.328Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"cc9c21b1-ebf2-4222-bf64-5b66ad557fdb","ts":"2026-04-16T12:52:26.188Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"c261a008-d2d8-43cd-880b-ef524408d4aa","ts":"2026-04-16T12:52:43.684Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"62e18708-ac34-423d-8bee-49e6893037ff","ts":"2026-04-16T12:52:49.039Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"250dfe52-9ce5-4a42-8d31-ef9c987f3f30","ts":"2026-04-16T12:53:30.033Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"efe636ef-e47b-456f-9839-96ff7e3c4b99","ts":"2026-04-16T12:54:01.893Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"f7203a10-02f4-45dc-b189-989643f720c0","ts":"2026-04-16T12:54:42.799Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"e2a898cf-5e5f-449c-84e1-e426585b476e","ts":"2026-04-16T12:54:54.535Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"6b43ec04-aca0-429f-abea-c902deb0737e","ts":"2026-04-16T12:55:00.094Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/AbstractTableIT.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"b23fe5c5-f27a-4081-a860-dc0908ca1b32","ts":"2026-04-16T12:55:48.969Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Order.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"73520d00-7bef-4a76-8504-e4c2317ae3ae","ts":"2026-04-16T12:56:57.564Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"58427a01-6d1a-4379-989b-e874dd756911","ts":"2026-04-16T12:57:35.709Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"515eb92f-8d01-4fba-9f3f-83ef103caf13","ts":"2026-04-16T12:57:40.996Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/Order.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"c276caf1-0314-4625-8f8c-b4926b5bffba","ts":"2026-04-16T12:58:02.655Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"7b5ab564-35d8-4964-b598-d9d9f2b5bc5c","ts":"2026-04-16T12:58:08.414Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"8e36afcb-70df-4a7c-82e9-74fa4de90b05","ts":"2026-04-16T12:58:26.033Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/integration/model/OrderKey.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"38ad9160-5ba3-4cdb-b154-9fb4d7708c1e","ts":"2026-04-16T12:58:46.487Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"fdc960d9-c834-4cd6-89dc-e4aa70ec4391","ts":"2026-04-16T12:59:15.984Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassDemo.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"a003915c-1259-4365-b343-791f2d57252a","ts":"2026-04-16T12:59:46.853Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"aeac3ac3-f12d-43ff-a5a4-f4dfe622b3a6","ts":"2026-04-16T12:59:58.026Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/EntityTableBeanDefinition.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"7418bf02-3ec1-4234-9809-77342452aa44","ts":"2026-04-16T13:00:12.361Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"45e81668-7c94-4dee-b3ac-e42016431691"} -{"id":"04305034-4def-44cd-ac00-a98427ea70aa","ts":"2026-04-16T15:03:33.703Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"48bca368-4ce4-4069-aa6c-007fc115c401","ts":"2026-04-16T15:12:20.229Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyDeserializationTest.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"08cb66e0-d5b4-4473-9ffb-39d09330ba91","ts":"2026-04-16T15:12:51.436Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyDeserializationTest.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"463647cc-5f9f-48e2-8391-b56be83f4bdb","ts":"2026-04-16T15:13:55.329Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/tables/RowMapper.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"5979af38-8b04-4abc-b486-b2c9b9c1a623","ts":"2026-04-16T15:14:34.146Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/TablePrimaryKeyClassTest.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"30fdd1eb-cdd2-498b-91b3-ca822d8e74cc","ts":"2026-04-16T16:52:39.827Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"3943ce35-7e42-44c0-9737-7218d0ebc39e","ts":"2026-04-16T16:54:49.970Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/client/tables/definition/TableDefinition.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"f77a3302-0a27-441d-8f34-3c3ce6fcac78","ts":"2026-04-16T16:55:17.594Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/test/java/com/datastax/astra/test/unit/tables/TableDefinitionEqualsTest.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"a1845daa-db62-42d3-9f17-8f1dfdf253a4","ts":"2026-04-16T17:00:30.661Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"ff69dd92-3eb5-4039-ba37-1a0fb224585d","ts":"2026-04-16T17:01:01.508Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"4bfeeb12-ca4b-4289-afa7-74a3c4cad8cc","ts":"2026-04-16T17:01:21.794Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"cec37b99-e5a9-43c1-b143-199c8acbe34f","ts":"2026-04-16T17:17:14.576Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"2229fcfa-3c9d-4060-89a4-1441e7d60a30","ts":"2026-04-16T17:17:27.199Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"c24b6fca-2959-41c0-b374-6cfc75cb02c8","ts":"2026-04-16T17:20:53.390Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiTableCrudRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"7338119c-99cd-4d60-a8d9-ce030c15c4e2","ts":"2026-04-16T17:25:39.144Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/DatabaseInfoController.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"8ef79ebe-43e2-4282-a03d-70b909df6802","ts":"2026-04-16T17:25:53.150Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookRepository.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"078a7a4d-b4ef-4a38-93d8-442826ed8e6b","ts":"2026-04-16T17:26:15.851Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/DatabaseInfoController.java","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"db49d022-a3d3-4d82-b21c-9ade55d439e3","ts":"2026-04-16T17:26:55.052Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/README.md","version":"1.0.0","taskID":"f0cf2500-10ab-4d8d-94e6-ba41457fb2b2"} -{"id":"a699d28f-0040-412e-a8bd-787c754f9a82","ts":"2026-04-17T11:59:00.638Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/reflection/CollectionBeanDefinition.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} -{"id":"61c1af0b-ca1c-4f20-9bb5-0be3101ccf3c","ts":"2026-04-17T11:59:05.182Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} -{"id":"6443efac-e92a-4c64-a27e-bb66be27c146","ts":"2026-04-17T12:10:05.152Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} -{"id":"4d03836c-4fa4-4783-937f-48bbd08ade0f","ts":"2026-04-17T12:10:30.967Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} -{"id":"92a2f77a-2dc2-4c9e-a12d-1c1290d1c2c7","ts":"2026-04-17T12:11:15.838Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} -{"id":"ea5fdc47-f6fa-4dc8-8f7e-2c490e11cc8f","ts":"2026-04-17T12:13:21.027Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiSpringQueryMapper.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} -{"id":"47f1b4ab-d3e8-44fe-95a0-90ee9ece6e94","ts":"2026-04-17T12:17:24.308Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} -{"id":"fecd24c0-a581-44cc-8745-3d3284967a99","ts":"2026-04-17T12:22:10.879Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"f941af27-c704-41eb-b855-fbad920d90b3"} -{"id":"50a4ef32-9da7-4852-9f2f-7f365719039f","ts":"2026-04-17T13:13:14.062Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"8480e585-16dd-4e3f-8d11-3b4ff31ef32d","ts":"2026-04-17T13:13:42.087Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-autoconfigure/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"263b9fe3-a1db-48c9-a8db-591a0f8aaa22","ts":"2026-04-17T13:14:56.542Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"11faa163-7a35-4ee4-8e45-367721484848","ts":"2026-04-17T13:18:31.584Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/HttpClientWrapper.java","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"7ca1502a-75f0-4567-939a-e759ec9aba9c","ts":"2026-04-17T13:19:00.643Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/observability/ApiExecutionInfos.java","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"87f5771a-422a-4aaa-a9f3-7937c567ee39","ts":"2026-04-17T13:19:07.829Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"b578777d-ad9f-4412-8e58-a69bbad1401e","ts":"2026-04-17T13:19:41.290Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/TestUtils.java","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"c25f2dd5-4356-4365-83cb-0cddc6c791d9","ts":"2026-04-17T13:20:44.475Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"714f5165-5ec3-4260-8748-dd2cbc60b3d6","ts":"2026-04-17T13:21:02.677Z","path":"/Users/cedricklunven/dev/astra-db-java/pom.xml","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"b013d198-3d3b-4056-9512-d434d5e58de7","ts":"2026-04-17T13:23:43.279Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/db/CdcClientTest.java","version":"1.0.0","taskID":"eae28fa6-1f78-49d0-84fd-960ab51d9bbf"} -{"id":"956bd2a1-edaa-4d25-899a-2c0e45ca79ea","ts":"2026-04-17T13:32:08.857Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/pom.xml","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"31aeba2c-0298-41f5-91b1-2680c88a9264","ts":"2026-04-17T13:32:17.201Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"e86ca3da-30f1-413a-a4fc-999e24e91f74","ts":"2026-04-17T13:32:29.038Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"88d9c020-c2d8-4a4e-87c7-ab82862646bb","ts":"2026-04-17T13:32:39.226Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"8c26b9c4-1a61-41aa-ac93-88aaaa762c1f","ts":"2026-04-17T13:32:57.317Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/DatabaseInfoController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"c1897810-7087-4dc8-a1dc-6b1110e76670","ts":"2026-04-17T13:34:25.654Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/DatabaseInfoController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"7ecc8260-a5bf-4c01-b843-ca459140813e","ts":"2026-04-17T13:34:42.669Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/Book.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"5161c44a-e7c3-45dc-9510-a7ba529fb119","ts":"2026-04-17T13:39:50.039Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"19745f68-7f14-4c0b-b7db-1642c06a986a","ts":"2026-04-17T13:40:49.612Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/application.yaml","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"280106ec-575e-4ea4-9d34-111832b03a48","ts":"2026-04-17T13:42:18.124Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"a6e098cd-f84c-4ea1-95f9-a79c1718262d","ts":"2026-04-17T13:45:31.055Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/banner.txt","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"ad0fa13a-477e-46e6-9b77-fc39df3b0e0f","ts":"2026-04-17T13:53:16.814Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"73a9e1eb-132f-4fcb-9677-27d6519dc5e1","ts":"2026-04-17T14:03:00.644Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"9996940e-7b29-4e39-8f83-fe856f42da57","ts":"2026-04-17T14:03:06.720Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/DataApiStarterSpringBootApplication.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"9939310d-178d-4232-ac7d-415b7b9d8d60","ts":"2026-04-17T14:06:18.675Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookService.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"8d62b81e-3fdb-423d-a9af-2adc5697ca08","ts":"2026-04-17T14:06:33.241Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"cd009b14-44af-4cc2-bdb1-4fc2bfed50be","ts":"2026-04-17T14:11:14.403Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"f9559078-6a7b-473e-b319-413810ae6644","ts":"2026-04-17T14:25:45.649Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/books/BookVectorSearchRequest.java","version":"1.0.0","taskID":"68bd0b25-9703-4494-9ac0-327372ecd691"} -{"id":"dcb595ed-ca97-4992-bcbe-ab47f9e43855","ts":"2026-04-17T14:42:21.809Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java","version":"1.0.0","taskID":"e52187da-c542-42a0-9c51-3c63c37571f1"} +{"id":"7a7d9a1b-85de-4483-8732-008a4b22a572","ts":"2026-04-17T16:59:31.240Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} +{"id":"dc628fad-b328-4023-8e61-4d8126e03128","ts":"2026-04-17T18:22:56.718Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DataAPIAnnotationIntrospector.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} +{"id":"ea271fa1-98f8-4eec-83af-5182764d0561","ts":"2026-04-17T18:24:29.870Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DocumentSerializer.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} diff --git a/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DataAPIAnnotationIntrospector.java b/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DataAPIAnnotationIntrospector.java new file mode 100644 index 00000000..6400c88a --- /dev/null +++ b/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DataAPIAnnotationIntrospector.java @@ -0,0 +1,81 @@ +package com.datastax.astra.internal.serdes.collections; + +/*- + * #%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.Lexical; +import com.datastax.astra.client.collections.mapping.Vector; +import com.datastax.astra.client.collections.mapping.Vectorize; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; + +/** + * Custom Jackson annotation introspector for Data API specific annotations. + *

+ * This introspector handles special field name mappings for Data API annotations: + *

    + *
  • {@link Vectorize} - serialized as "$vectorize"
  • + *
  • {@link Lexical} - serialized as "$lexical"
  • + *
  • {@link Vector} - serialized as "$vector"
  • + *
+ *

+ */ +public class DataAPIAnnotationIntrospector extends JacksonAnnotationIntrospector { + + /** + * Default constructor. + */ + public DataAPIAnnotationIntrospector() { + super(); + } + + /** + * Overrides the default field name resolution to handle Data API specific annotations. + *

+ * Fields annotated with {@link Vectorize}, {@link Lexical}, or {@link Vector} will be + * serialized with the appropriate "$" prefix. + *

+ * + * @param a the annotated field + * @param name the default property name + * @return the property name to use for serialization, with "$" prefix if applicable + */ + @Override + public PropertyName findNameForSerialization(Annotated a) { + // Check for @Vectorize annotation + if (a.hasAnnotation(Vectorize.class)) { + return PropertyName.construct("$vectorize"); + } + + // Check for @Lexical annotation + if (a.hasAnnotation(Lexical.class)) { + return PropertyName.construct("$lexical"); + } + + // Check for @Vector annotation + if (a.hasAnnotation(Vector.class)) { + return PropertyName.construct("$vector"); + } + + // Fall back to default behavior + return super.findNameForSerialization(a); + } +} diff --git a/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DocumentSerializer.java b/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DocumentSerializer.java index 64af4415..746437d1 100644 --- a/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DocumentSerializer.java +++ b/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DocumentSerializer.java @@ -59,7 +59,7 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.datastax.astra.internal.serdes.collections.DataAPIAnnotationIntrospector; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -108,7 +108,7 @@ public ObjectMapper getMapper() { .setDateFormat(new SimpleDateFormat("dd/MM/yyyy")) // Update 2.2, null fields are required to empty values //.setSerializationInclusion(Include.NON_NULL) - .setAnnotationIntrospector(new JacksonAnnotationIntrospector()); + .setAnnotationIntrospector(new DataAPIAnnotationIntrospector()); SimpleModule module = new SimpleModule(); diff --git a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java index b64aed81..735c5d5c 100644 --- a/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java +++ b/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java @@ -138,7 +138,7 @@ protected void init() { log.info("Detected schema action CREATE_IF_NOT_EXISTS, ensuring collection {} exists...", collectionName); if (!database.collectionExists(collectionName)) { log.info("Collection '{}' does not exist, creating it...", collectionName); - CollectionDefinition expected = database.getCollection(collectionName, documentClass).getDefinition(); + CollectionDefinition expected = beanDefinition.buildCollectionDefinition(); database.createCollection(collectionName, expected, documentClass); log.info("Collection '{}' created successfully", collectionName); } else { @@ -150,7 +150,7 @@ protected void init() { throw new IllegalArgumentException("Collection '" + collectionName + "' does not exist"); } else { CollectionDefinition existing = database.getCollection(collectionName).getDefinition(); - CollectionDefinition expected = database.getCollection(collectionName, documentClass).getDefinition(); + CollectionDefinition expected = beanDefinition.buildCollectionDefinition(); // Compare collection definitions if (!existing.equals(expected)) { diff --git a/samples/sample-spring-boot3x/src/main/resources/application.yaml b/samples/sample-spring-boot3x/src/main/resources/application.yaml index f2e0bb89..c9b5349b 100644 --- a/samples/sample-spring-boot3x/src/main/resources/application.yaml +++ b/samples/sample-spring-boot3x/src/main/resources/application.yaml @@ -11,6 +11,7 @@ logging: level: org.springframework.web: WARN com.ibm.astra.demo: INFO + com.datastax.astra.client: DEBUG root: WARN astra: diff --git a/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml b/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml index 245b7436..27e0d879 100644 --- a/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml +++ b/samples/sample-spring-boot3x/src/main/resources/logback-spring.xml @@ -25,9 +25,8 @@ - + - From 7accdb41252aa1017fff740522eacfc861eeb178 Mon Sep 17 00:00:00 2001 From: Cedrick Lunven Date: Mon, 20 Apr 2026 09:46:34 +0200 Subject: [PATCH 04/10] starter --- .bob/notes/pending-notes.txt | 5 +- samples/sample-spring-boot3x/pom.xml | 13 --- .../astra/demo/controller/BookController.java | 2 +- .../astra/demo/controller/HomeController.java | 76 ++++++++++++++++ .../astra/demo/controller/InfoController.java | 87 ------------------- ...aApiStarterSpringBootApplicationTests.java | 58 ------------- 6 files changed, 79 insertions(+), 162 deletions(-) delete mode 100644 samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/InfoController.java delete mode 100644 samples/sample-spring-boot3x/src/test/java/com/ibm/astra/demo/DataApiStarterSpringBootApplicationTests.java diff --git a/.bob/notes/pending-notes.txt b/.bob/notes/pending-notes.txt index 7faa1c8f..32dbe910 100644 --- a/.bob/notes/pending-notes.txt +++ b/.bob/notes/pending-notes.txt @@ -1,3 +1,2 @@ -{"id":"7a7d9a1b-85de-4483-8732-008a4b22a572","ts":"2026-04-17T16:59:31.240Z","path":"/Users/cedricklunven/dev/astra-db-java/integrations/data-api-spring-boot-3x-starter/src/main/java/com/datastax/astra/spring/DataApiCollectionCrudRepository.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} -{"id":"dc628fad-b328-4023-8e61-4d8126e03128","ts":"2026-04-17T18:22:56.718Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DataAPIAnnotationIntrospector.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} -{"id":"ea271fa1-98f8-4eec-83af-5182764d0561","ts":"2026-04-17T18:24:29.870Z","path":"/Users/cedricklunven/dev/astra-db-java/astra-db-java/src/main/java/com/datastax/astra/internal/serdes/collections/DocumentSerializer.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} +{"id":"a444944b-d1d4-4a03-814f-0dcd321fd968","ts":"2026-04-17T20:37:20.183Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} +{"id":"46e8eaeb-0c6e-416d-885f-d17c36bfa652","ts":"2026-04-17T21:26:50.143Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} diff --git a/samples/sample-spring-boot3x/pom.xml b/samples/sample-spring-boot3x/pom.xml index aa31947e..d71310ff 100644 --- a/samples/sample-spring-boot3x/pom.xml +++ b/samples/sample-spring-boot3x/pom.xml @@ -52,19 +52,6 @@ ${springdoc.version}
- - org.springframework.boot - spring-boot-starter-test - test - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java index b35e555a..36c780d5 100644 --- a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java @@ -27,7 +27,7 @@ import java.util.NoSuchElementException; @RestController -@RequestMapping("/api/v1/books") +@RequestMapping("/books") @Tag(name = "Books", description = "CRUD REST API for books") public class BookController { diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java index ce70e6e7..a98dc0e0 100644 --- a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java +++ b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java @@ -1,16 +1,92 @@ package com.ibm.astra.demo.controller; +import com.datastax.astra.client.admin.DatabaseAdmin; +import com.datastax.astra.client.databases.Database; +import com.datastax.astra.client.databases.DatabaseOptions; +import com.datastax.astra.client.tables.Table; +import com.datastax.astra.client.tables.definition.indexes.TableIndexDescriptor; +import com.datastax.astra.client.tables.definition.rows.Row; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; @Controller @Tag(name = "Home", description = "Home and utility endpoints") public class HomeController { + private static final Logger LOGGER = LoggerFactory.getLogger(HomeController.class); + + private final Database database; + + public HomeController(Database database) { + this.database = database; + } + @GetMapping("/") public String redirectToSwaggerUi() { return "redirect:/swagger-ui/index.html"; } + @GetMapping("/status") + @ResponseBody + @Operation(summary = "Get current database metadata") + public Map getDatabaseInfo() { + LOGGER.info("Fetching database metadata"); + + DatabaseOptions options = database.getOptions(); + DatabaseAdmin databaseAdmin = database.getDatabaseAdmin(); + + List tables = database.listTableNames(); + Map> indexesByTable = new LinkedHashMap<>(); + for (String tableName : tables) { + try { + Table table = database.getTable(tableName); + List indexNames = table.listIndexes().stream() + .map(TableIndexDescriptor::getName) + .toList(); + indexesByTable.put(tableName, indexNames); + } catch (Exception exception) { + LOGGER.warn("Unable to list indexes for table {}", tableName, exception); + indexesByTable.put(tableName, List.of()); + } + } + + Map response = new LinkedHashMap<>(); + response.put("databaseName", extractDatabaseName(database)); + response.put("databaseEndpoint", extractEndpoint(database)); + response.put("currentKeyspace", options.getKeyspace()); + response.put("keyspaces", databaseAdmin.listKeyspaceNames()); + response.put("collections", database.listCollectionNames()); + response.put("tables", tables); + response.put("types", database.listTypeNames()); + response.put("indexes", indexesByTable); + return response; + } + + private String extractEndpoint(Database database) { + return database.getRootEndpoint(); + } + + private String extractDatabaseName(Database database) { + try { + URI uri = URI.create(database.getRootEndpoint()); + String host = uri.getHost(); + if (host == null || host.isBlank()) { + return "unknown"; + } + return host.split("\\.")[0]; + } catch (Exception exception) { + LOGGER.warn("Unable to extract database name from endpoint", exception); + return "unknown"; + } + } } diff --git a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/InfoController.java b/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/InfoController.java deleted file mode 100644 index 110f4341..00000000 --- a/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/InfoController.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.ibm.astra.demo.controller; - -import com.datastax.astra.client.admin.DatabaseAdmin; -import com.datastax.astra.client.databases.Database; -import com.datastax.astra.client.databases.DatabaseOptions; -import com.datastax.astra.client.tables.Table; -import com.datastax.astra.client.tables.definition.indexes.TableIndexDescriptor; -import com.datastax.astra.client.tables.definition.rows.Row; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.net.URI; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/infos") -@Tag(name = "Data API", description = "Database metadata endpoints") -public class InfoController { - - private static final Logger LOGGER = LoggerFactory.getLogger(InfoController.class); - - private final Database database; - - public InfoController(Database database) { - this.database = database; - } - - @GetMapping - @Operation(summary = "Get current database metadata") - public Map getDatabaseInfo() { - LOGGER.info("Fetching database metadata"); - - DatabaseOptions options = database.getOptions(); - DatabaseAdmin databaseAdmin = database.getDatabaseAdmin(); - - List tables = database.listTableNames(); - Map> indexesByTable = new LinkedHashMap<>(); - for (String tableName : tables) { - try { - Table table = database.getTable(tableName); - List indexNames = table.listIndexes().stream() - .map(TableIndexDescriptor::getName) - .toList(); - indexesByTable.put(tableName, indexNames); - } catch (Exception exception) { - LOGGER.warn("Unable to list indexes for table {}", tableName, exception); - indexesByTable.put(tableName, List.of()); - } - } - - Map response = new LinkedHashMap<>(); - response.put("databaseName", extractDatabaseName(database)); - response.put("databaseEndpoint", extractEndpoint(database)); - response.put("currentKeyspace", options.getKeyspace()); - response.put("keyspaces", databaseAdmin.listKeyspaceNames()); - response.put("collections", database.listCollectionNames()); - response.put("tables", tables); - response.put("types", database.listTypeNames()); - response.put("indexes", indexesByTable); - return response; - } - - private String extractEndpoint(Database database) { - return database.getRootEndpoint(); - } - - private String extractDatabaseName(Database database) { - try { - URI uri = URI.create(database.getRootEndpoint()); - String host = uri.getHost(); - if (host == null || host.isBlank()) { - return "unknown"; - } - return host.split("\\.")[0]; - } catch (Exception exception) { - LOGGER.warn("Unable to extract database name from endpoint", exception); - return "unknown"; - } - } -} diff --git a/samples/sample-spring-boot3x/src/test/java/com/ibm/astra/demo/DataApiStarterSpringBootApplicationTests.java b/samples/sample-spring-boot3x/src/test/java/com/ibm/astra/demo/DataApiStarterSpringBootApplicationTests.java deleted file mode 100644 index 6febac51..00000000 --- a/samples/sample-spring-boot3x/src/test/java/com/ibm/astra/demo/DataApiStarterSpringBootApplicationTests.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.ibm.astra.demo; - -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration test for Spring Boot application startup and REST API. - * - * @author Cedrick LUNVEN (@clunven) - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class DataApiStarterSpringBootApplicationTests { - - private static final Logger LOGGER = LoggerFactory.getLogger(DataApiStarterSpringBootApplicationTests.class); - - @LocalServerPort - private int port; - - @Autowired - private TestRestTemplate restTemplate; - - @Test - void contextLoads() { - LOGGER.info("=".repeat(80)); - LOGGER.info("✅ Spring Boot Application Context loaded successfully!"); - LOGGER.info("=".repeat(80)); - assertThat(restTemplate).isNotNull(); - } - - @Test - void testHelloEndpoint() { - LOGGER.info("Testing hello endpoint at port: {}", port); - - String url = "http://localhost:" + port + "/api/hello"; - ResponseEntity response = restTemplate.getForEntity(url, Map.class); - - LOGGER.info("Response status: {}", response.getStatusCode()); - LOGGER.info("Response body: {}", response.getBody()); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody().get("message")).isEqualTo("Hello from DataAPI Spring Boot!"); - assertThat(response.getBody().get("status")).isEqualTo("running"); - - LOGGER.info("✅ Hello endpoint test passed!"); - } -} From ab92ff0b0ec8e16136a845a41f171e909d009d7a Mon Sep 17 00:00:00 2001 From: Cedrick Lunven Date: Mon, 20 Apr 2026 14:32:21 +0200 Subject: [PATCH 05/10] documentation --- .bob/notes/pending-notes.txt | 3 +- .../OBJECT-MAPPING-GUIDE.md | 671 ++++++++++++++++++ 2 files changed, 672 insertions(+), 2 deletions(-) create mode 100644 samples/sample-spring-boot3x/OBJECT-MAPPING-GUIDE.md diff --git a/.bob/notes/pending-notes.txt b/.bob/notes/pending-notes.txt index 32dbe910..93d2be1c 100644 --- a/.bob/notes/pending-notes.txt +++ b/.bob/notes/pending-notes.txt @@ -1,2 +1 @@ -{"id":"a444944b-d1d4-4a03-814f-0dcd321fd968","ts":"2026-04-17T20:37:20.183Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/HomeController.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} -{"id":"46e8eaeb-0c6e-416d-885f-d17c36bfa652","ts":"2026-04-17T21:26:50.143Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/src/main/java/com/ibm/astra/demo/controller/BookController.java","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} +{"id":"c6f32059-a429-4062-9f8d-4269b58cd9e7","ts":"2026-04-20T12:31:04.159Z","path":"/Users/cedricklunven/dev/astra-db-java/samples/sample-spring-boot3x/OBJECT-MAPPING-GUIDE.md","version":"1.0.0","taskID":"24aa1015-e426-48cc-9669-8e6b15e362b4"} diff --git a/samples/sample-spring-boot3x/OBJECT-MAPPING-GUIDE.md b/samples/sample-spring-boot3x/OBJECT-MAPPING-GUIDE.md new file mode 100644 index 00000000..2ce2f36f --- /dev/null +++ b/samples/sample-spring-boot3x/OBJECT-MAPPING-GUIDE.md @@ -0,0 +1,671 @@ +# Object Mapping and Spring Data Integration Guide + +This guide explains how to use the DataStax Astra DB Java SDK's object mapping features and Spring Data integration in your Spring Boot applications. + +## Table of Contents + +- [Overview](#overview) +- [Core Annotations](#core-annotations) + - [@DataApiCollection](#dataapicollection) + - [@DocumentId](#documentid) + - [@Vectorize](#vectorize) + - [@Lexical](#lexical) + - [@Vector](#vector) +- [Spring Data Integration](#spring-data-integration) +- [Configuration](#configuration) +- [Complete Example](#complete-example) +- [Best Practices](#best-practices) + +## Overview + +The Astra DB Java SDK provides a powerful object mapping framework that allows you to work with Java objects instead of raw documents. Combined with Spring Data integration, you get a familiar repository pattern for database operations. + +## Core Annotations + +### @DataApiCollection + +The `@DataApiCollection` annotation marks a class as a collection document and configures collection-level settings. + +**Location:** `com.datastax.astra.client.collections.mapping.DataApiCollection` + +**Properties:** + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `name` | String | Class name (lowercase) | Collection name in the database | +| `defaultIdType` | String | "" | Default ID type (e.g., "uuid", "objectId", "uuidv6", "uuidv7") | +| `indexingDeny` | String[] | {} | Fields to exclude from indexing | +| `indexingAllow` | String[] | {} | Fields to include in indexing (mutually exclusive with `indexingDeny`) | +| `vectorDimension` | int | -1 | Vector dimension size for vector search | +| `vectorSimilarity` | SimilarityMetric | COSINE | Similarity metric (COSINE, DOT_PRODUCT, EUCLIDEAN) | +| `vectorizeProvider` | String | "" | Vectorization service provider (e.g., "openai", "nvidia", "huggingface") | +| `vectorizeModel` | String | "" | Vectorization model name | +| `vectorizeSharedSecret` | String | "" | Shared secret key name for authentication | +| `lexicalEnabled` | boolean | false | Enable lexical (full-text) search | +| `lexicalAnalyzer` | AnalyzerTypes | STANDARD | Analyzer type for lexical search | +| `rerankEnabled` | boolean | false | Enable reranking | +| `rerankProvider` | String | "" | Reranking service provider | +| `rerankModel` | String | "" | Reranking model name | + +**Example:** + +```java +@DataApiCollection( + name = "books", + vectorDimension = 1536, + vectorSimilarity = SimilarityMetric.COSINE, + vectorizeProvider = "openai", + vectorizeModel = "text-embedding-ada-002", + vectorizeSharedSecret = "OPENAI_API_KEY", + lexicalEnabled = true, + indexingDeny = {"internal_field", "temp_data"} +) +public class Book { + // fields... +} +``` + +### @DocumentId + +The `@DocumentId` annotation marks a field as the document's unique identifier (`_id` in the database). + +**Location:** `com.datastax.astra.client.collections.mapping.DocumentId` + +**Rules:** +- Only one field per class can be annotated with `@DocumentId` +- The field is serialized as `_id` in the database +- If not provided during insertion, the server generates a unique ID + +**Example:** + +```java +@DataApiCollection(name = "users") +public class User { + @DocumentId + private String id; + + private String username; + private String email; + + // getters and setters +} +``` + +**Supported ID Types:** +- `String` +- `UUID` +- `ObjectId` +- `UUIDv6` +- `UUIDv7` +- Custom types + +### @Vectorize + +The `@Vectorize` annotation marks a field for automatic vectorization. The field content will be sent to the configured vectorization service to generate embeddings. + +**Location:** `com.datastax.astra.client.collections.mapping.Vectorize` + +**Rules:** +- Only one field per class can be annotated with `@Vectorize` +- The field must be of type `String` +- The field is serialized as `$vectorize` in the database +- Requires `vectorizeProvider` and `vectorizeModel` in `@DataApiCollection` + +**Example:** + +```java +@DataApiCollection( + name = "articles", + vectorDimension = 1536, + vectorizeProvider = "openai", + vectorizeModel = "text-embedding-ada-002", + vectorizeSharedSecret = "OPENAI_API_KEY" +) +public class Article { + @DocumentId + private String id; + + private String title; + + @Vectorize + private String content; // This will be automatically vectorized + + // getters and setters +} +``` + +### @Lexical + +The `@Lexical` annotation marks a field for lexical (full-text) search indexing. + +**Location:** `com.datastax.astra.client.collections.mapping.Lexical` + +**Rules:** +- Only one field per class can be annotated with `@Lexical` +- The field must be of type `String` +- The field is serialized as `$lexical` in the database +- Requires `lexicalEnabled = true` in `@DataApiCollection` + +**Example:** + +```java +@DataApiCollection( + name = "documents", + lexicalEnabled = true, + lexicalAnalyzer = AnalyzerTypes.ENGLISH +) +public class Document { + @DocumentId + private String id; + + @Lexical + private String searchableText; // Indexed for full-text search + + private String metadata; + + // getters and setters +} +``` + +### @Vector + +The `@Vector` annotation marks a field that contains pre-computed vector embeddings. + +**Location:** `com.datastax.astra.client.collections.mapping.Vector` + +**Rules:** +- Only one field per class can be annotated with `@Vector` +- The field must be of type `float[]` or `DataAPIVector` +- The field is serialized as `$vector` in the database +- Use when you compute embeddings yourself (vs. `@Vectorize` for automatic vectorization) + +**Example:** + +```java +@DataApiCollection( + name = "embeddings", + vectorDimension = 768, + vectorSimilarity = SimilarityMetric.COSINE +) +public class Embedding { + @DocumentId + private String id; + + private String text; + + @Vector + private float[] embedding; // Pre-computed vector + + // getters and setters +} +``` + +**Using DataAPIVector:** + +```java +@Vector +private DataAPIVector embedding; + +// Usage +embedding = new DataAPIVector(new float[]{0.1f, 0.2f, 0.3f}); +``` + +## Spring Data Integration + +The SDK provides Spring Data integration through the `DataApiCollectionCrudRepository` interface, which implements Spring's `CrudRepository` and `QueryByExampleExecutor`. + +### Setting Up Dependencies + +Add the Spring Boot starter to your `pom.xml`: + +```xml + + com.datastax.astra + data-api-spring-boot-3x-starter + ${astra-sdk.version} + +``` + +### Creating a Repository + +```java +import com.datastax.astra.spring.DataApiCollectionCrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BookRepository extends DataApiCollectionCrudRepository { + // Spring Data methods are automatically available + // You can also add custom query methods +} +``` + +### Available Methods + +The repository provides all standard Spring Data CRUD operations: + +**Basic CRUD:** +```java +// Create +Book book = new Book(); +book.setTitle("1984"); +book.setAuthor("George Orwell"); +bookRepository.save(book); + +// Read +Optional found = bookRepository.findById("book-1"); +List allBooks = (List) bookRepository.findAll(); + +// Update +book.setTitle("Nineteen Eighty-Four"); +bookRepository.save(book); + +// Delete +bookRepository.deleteById("book-1"); +bookRepository.delete(book); +``` + +**Batch Operations:** +```java +List books = Arrays.asList(book1, book2, book3); +bookRepository.saveAll(books); + +List ids = Arrays.asList("book-1", "book-2"); +List found = (List) bookRepository.findAllById(ids); + +bookRepository.deleteAll(books); +``` + +**Query by Example:** +```java +// Find books by author +Book probe = new Book(); +probe.setAuthor("George Orwell"); +List orwellBooks = (List) bookRepository.findAll(Example.of(probe)); + +// With sorting +Sort sort = Sort.by(Sort.Direction.DESC, "title"); +List sorted = (List) bookRepository.findAll(Example.of(probe), sort); + +// With pagination +Pageable pageable = PageRequest.of(0, 10, Sort.by("title")); +Page page = bookRepository.findAll(Example.of(probe), pageable); +``` + +**Data API Filters:** +```java +import com.datastax.astra.client.core.query.Filter; +import com.datastax.astra.client.core.query.Filters; + +// Using Data API filters directly +Filter filter = Filters.eq("author", "George Orwell"); +List books = (List) bookRepository.findAll(filter); + +// With sorting +Sort sort = Sort.by("title"); +List sorted = (List) bookRepository.findAll(filter, sort); + +// With pagination +Pageable pageable = PageRequest.of(0, 10); +List paged = (List) bookRepository.findAll(filter, pageable); +``` + +## Configuration + +### Application Properties + +Configure the Astra DB connection in `application.yml`: + +```yaml +astra: + data-api: + # Required: Your Astra DB token + token: ${ASTRA_DB_TOKEN} + + # Required: Your database endpoint URL + endpoint-url: ${ASTRA_DB_ENDPOINT} + + # Optional: Keyspace name (default: "default_keyspace") + keyspace: my_keyspace + + # Optional: Schema action (CREATE_IF_NOT_EXISTS, VALIDATE, NONE) + schema-action: CREATE_IF_NOT_EXISTS + + # Optional: Enable request logging + log-request: true + + # Optional: Destination (ASTRA, ASTRA_DEV, ASTRA_TEST, DSE, HCD, CASSANDRA) + destination: ASTRA + + # Optional: Advanced options + options: + # HTTP configuration + http: + retry-count: 3 + retry-delay: 100 + version: HTTP_2 + redirect: NORMAL + + # Timeout configuration (in milliseconds) + timeout: + connect: 5000 + request: 10000 + general: 30000 + collection-admin: 60000 + + # Embedding API key for vectorization + embedding-api-key: ${OPENAI_API_KEY} +``` + +### Logging Configuration + +To see Data API request/response logs, configure logging in `application.yml`: + +```yaml +logging: + level: + com.datastax.astra.client.DataAPIClient: DEBUG +``` + +### Schema Actions + +The `schema-action` property controls how collections are managed: + +- **`CREATE_IF_NOT_EXISTS`** (default): Creates collections if they don't exist +- **`VALIDATE`**: Validates that collections exist and match the schema +- **`NONE`**: No automatic schema management + +## Complete Example + +### 1. Document Class + +```java +package com.example.demo.model; + +import com.datastax.astra.client.collections.mapping.*; +import com.datastax.astra.client.core.vector.SimilarityMetric; +import lombok.Data; + +import java.util.Set; + +@Data +@DataApiCollection( + name = "books", + vectorDimension = 1536, + vectorSimilarity = SimilarityMetric.COSINE, + vectorizeProvider = "openai", + vectorizeModel = "text-embedding-ada-002", + vectorizeSharedSecret = "OPENAI_API_KEY", + lexicalEnabled = true, + lexicalAnalyzer = AnalyzerTypes.ENGLISH +) +public class Book { + + @DocumentId + private String id; + + private String title; + private String author; + private Integer year; + private Set genres; + + @Vectorize + private String description; // Automatically vectorized + + @Lexical + private String fullText; // Indexed for full-text search +} +``` + +### 2. Repository Interface + +```java +package com.example.demo.repository; + +import com.datastax.astra.spring.DataApiCollectionCrudRepository; +import com.example.demo.model.Book; +import org.springframework.stereotype.Repository; + +@Repository +public interface BookRepository extends DataApiCollectionCrudRepository { +} +``` + +### 3. Service Layer + +```java +package com.example.demo.service; + +import com.datastax.astra.client.core.query.Filter; +import com.datastax.astra.client.core.query.Filters; +import com.example.demo.model.Book; +import com.example.demo.repository.BookRepository; +import org.springframework.data.domain.Example; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class BookService { + + private final BookRepository bookRepository; + + public BookService(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + public Book createBook(Book book) { + return bookRepository.save(book); + } + + public Optional findById(String id) { + return bookRepository.findById(id); + } + + public List findByAuthor(String author) { + Book probe = new Book(); + probe.setAuthor(author); + return (List) bookRepository.findAll(Example.of(probe)); + } + + public List findByGenre(String genre) { + Filter filter = Filters.eq("genres", genre); + return (List) bookRepository.findAll(filter); + } + + public List findAll() { + return (List) bookRepository.findAll(); + } + + public void deleteBook(String id) { + bookRepository.deleteById(id); + } +} +``` + +### 4. REST Controller + +```java +package com.example.demo.controller; + +import com.example.demo.model.Book; +import com.example.demo.service.BookService; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/books") +public class BookController { + + private final BookService bookService; + + public BookController(BookService bookService) { + this.bookService = bookService; + } + + @GetMapping + public List getAllBooks() { + return bookService.findAll(); + } + + @GetMapping("/{id}") + public Book getBook(@PathVariable String id) { + return bookService.findById(id) + .orElseThrow(() -> new RuntimeException("Book not found")); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Book createBook(@RequestBody Book book) { + return bookService.createBook(book); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteBook(@PathVariable String id) { + bookService.deleteBook(id); + } +} +``` + +### 5. Application Configuration + +```yaml +# application.yml +astra: + data-api: + token: ${ASTRA_DB_TOKEN} + endpoint-url: ${ASTRA_DB_ENDPOINT} + keyspace: bookstore + schema-action: CREATE_IF_NOT_EXISTS + log-request: true + options: + embedding-api-key: ${OPENAI_API_KEY} + +logging: + level: + com.datastax.astra.client.DataAPIClient: DEBUG +``` + +## Best Practices + +### 1. Annotation Usage + +- **Use only one special annotation per field**: A field can have only one of `@DocumentId`, `@Vectorize`, `@Lexical`, or `@Vector` +- **Choose between `@Vectorize` and `@Vector`**: Use `@Vectorize` for automatic vectorization, `@Vector` for pre-computed embeddings +- **Configure collection settings**: Always specify `vectorDimension` when using vector features + +### 2. ID Management + +```java +// Let the server generate IDs +Book book = new Book(); +book.setTitle("1984"); +bookRepository.save(book); // ID will be auto-generated + +// Or provide your own ID +book.setId("book-1984"); +bookRepository.save(book); +``` + +### 3. Indexing Strategy + +```java +// Index only necessary fields +@DataApiCollection( + name = "products", + indexingAllow = {"name", "category", "price"} // Only index these fields +) + +// Or exclude internal fields +@DataApiCollection( + name = "users", + indexingDeny = {"password_hash", "internal_notes"} // Don't index these +) +``` + +### 4. Vector Search Configuration + +```java +// For OpenAI embeddings +@DataApiCollection( + name = "documents", + vectorDimension = 1536, // text-embedding-ada-002 + vectorizeProvider = "openai", + vectorizeModel = "text-embedding-ada-002", + vectorizeSharedSecret = "OPENAI_API_KEY" +) + +// For NVIDIA embeddings +@DataApiCollection( + name = "documents", + vectorDimension = 1024, // NV-Embed-QA + vectorizeProvider = "nvidia", + vectorizeModel = "NV-Embed-QA", + vectorizeSharedSecret = "NVIDIA_API_KEY" +) +``` + +### 5. Error Handling + +```java +@Service +public class BookService { + + public Book createBook(Book book) { + try { + return bookRepository.save(book); + } catch (DataAPIException e) { + // Handle Data API specific errors + log.error("Failed to create book: {}", e.getMessage()); + throw new ServiceException("Could not create book", e); + } + } +} +``` + +### 6. Testing + +```java +@SpringBootTest +class BookRepositoryTest { + + @Autowired + private BookRepository bookRepository; + + @Test + void testCreateAndFind() { + Book book = new Book(); + book.setTitle("Test Book"); + book.setAuthor("Test Author"); + + Book saved = bookRepository.save(book); + assertNotNull(saved.getId()); + + Optional found = bookRepository.findById(saved.getId()); + assertTrue(found.isPresent()); + assertEquals("Test Book", found.get().getTitle()); + } + + @AfterEach + void cleanup() { + bookRepository.deleteAll(); + } +} +``` + +## Additional Resources + +- [Astra DB Java SDK Documentation](https://docs.datastax.com/en/astra-db-serverless/api-reference/client-sdks.html) +- [Spring Data Documentation](https://spring.io/projects/spring-data) +- [Vector Search Guide](https://docs.datastax.com/en/astra-db-serverless/databases/vector-search.html) +- [Sample Application](https://github.com/datastax/astra-db-java/tree/main/samples/sample-spring-boot3x) + +## Support + +For issues and questions: +- GitHub Issues: https://github.com/datastax/astra-db-java/issues +- Community Forum: https://community.datastax.com/ +- Documentation: https://docs.datastax.com/ From 604c042251a5c3fcf43495aded3d09c18d945130 Mon Sep 17 00:00:00 2001 From: Cedrick Lunven Date: Tue, 21 Apr 2026 13:56:26 +0200 Subject: [PATCH 06/10] fix: Convert sample-hcd from git submodule to regular directory - Removed nested .git repository from samples/sample-hcd - Now tracking all sample-hcd files directly in main repository - Fixes GitHub history showing sample-hcd as a redirect/submodule --- samples/sample-hcd | 1 - samples/sample-hcd/.gitignore | 38 ++ samples/sample-hcd/INSTALL-HCD-LOCAL.MD | 159 +++++++++ samples/sample-hcd/INSTALL-HCD-MC.MD | 333 ++++++++++++++++++ samples/sample-hcd/README.MD | 9 + samples/sample-hcd/pom.xml | 46 +++ .../mc/demo/MissionControlLangchain4j.java | 170 +++++++++ .../demo/MissionControlManualEmbedding.java | 168 +++++++++ .../java/com/ibm/mc/demo/SuperDuperSongs.java | 82 +++++ .../main/java/com/ibm/mc/demo/dto/Lyric.java | 6 + .../main/java/com/ibm/mc/demo/dto/Song.java | 5 + .../src/main/resources/application.sample | 7 + .../sample-hcd/src/main/resources/logback.xml | 38 ++ samples/sample-hcd/z_img/01-sign-in.png | Bin 0 -> 27210 bytes .../sample-hcd/z_img/02-create-cluster.png | Bin 0 -> 52584 bytes samples/sample-hcd/z_img/03-list-clusters.png | Bin 0 -> 92406 bytes .../sample-hcd/z_img/04-create-cluster-01.png | Bin 0 -> 87341 bytes .../sample-hcd/z_img/05-create-cluster-02.png | Bin 0 -> 162013 bytes .../sample-hcd/z_img/06-create-cluster-03.png | Bin 0 -> 192961 bytes .../sample-hcd/z_img/07-create-cluster-04.png | Bin 0 -> 85575 bytes .../sample-hcd/z_img/08-create-gateway-01.png | Bin 0 -> 115796 bytes .../sample-hcd/z_img/09-create-gateway-02.png | Bin 0 -> 101509 bytes .../sample-hcd/z_img/10-create-gateway-03.png | Bin 0 -> 26206 bytes .../sample-hcd/z_img/11-create-gateway-04.png | Bin 0 -> 35538 bytes .../src/main/resources/old.cql | 0 25 files changed, 1061 insertions(+), 1 deletion(-) delete mode 160000 samples/sample-hcd create mode 100644 samples/sample-hcd/.gitignore create mode 100644 samples/sample-hcd/INSTALL-HCD-LOCAL.MD create mode 100644 samples/sample-hcd/INSTALL-HCD-MC.MD create mode 100644 samples/sample-hcd/README.MD create mode 100644 samples/sample-hcd/pom.xml create mode 100644 samples/sample-hcd/src/main/java/com/ibm/mc/demo/MissionControlLangchain4j.java create mode 100644 samples/sample-hcd/src/main/java/com/ibm/mc/demo/MissionControlManualEmbedding.java create mode 100644 samples/sample-hcd/src/main/java/com/ibm/mc/demo/SuperDuperSongs.java create mode 100644 samples/sample-hcd/src/main/java/com/ibm/mc/demo/dto/Lyric.java create mode 100644 samples/sample-hcd/src/main/java/com/ibm/mc/demo/dto/Song.java create mode 100644 samples/sample-hcd/src/main/resources/application.sample create mode 100644 samples/sample-hcd/src/main/resources/logback.xml create mode 100644 samples/sample-hcd/z_img/01-sign-in.png create mode 100644 samples/sample-hcd/z_img/02-create-cluster.png create mode 100644 samples/sample-hcd/z_img/03-list-clusters.png create mode 100644 samples/sample-hcd/z_img/04-create-cluster-01.png create mode 100644 samples/sample-hcd/z_img/05-create-cluster-02.png create mode 100644 samples/sample-hcd/z_img/06-create-cluster-03.png create mode 100644 samples/sample-hcd/z_img/07-create-cluster-04.png create mode 100644 samples/sample-hcd/z_img/08-create-gateway-01.png create mode 100644 samples/sample-hcd/z_img/09-create-gateway-02.png create mode 100644 samples/sample-hcd/z_img/10-create-gateway-03.png create mode 100644 samples/sample-hcd/z_img/11-create-gateway-04.png create mode 100644 samples/sample-openrag-api/src/main/resources/old.cql diff --git a/samples/sample-hcd b/samples/sample-hcd deleted file mode 160000 index b1ace2ed..00000000 --- a/samples/sample-hcd +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b1ace2ed19caa9aefae0f5bf85ca3a6522bbf780 diff --git a/samples/sample-hcd/.gitignore b/samples/sample-hcd/.gitignore new file mode 100644 index 00000000..1abd3da5 --- /dev/null +++ b/samples/sample-hcd/.gitignore @@ -0,0 +1,38 @@ +astra-sdk-java.wiki/ +.env +.astrarc +sec +application.properties + + + +# eclipse conf file +.settings +.classpath +.project +.cache + +# idea conf files +.idea +*.ipr +*.iws +*.iml + +# building +target +build +tmp +dist + +# misc +.DS_Store + +.factorypath +.sts4-cache +*.log +# hold token values +test-config-astra.properties +test-config-embedding-providers.properties + +release.properties +pom.xml.releaseBackup diff --git a/samples/sample-hcd/INSTALL-HCD-LOCAL.MD b/samples/sample-hcd/INSTALL-HCD-LOCAL.MD new file mode 100644 index 00000000..b32373fd --- /dev/null +++ b/samples/sample-hcd/INSTALL-HCD-LOCAL.MD @@ -0,0 +1,159 @@ +# Setup to Pull HCD from ECR + +This guide provides step-by-step instructions for setting up AWS CLI and Podman/Docker to pull HCD (Hyper-Converged Database) images from the DataStax ECR (Elastic Container Registry) repository. + +## Table of Contents + +1. [Local Laptop Setup](#local-laptop-setup) +2. [GitHub Actions Setup (CI/CD)](#github-actions-setup-cicd) +3. [Re-authentication](#re-authentication) +4. [Troubleshooting](#troubleshooting) + +--- + +## Local Laptop Setup + +### Prerequisites + +- AWS CLI installed +- Podman (or Docker) installed +- Active Okta account with appropriate permissions + +### Step 1: Install Required Tools + +```bash +# Install AWS CLI (if not already installed) +# Visit: https://aws.amazon.com/cli/ + +# Install Podman (if not already installed) +# Visit: https://podman.io/getting-started/installation +``` + +### Step 2: Configure AWS CLI with SSO + +Ensure you are logged into Okta, then configure AWS CLI: + +```bash +aws configure sso +``` + +Provide the following information when prompted: + +| Prompt | Value | +|--------|------------------------------------------| +| **Session name** | Choose any name (e.g., `alala`) | +| **Start URL** | `https://.awsapps.com/start/#` | +| **SSO Region** | `us-west-2` | +| **SSO registration scopes** | Leave as default `[sso:account:access]` | + +### Step 3: Authorize via Browser + +1. A browser window will open automatically +2. Complete the Okta authentication +3. Click **"Allow access to your data"** when prompted +4. You'll see a green confirmation message +5. Close the browser tab and return to the terminal + +### Step 4: Select AWS Account + +When prompted, select the **`engops-shared`** account. + +**Expected output:** + +``` +There are 3 AWS accounts available to you. +Using the account ID +The only role available to you is: DATAAPI-ENG-DEV_ENG-SHAR +Using the role name "DATAAPI-ENG-DEV_ENG-SHAR" +Default client Region [us-west-2]: +CLI default output format (json if not specified) [None]: +Profile name [DATAAPI-ENG-DEV_ENG-SHAR-]: + +To use this profile, specify the profile name using --profile, as shown: + +aws sts get-caller-identity --profile DATAAPI-ENG-DEV_ENG-SHAR- +``` + +### Step 5: Verify Configuration + +Test your configuration: + +```bash +aws sts get-caller-identity --profile DATAAPI-ENG-DEV_ENG-SHAR- +``` + +Check your AWS configuration file (`~/.aws/config`): + +```ini +[profile DATAAPI-ENG-DEV_ENG-SHAR-] +sso_session = Engops +sso_account_id = +sso_role_name = DATAAPI-ENG-DEV_ENG-SHAR +region = us-west-2 + +[sso-session Engops] +sso_start_url = https://awsapps.com/start/# +sso_region = us-west-2 +sso_registration_scopes = sso:account:access +``` + +> **Note:** No credentials file is created. Authentication is cached in `~/.aws/sso/` directory. + +### Step 6: Login to ECR with Podman + +Authenticate Podman with ECR: + +```bash +aws ecr get-login-password \ + --profile DATAAPI-ENG-DEV_ENG-SHAR- \ + --region us-west-2 \ + | podman login --username AWS --password-stdin \ + .dkr.ecr.us-west-2.amazonaws.com +``` + +### Step 7: Pull HCD Image + +Pull the HCD image from ECR: + +```bash +podman pull /engops-shared/hcd/prod/hcd:1.2.1-early-preview +``` + +**Success!** You can now use the HCD image locally. + +--- + +## Re-authentication + +AWS SSO tokens expire periodically. If you encounter an authentication error: + +``` +Error when retrieving token from sso: Token has expired and refresh failed +``` + +Re-authenticate with: + +```bash +aws sso login --profile DATAAPI-ENG-DEV_ENG-SHAR- +``` + +This will open a browser tab for Okta authentication. Once completed, you can retry your previous command. + +--- + +## Start HCD image + + + + + +## Additional Resources + +- [AWS CLI SSO Configuration](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html) +- [Amazon ECR User Guide](https://docs.aws.amazon.com/ecr/) +- [Podman Documentation](https://docs.podman.io/) +- [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) + +--- + +**Last Updated:** September 2025 \ No newline at end of file diff --git a/samples/sample-hcd/INSTALL-HCD-MC.MD b/samples/sample-hcd/INSTALL-HCD-MC.MD new file mode 100644 index 00000000..810137fe --- /dev/null +++ b/samples/sample-hcd/INSTALL-HCD-MC.MD @@ -0,0 +1,333 @@ +# Data API with HCD in Mission Control + +This guide walks you through setting up and accessing the Data API with Hyper-Converged Database (HCD) in Mission Control using Google Kubernetes Engine (GKE). + +## Prerequisites + +- Access to the `k8ssandra` GCP project (contact Alexander D. for permissions) +- IBM/Datastax email address for authentication +- Basic familiarity with Kubernetes and command-line tools + +## Table of Contents + +1. [gcloud CLI Configuration](#1-gcloud-cli-configuration) +2. [Kubernetes Client Setup](#2-kubernetes-client-setup) +3. [List Organizations](#3-list-organizations) +4. [List Clusters](#4-list-clusters) +5. [Create a New Cluster](#5-create-a-new-cluster) +6. [Setup Data API](#6-setup-data-api) + +--- + +## 1. gcloud CLI Configuration + +> **Reference:** [Google Cloud SDK Installation Guide](https://docs.cloud.google.com/sdk/docs/install-sdk) + +### Install gcloud CLI + +Download and extract the Google Cloud SDK (ARM version for macOS): + +```bash +# Download the SDK +curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-darwin-arm.tar.gz + +# Extract the archive +tar -xf google-cloud-cli-darwin-arm.tar.gz + +# Run the installation script +./google-cloud-sdk/install.sh +``` + +### Initialize and Authenticate + +```bash +# Initialize gcloud CLI +gcloud init + +# List authenticated accounts +gcloud auth list + +# Authenticate with your IBM/Datastax email +gcloud auth login + +# List available projects +gcloud projects list + +# Set k8ssandra as the default project +gcloud config set project k8ssandra +``` + +### Install GKE Components + +```bash +# Install the GKE authentication plugin +gcloud components install gke-gcloud-auth-plugin + +# List available clusters +gcloud container clusters list +``` + +**Expected output:** + +``` +NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS +rad-ingress-test us-central1-a 1.33.5-gke.2326000 35.193.248.128 e2-standard-4 1.33.5-gke.2326000 3 RUNNING +ui-playground-new us-central1-a 1.33.5-gke.2228001 34.44.121.239 e2-standard-4 1.33.5-gke.2228001 11 RUNNING +ui-playground-new-dp us-central1-a 1.33.5-gke.2326000 34.136.191.13 e2-standard-4 1.33.5-gke.2326000 1 RUNNING +temporal-oss us-central1-c 1.33.5-gke.2326000 34.46.16.158 n2-standard-8 1.33.5-gke.2326000 5 RUNNING +temporal-oss-2 us-central1-c 1.33.5-gke.2326000 34.67.152.118 e2-standard-4 1.33.5-gke.2326000 3 RUNNING +temporal-oss-jeff us-central1-c 1.34.3-gke.1245000 34.45.75.50 e2-standard-4 1.34.3-gke.1245000 1 RUNNING +``` + +--- + +## 2. Kubernetes Client Setup + +### Connect to the Cluster + +```bash +# Retrieve credentials for ui-playground-new cluster +gcloud container clusters get-credentials ui-playground-new \ + --region=us-central1-a \ + --project=k8ssandra +``` + +**Expected output:** + +``` +Fetching cluster endpoint and auth data. +kubeconfig entry generated for ui-playground-new. +``` + +### Verify Connection + +```bash +# Get cluster information +kubectl cluster-info +``` + +**Expected output:** + +``` +Kubernetes control plane is running at https://**.**.**.** +GLBCDefaultBackend is running at https://**.**.**.**/api/v1/namespaces/kube-system/services/default-http-backend:http/proxy +KubeDNS is running at https://**.**.**.**/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy +Metrics-server is running at https://**.**.**.**/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy +``` + +--- + +## 3. List Organizations + +### Access Mission Control UI + +1. Navigate to: https://mc-ui.k8ssandra.io/ +2. Authenticate using your W3 login credentials + +![Sign In](z_img/01-sign-in.png) + +3. Locate the **Data API Integration** organization + +![Create Cluster](z_img/02-create-cluster.png) + +### List Organizations via kubectl + +Each organization in Mission Control corresponds to a Kubernetes namespace: + +```bash +kubectl get ns | grep data-api +``` + +**Expected output:** + +``` +data-api-integration-tests-y42ou8ut Active 83d +``` + +--- + +## 4. List Clusters + +### View Clusters in UI + +Navigate to your organization to see available clusters: + +![List Clusters](z_img/03-list-clusters.png) + +### List Clusters via kubectl + +```bash +kubectl get svc -n data-api-integration-tests-y42ou8ut +``` + +Each cluster has multiple associated services in its namespace. + +--- + +## 5. Create a New Cluster + +### Step 1: Initialize Cluster Creation + +1. In the Mission Control UI, click **Create Cluster** +2. Enter a cluster name (e.g., `demo`) +3. Select **HCD** as the cluster type + +![Create Cluster Step 1](z_img/04-create-cluster-01.png) + +### Step 2: Configure Data Center + +1. Select **Single DC** deployment +2. Provide a data center name (default: `dc-1`) +3. Provide a rack name (default: `rack-name`) + +![Create Cluster Step 2](z_img/05-create-cluster-02.png) + +### Step 3: Configure Storage and Credentials + +1. Select storage class: `standard` (recommended for testing) +2. Configure superuser credentials: +- **Username:** Leave empty to use `-superuser` +- **Password:** Leave empty to auto-generate + +![Create Cluster Step 3](z_img/06-create-cluster-03.png) + +### Step 4: Wait for Initialization + +The cluster will take several minutes to initialize: + +![Create Cluster Step 4](z_img/07-create-cluster-04.png) + +### Verify Cluster Services + +```bash +kubectl get svc -n data-api-integration-tests-y42ou8ut +``` + +### Retrieve Cluster Credentials + +```bash +# Get username +kubectl get secret demo-superuser \ + -n data-api-integration-tests-y42ou8ut \ + -o jsonpath="{.data.username}" | base64 -d + +# Get password +kubectl get secret demo-superuser \ + -n data-api-integration-tests-y42ou8ut \ + -o jsonpath="{.data.password}" | base64 -d +``` + +--- + +## 6. Setup Data API + +### Step 1: Create a Gateway + +1. Navigate to the **Connect** tab +2. Select **Data API** +3. Click **Create Gateway** + +![Create Gateway Step 1](z_img/08-create-gateway-01.png) + +### Step 2: Configure Gateway Port + +Choose a port number above 30000: + +![Create Gateway Step 2](z_img/09-create-gateway-02.png) + +**Note:** The gateway will initially show an ERROR state: + +![Create Gateway Step 3](z_img/10-create-gateway-03.png) + +After a few seconds, it should transition to ACTIVE: + +![Create Gateway Step 4](z_img/11-create-gateway-04.png) + +> ⚠️ If the gateway remains in ERROR state, the port may already be in use. Try a different port number. + +### Step 3: Get External IP Address + +As of February 2026, ingress is not yet configured. You'll need to manually retrieve the external IP: + +```bash +# List gateway services +kubectl get svc -n data-api-integration-tests-y42ou8ut | grep data-api +``` + +**Expected output:** + +``` +demo-dc-1-data-api-np NodePort 12.123.123.12 8181:30444/TCP 11m +``` + +```bash +# Get node external IPs +kubectl get nodes -o wide +``` + +**Expected output:** + +``` +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE +gke-ui-playground-new-default-pool-b6a52a69-0m7y Ready 6d13h v1.33.5-gke.2228001 10.128.0.98 123.123.123.123 Container-Optimized OS from Google +gke-ui-playground-new-default-pool-b6a52a69-22ma Ready 6d13h v1.33.5-gke.2228001 10.128.0.14 124.124.124.124 Container-Optimized OS from Google +``` + +### Step 4: Test Data API Connection + +```bash +# Set environment variables +export HCD_URL="" +export HCD_GATEWAY_PORT="" +export HCD_USERNAME="demo-superuser" +export HCD_PASSWORD="" +export HCD_USERNAME_B64=$(echo -n "$HCD_USERNAME" | base64) +export HCD_PASSWORD_B64=$(echo -n "$HCD_PASSWORD" | base64) + +# Test connection by listing keyspaces +curl -X POST "http://${HCD_URL}:${HCD_GATEWAY_PORT}/v1" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -H "Token: Cassandra:${HCD_USERNAME_B64}:${HCD_PASSWORD_B64}" \ + -d '{"findKeyspaces":{}}' +``` + +**Expected output:** + +```json +{ + "status": { + "keyspaces": [ + "system_auth", + "system_schema", + "system_distributed", + "system", + "reaper_db", + "system_traces", + "system_views", + "system_virtual_schema" + ] + } +} +``` + +> ⚠️ If the command hangs, you may need to request a firewall port opening. + +--- + +## Troubleshooting + +- **Gateway stuck in ERROR state:** The selected port may be in use. Choose a different port above 30000. +- **Connection timeout:** Verify firewall rules allow traffic on the gateway port. +- **Authentication failed:** Double-check credentials retrieved from Kubernetes secrets. +- **Cluster not appearing:** Ensure you have proper permissions in the `k8ssandra` GCP project. + +## Additional Resources + +- [Google Cloud SDK Documentation](https://cloud.google.com/sdk/docs) +- [Kubernetes Documentation](https://kubernetes.io/docs/home/) +- [Mission Control Documentation](https://mc-ui.k8ssandra.io/) + +--- + +**Last Updated:** February 2026 \ No newline at end of file diff --git a/samples/sample-hcd/README.MD b/samples/sample-hcd/README.MD new file mode 100644 index 00000000..eee82302 --- /dev/null +++ b/samples/sample-hcd/README.MD @@ -0,0 +1,9 @@ +# Working with Data API and HCD + +## Table of Content + +- [Run HCD in Mission Control](INSTALL-HCD-MC.MD) +- [Run HCD on your machine](INSTALL-HCD-LOCAL.MD) +- Implementation + +## Implementations \ No newline at end of file diff --git a/samples/sample-hcd/pom.xml b/samples/sample-hcd/pom.xml new file mode 100644 index 00000000..facaf484 --- /dev/null +++ b/samples/sample-hcd/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + com.datastax.astra + astra-db-java-samples-parent + 2.2.1-SNAPSHOT + + + sample-hcd + + sample hcd + + + 21 + 21 + + + + + com.datastax.astra + astra-db-java + + + + com.datastax.astra + langchain4j-astradb + + + + dev.langchain4j + langchain4j-open-ai-official + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + \ No newline at end of file diff --git a/samples/sample-hcd/src/main/java/com/ibm/mc/demo/MissionControlLangchain4j.java b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/MissionControlLangchain4j.java new file mode 100644 index 00000000..3337e68b --- /dev/null +++ b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/MissionControlLangchain4j.java @@ -0,0 +1,170 @@ +package com.ibm.mc.demo; + +import com.datastax.astra.client.DataAPIClient; +import com.datastax.astra.client.DataAPIDestination; +import com.datastax.astra.client.admin.DataAPIDatabaseAdmin; +import com.datastax.astra.client.collections.Collection; +import com.datastax.astra.client.collections.definition.CollectionDefinition; +import com.datastax.astra.client.core.auth.UsernamePasswordTokenProvider; +import com.datastax.astra.client.core.options.DataAPIClientOptions; +import com.datastax.astra.client.databases.Database; +import com.datastax.astra.client.databases.DatabaseOptions; +import com.datastax.astra.client.databases.definition.keyspaces.KeyspaceOptions; +import com.ibm.mc.demo.dto.Lyric; +import com.ibm.mc.demo.dto.Song; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.openaiofficial.OpenAiOfficialEmbeddingModel; +import dev.langchain4j.model.output.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.stream.IntStream; + +import static com.datastax.astra.client.core.vector.SimilarityMetric.COSINE; +import static com.ibm.mc.demo.SuperDuperSongs.HOTEL_CALIFORNIA; +import static com.ibm.mc.demo.SuperDuperSongs.ROMEO_AND_JULIET; +import static com.ibm.mc.demo.SuperDuperSongs.TWINKLE_TWINKLE; +import static com.openai.models.embeddings.EmbeddingModel.TEXT_EMBEDDING_3_SMALL; + +/** + * We want to leverage the AstraDB langchain4j store + */ +public class MissionControlLangchain4j { + + private static final Logger log = LoggerFactory.getLogger(MissionControlLangchain4j.class); + + public static void main(String[] args) { + // 1) Connect to HCD and ensure keyspace exists + Database hcd = connectHcdAndCreateKeyspace(); + + // 2) Create the embedding model (OpenAI official via LangChain4j) + EmbeddingModel embeddingModel = initOpenAIEmbeddingModel(); + + // 3) Create (or get) the 'lyrics' collection with a 1536-dim vector (cosine) + Collection collectionLyrics = hcd.createCollection( + "lyrics", + new CollectionDefinition().vector(1536, COSINE), + Lyric.class + ); + + collectionLyrics.deleteAll(); + + // 4) Embed one of the sample songs and insert all lyric lines at once + insertSong(collectionLyrics, ROMEO_AND_JULIET, embeddingModel); + insertSong(collectionLyrics, TWINKLE_TWINKLE, embeddingModel); + insertSong(collectionLyrics, HOTEL_CALIFORNIA, embeddingModel); + } + + static Properties loadProperties() { + Properties props = new Properties(); + try (InputStream input = MissionControlLangchain4j.class.getClassLoader() + .getResourceAsStream("application.properties")) { + if (input == null) { + throw new IllegalStateException("Unable to find application.properties"); + } + props.load(input); + } catch (IOException ex) { + throw new RuntimeException("Failed to load application.properties", ex); + } + return props; + } + + static Database connectHcdAndCreateKeyspace() { + // Load settings from application.properties + Properties props = loadProperties(); + String cassandraUserName = props.getProperty("cassandra.username"); + String cassandraPassword = props.getProperty("cassandra.password"); + String keyspaceName = props.getProperty("cassandra.keyspace"); + String dataApiUrl = props.getProperty("cassandra.data-api.url"); + + // Build token: Cassandra:base64(username):base64(password) + String token = new UsernamePasswordTokenProvider(cassandraUserName, cassandraPassword) + .getTokenAsString(); + + // Initialize client for HCD destination with options + DataAPIClientOptions clientOptions = new DataAPIClientOptions() + .destination(DataAPIDestination.HCD) + .logRequests(); // trace Data API request and responses + + DataAPIClient client = new DataAPIClient(token, clientOptions); + log.info("Contacting HCD on '{}'", dataApiUrl); + + // Create keyspace via DataAPIDatabaseAdmin (idempotent) + ((DataAPIDatabaseAdmin) client + .getDatabase(dataApiUrl) + .getDatabaseAdmin()) + .createKeyspace(keyspaceName, KeyspaceOptions.simpleStrategy(1)); + log.info("Keyspace '{}' created (if not exists)", keyspaceName); + + // Connect to database with keyspace + DatabaseOptions dbOptions = new DatabaseOptions(token, clientOptions).keyspace(keyspaceName); + Database db = client.getDatabase(dataApiUrl, dbOptions); + log.info("Connected to HCD"); + return db; + } + + static EmbeddingModel initOpenAIEmbeddingModel() { + String apiKey = System.getenv("OPENAI_API_KEY"); + if (apiKey == null || apiKey.isBlank()) { + throw new IllegalStateException("OPENAI_API_KEY environment variable is not set"); + } + return OpenAiOfficialEmbeddingModel.builder() + .apiKey(apiKey) + .modelName(TEXT_EMBEDDING_3_SMALL) + .build(); + } + + static void insertSong(Collection collectionLyrics, Song song, EmbeddingModel embeddedModel) { + Song embedded = embedSong(song, embeddedModel); + log.info("Song '{}' lyrics have been embedded", song.title()); + + insertEmbeddedSong(collectionLyrics, embedded); + log.info("Inserted lyrics for '{}' by {}", embedded.title(), embedded.band()); + } + + /** + * Populate the vectors part of the Song using batch embedding. + * Assumes lyrics are non-null and non-empty. + */ + static Song embedSong(Song song, EmbeddingModel embeddingModel) { + // Build TextSegments preserving order + List segments = Arrays.stream(song.lyrics()) + .map(TextSegment::from) + .toList(); + + // Batch embed + Response> response = embeddingModel.embedAll(segments); + List embeddings = response.content(); + + // Convert to float[][] via stream + float[][] vectors = embeddings.stream() + .map(Embedding::vector) + .toArray(float[][]::new); + + // Return new song with vectors populated + return new Song(song.band(), song.title(), song.lyrics(), vectors); + } + + /** + * Insert all lyrics for a song in a single call. + */ + static void insertEmbeddedSong(Collection collectionLyrics, Song s) { + + List lyrics = IntStream.range(0, s.lyrics().length) + .mapToObj(i -> new Lyric(s.band(), s.title(), s.lyrics()[i], s.vectors()[i])) + .toList(); + + insertLyrics(collectionLyrics, lyrics); + } + + static void insertLyrics(Collection collectionLyrics, List lyrics) { + collectionLyrics.insertMany(lyrics); + } +} diff --git a/samples/sample-hcd/src/main/java/com/ibm/mc/demo/MissionControlManualEmbedding.java b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/MissionControlManualEmbedding.java new file mode 100644 index 00000000..383586d7 --- /dev/null +++ b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/MissionControlManualEmbedding.java @@ -0,0 +1,168 @@ +package com.ibm.mc.demo; + +import com.datastax.astra.client.DataAPIClient; +import com.datastax.astra.client.DataAPIDestination; +import com.datastax.astra.client.admin.DataAPIDatabaseAdmin; +import com.datastax.astra.client.collections.Collection; +import com.datastax.astra.client.collections.definition.CollectionDefinition; +import com.datastax.astra.client.core.auth.UsernamePasswordTokenProvider; +import com.datastax.astra.client.core.options.DataAPIClientOptions; +import com.datastax.astra.client.databases.Database; +import com.datastax.astra.client.databases.DatabaseOptions; +import com.datastax.astra.client.databases.definition.keyspaces.KeyspaceOptions; +import com.ibm.mc.demo.dto.Lyric; +import com.ibm.mc.demo.dto.Song; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.openaiofficial.OpenAiOfficialEmbeddingModel; +import dev.langchain4j.model.output.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.stream.IntStream; + +import static com.datastax.astra.client.core.vector.SimilarityMetric.COSINE; +import static com.ibm.mc.demo.SuperDuperSongs.HOTEL_CALIFORNIA; +import static com.ibm.mc.demo.SuperDuperSongs.ROMEO_AND_JULIET; +import static com.ibm.mc.demo.SuperDuperSongs.TWINKLE_TWINKLE; +import static com.openai.models.embeddings.EmbeddingModel.TEXT_EMBEDDING_3_SMALL; + +public class MissionControlManualEmbedding { + + private static final Logger log = LoggerFactory.getLogger(MissionControlManualEmbedding.class); + + + public static void main(String[] args) { + // 1) Connect to HCD and ensure keyspace exists + Database hcd = connectHcdAndCreateKeyspace(); + + // 2) Create the embedding model (OpenAI official via LangChain4j) + EmbeddingModel embeddingModel = initOpenAIEmbeddingModel(); + + // 3) Create (or get) the 'lyrics' collection with a 1536-dim vector (cosine) + Collection collectionLyrics = hcd.createCollection( + "lyrics", + new CollectionDefinition().vector(1536, COSINE), + Lyric.class + ); + + collectionLyrics.deleteAll(); + + // 4) Embed one of the sample songs and insert all lyric lines at once + insertSong(collectionLyrics, ROMEO_AND_JULIET, embeddingModel); + insertSong(collectionLyrics, TWINKLE_TWINKLE, embeddingModel); + insertSong(collectionLyrics, HOTEL_CALIFORNIA, embeddingModel); + } + + static Properties loadProperties() { + Properties props = new Properties(); + try (InputStream input = MissionControlManualEmbedding.class.getClassLoader() + .getResourceAsStream("application.properties")) { + if (input == null) { + throw new IllegalStateException("Unable to find application.properties"); + } + props.load(input); + } catch (IOException ex) { + throw new RuntimeException("Failed to load application.properties", ex); + } + return props; + } + + static Database connectHcdAndCreateKeyspace() { + // Load settings from application.properties + Properties props = loadProperties(); + String cassandraUserName = props.getProperty("cassandra.username"); + String cassandraPassword = props.getProperty("cassandra.password"); + String keyspaceName = props.getProperty("cassandra.keyspace"); + String dataApiUrl = props.getProperty("cassandra.data-api.url"); + + // Build token: Cassandra:base64(username):base64(password) + String token = new UsernamePasswordTokenProvider(cassandraUserName, cassandraPassword) + .getTokenAsString(); + + // Initialize client for HCD destination with options + DataAPIClientOptions clientOptions = new DataAPIClientOptions() + .destination(DataAPIDestination.HCD) + .logRequests(); // trace Data API request and responses + + DataAPIClient client = new DataAPIClient(token, clientOptions); + log.info("Contacting HCD on '{}'", dataApiUrl); + + // Create keyspace via DataAPIDatabaseAdmin (idempotent) + ((DataAPIDatabaseAdmin) client + .getDatabase(dataApiUrl) + .getDatabaseAdmin()) + .createKeyspace(keyspaceName, KeyspaceOptions.simpleStrategy(1)); + log.info("Keyspace '{}' created (if not exists)", keyspaceName); + + // Connect to database with keyspace + DatabaseOptions dbOptions = new DatabaseOptions(token, clientOptions).keyspace(keyspaceName); + Database db = client.getDatabase(dataApiUrl, dbOptions); + log.info("Connected to HCD"); + return db; + } + + static EmbeddingModel initOpenAIEmbeddingModel() { + String apiKey = System.getenv("OPENAI_API_KEY"); + if (apiKey == null || apiKey.isBlank()) { + throw new IllegalStateException("OPENAI_API_KEY environment variable is not set"); + } + return OpenAiOfficialEmbeddingModel.builder() + .apiKey(apiKey) + .modelName(TEXT_EMBEDDING_3_SMALL) + .build(); + } + + static void insertSong(Collection collectionLyrics, Song song, EmbeddingModel embeddedModel) { + Song embedded = embedSong(song, embeddedModel); + log.info("Song '{}' lyrics have been embedded", song.title()); + + insertEmbeddedSong(collectionLyrics, embedded); + log.info("Inserted lyrics for '{}' by {}", embedded.title(), embedded.band()); + } + + /** + * Populate the vectors part of the Song using batch embedding. + * Assumes lyrics are non-null and non-empty. + */ + static Song embedSong(Song song, EmbeddingModel embeddingModel) { + // Build TextSegments preserving order + List segments = Arrays.stream(song.lyrics()) + .map(TextSegment::from) + .toList(); + + // Batch embed + Response> response = embeddingModel.embedAll(segments); + List embeddings = response.content(); + + // Convert to float[][] via stream + float[][] vectors = embeddings.stream() + .map(Embedding::vector) + .toArray(float[][]::new); + + // Return new song with vectors populated + return new Song(song.band(), song.title(), song.lyrics(), vectors); + } + + /** + * Insert all lyrics for a song in a single call. + */ + static void insertEmbeddedSong(Collection collectionLyrics, Song s) { + + List lyrics = IntStream.range(0, s.lyrics().length) + .mapToObj(i -> new Lyric(s.band(), s.title(), s.lyrics()[i], s.vectors()[i])) + .toList(); + + insertLyrics(collectionLyrics, lyrics); + } + + static void insertLyrics(Collection collectionLyrics, List lyrics) { + collectionLyrics.insertMany(lyrics); + } +} diff --git a/samples/sample-hcd/src/main/java/com/ibm/mc/demo/SuperDuperSongs.java b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/SuperDuperSongs.java new file mode 100644 index 00000000..c9196c84 --- /dev/null +++ b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/SuperDuperSongs.java @@ -0,0 +1,82 @@ +package com.ibm.mc.demo; + +import com.ibm.mc.demo.dto.Song; + +public class SuperDuperSongs { + + // --- Sample data --- + public static final Song ROMEO_AND_JULIET = new Song( + "Dire Straits", + "Romeo And Juliet", + new String[] { + "A lovestruck Romeo sings the streets a serenade", + "Says something like, You and me babe, how about it?", + "Juliet says,Hey, it's Romeo, you nearly gimme a heart attack", + "He's underneath the window", + "She's singing, Hey la, my boyfriend's back", + "You shouldn't come around here singing up at people like that", + "Anyway, what you gonna do about it?" + }, + null + ); + + public static final Song TWINKLE_TWINKLE = new Song( + "Traditional", + "Twinkle Twinkle Little Star", + new String[] { + "Twinkle, twinkle, little star", + "How I wonder what you are", + "Up above the world so high", + "Like a diamond in the sky", + "Twinkle, twinkle, little star", + "How I wonder what you are" + }, + null + ); + + public static final Song HAPPY_BIRTHDAY = new Song( + "Traditional", + "Happy Birthday", + new String[] { + "Happy birthday to you", + "Happy birthday to you", + "Happy birthday dear friend", + "Happy birthday to you" + }, + null + ); + + + public static final Song HOTEL_CALIFORNIA = new Song( + "Eagles", + "Hotel California", + new String[] { + "On a dark desert highway, cool wind in my hair", + "Warm smell of colitas, rising up through the air", + "Up ahead in the distance, I saw a shimmering light", + "My head grew heavy and my sight grew dim", + "I had to stop for the night", + "There she stood in the doorway", + "I heard the mission bell", + "And I was thinking to myself", + "This could be Heaven or this could be Hell", + "Then she lit up a candle and she showed me the way", + "There were voices down the corridor", + "I thought I heard them say" + }, + null + ); + + public static final Song SONNET_18 = new Song( + "William Shakespeare", + "Sonnet 18", + new String[] { + "Shall I compare thee to a summer's day?", + "Thou art more lovely and more temperate.", + "Rough winds do shake the darling buds of May,", + "And summer's lease hath all too short a date." + }, + null + ); + +} diff --git a/samples/sample-hcd/src/main/java/com/ibm/mc/demo/dto/Lyric.java b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/dto/Lyric.java new file mode 100644 index 00000000..511da4c3 --- /dev/null +++ b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/dto/Lyric.java @@ -0,0 +1,6 @@ +package com.ibm.mc.demo.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record Lyric(String band, String title, String lyric, @JsonProperty("$vector") float[] lyricVector) { +} diff --git a/samples/sample-hcd/src/main/java/com/ibm/mc/demo/dto/Song.java b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/dto/Song.java new file mode 100644 index 00000000..bd047b70 --- /dev/null +++ b/samples/sample-hcd/src/main/java/com/ibm/mc/demo/dto/Song.java @@ -0,0 +1,5 @@ +package com.ibm.mc.demo.dto; + +// --- Records --- +public record Song(String band, String title, String[] lyrics, float[][] vectors) { +} diff --git a/samples/sample-hcd/src/main/resources/application.sample b/samples/sample-hcd/src/main/resources/application.sample new file mode 100644 index 00000000..9146ba23 --- /dev/null +++ b/samples/sample-hcd/src/main/resources/application.sample @@ -0,0 +1,7 @@ + +# Cassandra / HCD Connection Settings +cassandra.username= +cassandra.password= +cassandra.keyspace= +cassandra.data-api.url= + diff --git a/samples/sample-hcd/src/main/resources/logback.xml b/samples/sample-hcd/src/main/resources/logback.xml new file mode 100644 index 00000000..39438b82 --- /dev/null +++ b/samples/sample-hcd/src/main/resources/logback.xml @@ -0,0 +1,38 @@ + + + + + %d{HH:mm:ss.SSS} %magenta(%-5level) %cyan(%-30logger{30}) : %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/sample-hcd/z_img/01-sign-in.png b/samples/sample-hcd/z_img/01-sign-in.png new file mode 100644 index 0000000000000000000000000000000000000000..791f076bbda4cb6d0cb4952c18bcedc0bfdf6453 GIT binary patch literal 27210 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 Lwn~ZegU9~^X^9j| literal 0 HcmV?d00001 diff --git a/samples/sample-hcd/z_img/03-list-clusters.png b/samples/sample-hcd/z_img/03-list-clusters.png new file mode 100644 index 0000000000000000000000000000000000000000..4d0e2ac8e2977be5c61b1510e5e1b75e4405a90e GIT binary patch literal 92406 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 XA)b#;#!pwTfj?<+d9hp(J+J=*S~BvV literal 0 HcmV?d00001 diff --git a/samples/sample-hcd/z_img/05-create-cluster-02.png b/samples/sample-hcd/z_img/05-create-cluster-02.png new file mode 100644 index 0000000000000000000000000000000000000000..1076bb8f53578a61860c46509752157eff304df8 GIT binary patch literal 162013 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

Q$~g+z5{& z&Z7YS>WLCF&;Ndlr>xK|M25{y^7IUqGAkl^!#O2@UQFxICb?P;->HJ3u|vdmT3lqW ziTvyP*8(NQ#a$im&riq+8b${-m$xPh~Q+@*sT1rrkU}deJ03BuHFZ02sp(N$e}r;w`2Wf64cPyJFV(JLG#t zsnwW_V4eXF^4FE#G7p4p0hz!;A9?kRWWzHg$rnJB<*3Pa8bFY_di7A}|52KxE$*Qa zdcGbP^-RSKm=&Wdm3Q}Uok_v2EiE=dx+Y1#zqJ}+1Ym0TOVv^!XQCXtqEV0;0Bty^HBEa0~$5Tf2(US^S=z z5(D6ID5*F6B@-{QP&4Qe1<1W`la4!>XPg5|qs*X>3z78PbfQkZLz1~lOC51}759{{ zqRKXgI{-iq$Mw}n^YJ9BkCb)_^E0ZHAg%KyON8R$l8*dn{3}>#uir(qGc^ z*lNsm#`~*2!b2VD;|*Z6OMsA=lat1bY{c0gEciG`l=PorKRtla7n_P;PzK_ z8pvljIiLRV%JL=i%dnbq&rES2K=wK;Cz!V{whs&*9Fuy(ex{$9hjQwjWry88_|EzX z2nK34pFNJYlzQ)@6;^xf)TdA|vr`>yn|SX27JD%~4ve~-qF50lH$Gym>ULW)xjGFd zUW4;^_#8BGSSsvzA}lOu-Xv2`yc^81L1Mcp_^BD$v%fumnuR^ZcmYJZ?spedE>n}Y z70w!$)hW<>zts0RHLct%?sRI({iy4;*W6!Zq5uHqE%R~bK*URoJoyrJ^WcqiMq(_F z0|`_t_Q49^R(cE@ZS?lqts}GB27y;biJc_6Jlu9nOLCnSaTrc8hiK1L6~>$oOT9jPs3Z02$IECS zaJJzqQ0A0>X65d(XxX2qOFe5zix4cRJZD?7tfAz2gQz`t^v5Np_<^pP{=CSSfq2ekf(W zS)qT!GdI;U8C$}GuJ2Tpd%o0P*ZPA>>&6Mz(^6@La-;T^8uRkECRZX2_de+w`x;)K z_V{j|L2<#P71h;g2lRAU_w$gMPr_q>c=EpjO)8G9@I0=N@_oqE~+JUI9#kB|ma8vFBP?HIIh8hYL z(KQy4i`_;p2|3}W{p?hqAM?lJbm2dYGVVGXc&vSGIRH>kz0HFj3B0Y|hvXOcU!-sX zXsbMFhJ}R3hbaISroY^F#=3@-p-HV9ftt3&WJJyQA|rf$eq>E?lbQB8xAvrL+vrm5 zIrhIcf3*J7RbUP0uE6T1?mX?@N#5(Sr_;XIm}45a#vZ8cdk;K7UA^CyedF9D>zU4o#+gZ2FA4W~}97_0n$kQ$v z#}eH>uV$sZs}E#3Y@uNh7ngIhP1d+OdLrdTeWhAGB@SDbe`%Y&*BXfQdU!M6j8F1lsP@d18boE6-@ejmO+Dd}V?5xO1zGyS zNVd4>mw$Oy-BXT-xbGfsjg@2{f$Z_zR;DWKGgTHYTEdckL!vF{dqRcXSYM<{Hip@G z?jXu*X@nx(k~~B?!VFGX(`E^14;84?$te5jH57fr(t)RqJ3|TC7m3&-@|tFR&WVxi z=ej^lhw+kc!n>4){zW!-d-?|o_Q1ndbBiNfY9$!C-YQp?=CPu-zS-^xfL(qK_WG$^ za^I=tt~uoFNjSB3CHqU}Q{>q?hjO*{7^Bza(MWWKVS-P708Y#3tqrR22*0bvA@0jY z(61zsf&O5}(eK{h3%g6YZCfHES&q+I!fB}54<;RZg7Sf|Xj@r1g~HKOUigVM!>YKi z@<_3%vPn87IuLh7Ue|u@LnjscQAh&B<{m|#7*+~PZIuqloTY|+{hbAXngMpF{Z(H) zMmpN|INy`qx%lbSa=pd9rKRpvDJdsIlzzJGIhgD+G9C>^y#6uV)7yX5uk3H_qRQepv|#-F{P6GN3)g2G1-(YFqY{*|?@Fs@e(Qo> zsR5Fi(`R$kUNZv8F^?$oy(V`+B+e%v1EI2svy_wrn}Xm})q!unxVC_$m2L`7;g~t! z@qH`0kmQK7R28}J#J8Y$9r0L8FT0~!MSHt8>$@seyg`{F^jyGq1avt`a=LAEkc?VW|SKf9-k+j0P zlVQV?n#p@OC?GoQsY424w!vGSXnIeMeJXMIGWVEXv?JJ0lq`v=+bB7>9G$WrGgX#E z1gjiKc{%2{o0UG`=S6uTaD^D$?)edRG-SA6qkOMf3u`?mrjF<|4`8JRh08nTGd?YZ z-hfCw_~`}d=(EKVmj!JPle~aU*i`?r$)tO00D!$WML$m;EID7ODclpXnar}b;(G9X z&Y7Zy$Q;^N4`@NWuB?%=3Bs)!|F(TLhTy2_xS@+X0JpCYSztZ|2k`o$rM#c zkmU1ILo=7Nm-w9`GFzf$TOqE^6damuzE_R`9y+zTJ1Z+2v`6^+Q?BGoUo7*7To;4} z?_J3$FD%WD0`ZY|5_K4RT*u8e0~M`Q)|>6=793kC3vk=v|3h{+1MBg5v=*J%l_pq(;zaa*AO_XDBGy zd03kCko`qA@tLHjJ=dR%V;)PS=%styGi??#HS;^@D7!kfsvh$UwjkX{5 zWgxjqX|cY?+ER!)f8JpEq(ko1PG6>$B2~jN=UF!ZhPUm{a%X!xHf&Lrp05Wg6^1MX zfH6c>N!Rv+KiW`GOf)~5J}iiyRWSL5Ys4w zszA#s0Na8+MsgURYqcmRa3{`-HZT4 z={(v%9-Y@4!h<%WpRjw>%+72y2W3SJDOKqkt3ysA(o~28zf(r`lr8n3s-Z}C<{Xd$ zq}p;g&tk98p>dy{uGbdQRrDEL12RMeJJaSadY%Bu2TYW2CI_z39E%zr`9EwCfg2w9 zQSjz&P?~*Kj({6UKHHf_NW4Cjx-SOeZy_$Fa=bM1R+!-JY5lno-E}IiUB)hcYd!ZD z?#BLcV`rMlij64$nbD*=z?Bh5RE_>*;9-!u@WZaE&X(Ec$=~8BELi9@tjv?{E6M9) zB9N=20^f~sW!8}?UjYkyR#>Ad*Fmq&VjM!%>QTko2Mtof$sSc#( zm#m%9qY(a!qB?Inn=R5E(ef!t2p$PKVPO3$J+3nSK)UjdgF;ebeP-yVaK{UKnUnd* zyDN~FBsSZlOe82>(kcz1@p*JY5e7GRVdHc>S3*!r`qt<+G5S3Cy8UDp{cJ4&RDrAI zSunznRa^L3x47zab>!zo@{=+JVqcFp15wz)MvNF{5zqP+DftUloG!2R6LUKSC}at*@ck?W7m4Cs_| z8+|32ByR$#Si@dX{ta`-avz;@TesHh6?s8D<&UM;&9>go+P+himW`f#7;_k@PSNa7MU*&2b?CBiH?LU`sMN~=G7m%JWt@)ZuWNT6b|tYB;>wB zJ0>X1Wu46ZfhvXaVy`vR`W@FaqA!KtRK3hsnX9KOp~Pv{Fy$_}-a`l$60V%~lF$jF zH}!)^otQ>246PA#CtlAP1$Q5a4oM;psdqExdwu6ge%Z5YsIjCV=fU+A3Yi%SuN;sp zkPua$ddF1m#8}lPqS9*?8%fF)BSDYDSZFLRS>z8AG-M$f-djc65RfykF_d`}8P|;X z`d_~VaI)(4HXB9Ze;fdk3{i>MpvGkH=G6++uV|*FoiFDV9f6O$V%I84T+gtX#@LNV z8!}9g1btWk`l0fPM0DST(85ZpY4_x5f0l*GC8#DTuAS$vp0OzyS{P@d;Lnc$@Ng7v z+LC*`*gof+45X~)Yi{p)GEZANg`vs(ExFX84&N;lFVD+tuQsU=99f`vt80t zO9|si{%NtL@|??@ql4#lsoVy>CsC&O+n`I-&X)!3sELrf4+2@;Z?WxR&`I6C-a8e! z5@uLtAWHSxSDMJCp;WV?^Yf$UrSuloDW?mFYJNNNN=whwhTZM~thzcM%nc+yDCkyP zDa<;*maz_ELTD8^>0O6mIQ(mL?FUbPFn`Exg4q_&HyK%?~(mDfQN8@+Gd z$qRIJ?`DZ>6S>qm>H-qXR`?F?y@|?UCD(MDZ)rt z8ZIUlXATrV{2ZkW39&T186uQZ5Cf99$QuKTt4f9rMr#MJV$VUvFR zb-%2ea5fg|mK5a$8N)0Hlm5bvw{;9#u<6wL-2+g!LhI8&8`Bp0WbRcnChB;{kg1FA z@6wlGUFimoE9CWMkB!#A=`fIbr8*K#Xq75kT91`(-XY?lp9=Gz80F~;h)gr8ZDo!W z&#>W;!q+wLHNeUv*vatSo~#t@$x1f7-v5oV%?A9d0EX95wU9|{oK!h%r;Yw|Io;N- z`5bB&H|%r=+^b(onCbSGW;_UPLPFAAKXc%bA|tkpx9hWh>m^d3%l$M?$O`)yvSe-! zTiLIpll+2z=C{IUxopdSnceQNuf#e&h`Af4LX&GX{nu!jA`!;bbV9;i{Red5QV0q^ z_b2nJaf_hdz)Yo)Th(Uq!C+ym*??fWn4w`~weu)7vVHiaf86-NZL=3Zg5#TJqg}(j zm}ZvP#XcnUu2LEgfB$4IRO z)Fp4x>rE#mcVX{dM{84&ZK4*qh28Z4H7lh)`pJm+iS%XdoxAyajjh9P_GADrE+@gk z3||H|6=1{qI$1jR&H+x(;;pgY`NfXE5J_ggz9a;JM<8!W91#I?fGgqC#}|@g9Zhh% z!YdEYfHjvzTHir15Xv;mW}CpJU}z%ay~Ylp_*{^o_W{eQGZ_03pf;Q7%u&R(9kI{F z*-fWGiy^)bt757pKNc?0^t5c)OxIM}q@v(SF6$4XJ>M3h@VB=Da1FQ+9`x*w94Kc( zokCdu8rQ4FMjt_w6cWFuKz_LC9;NHh{&g7UI_*A^*H+;Nkx&YMhzF85i9$FwD&O82 z=8c(hTX=33=CM^a!WHgYsm60c5+;m1K9i8xwzW<~HV1Rf$-<_M+f&qE808&dNnq7q zromU5sT6*oY+R@Mxx{ z-$aG&6IXw16~CNG=#$Dz4HEsG1Yf1#!GbWI*Q&+0dX;?I9dm-OCuejUFZ9RbFq4(P0vhDuS;UG^V|$3SM+A}}O;P!ge-->6l}#KxRqZ`2fPvY4wNp^xfhhF2_1 zVxXu;WH~Jra1WVz!A~VSzY}ODi{loGQpJFz0HKP>NWeyr zwBu`~uUQHBe2l$!A%(%gvO-8$>|W@$g~VM9TB12>@Q)Pb8=>cDH(62`BrXb-vf;FW zVbArJFGPrSKiS`fMZ|6M-NG&K(|92<6e_qq4d#5QbqAARdWBp?SbqfUrXYE;YeI8R z{A5;AlYYK$dDge~Eh&zRs??QZvzKq)a>DD{q~AWuezo)Ws&}?Z{7>Zn61!7f#M+$} za@`is#oh76bL(vO?qZ}DcpFvgDV48buv6lCTI&re6Ml`5r8<7SK?Qze-e0#r=(Ax7UX zW_~up8+j$pX@+6jQKipj)=513N9#TQwK)#`dQ|jT$&zg<=^GI$It@ z+ug@>z1RTm9sV5D!u`~!en#*2&tXsxeEWg2@)_8?LzWQ^gR0kUSh8k;78*VzD98#o#V@Etu%8aRzv$_UE1MJzS_@icia3IQHE@ z2@KA)db`6ug-#%a*18YBC%~;Mbwr}eIX6(2>$EprHehU%cFRxr$!<;Iadjdg&V4(n zJNync=ysrhTVLujB%~%$j9qi?ejp6p;W_?*Yq?%lJeRO-J>bU>hM$x^yi;w?)gH0|3#d4$Y5b7o!zmI>`Vyvg|P?{an*d7cO2e zs1!&l`Y!4{M||orDdccbgxmQ5Q)Re5VLw_>VWidauli0Ci=XRRNjHHdv?H!DG&lY0 zRV|(g180l!U>vP}$>?wwmABYjAbHmvA+xSKQ;Zhz&k`;IZ2AO8r(2Ew4|{(dRb|w* zkHXR@Qj*dkg3?mbAkrWRN;d*-Qo2Dz1*KaWq(o{{n{JQ}H_fKIyYpK-&p7Wc}fp`pfJyzzI=t&L7* z3HVNPM@6>#P7U9lnsgA7`+wsy%df5T&amD~bX1;2HF1i@mMJI3TwFRiy#3%&`M9~o znd^EhY5=VWsGfG1+H^=X*6#giCQ?MAdG88eEEf>3&G3E9iHruMm1xqpeRxQaIn{3A zQLn^4wf(L+L(8&b+(Plz-=e(BSnx@pxnU}17vQZ|zr||igBvW^YHZCs!*R9_3>2SF zz1v0K>VJQHi}Q#KYp{V8m2H^`W%J+~Cy%mPSQ9=sObNV3xJ~Y}&Ai1pMO|~Cb`)%& z{<*i^<%NAjR$>JJel;7APqPZz@vQ(}dHLxq7SByY90 zo6kKs?q=SG(bg>`QJ?)5y==yGT@K4i4~+IUIO?(M54{?p^C2}5tE#7DewPn0<>nLN zBVi^Jhp-CM%HE5OvZ?RAc1wteDJL$aN5h;}}pYoyHl4EEdD*rV_Gz(8z-XCx#_`;BBJjKqk? zjm6)KA3!ep;NWy1wA(qU`fYdJ6TbMDmLtXIKpxZ zzIq-J5fRwCWLz6+Q+uNJ%VVe1nlt9RFN^!y^b76L0##{B8I8C7lu|+$Ke;E;f?0Hm z6!L3!jVb_UV>#IMVW@NePe&^$Uq8jaN8cAV)Zgk}c6ijg>@qL0edd&LbS^N`>2lCq z6;e&?x4ZLF{5~Zfb#;iv$LSgk*T(GGm^D|UC=3*~%sOiXSuc@@O3KnAe)VHpWXi;* z5H!)?YGki(ztup=W(|dsg{VMxfWW!*N*c@Wle9fL&u!dB#0tHl-b1*{Wc3&X(uaEi z?!P-@a~n+_5uv*~Zno6OqI$Y|Z15Jj;u`Qo+F?Cw&81wD)fii4t*)axr}GPR-qqZC zPU0cF?2LXJvn}%TQR@0!1-{ANm#;~hj~ZxyJL;P|`P zyeC2VW9Nv?;?|O62}6DLOXB>!>YEGm9;@uAu!b<_{8y!4gP&PQ>x7IaHePpRIdjv!v3XP8(K3 zR5RT=)p+Rjwmqw87Si4-|T<_XM zn6cMdmrW%lL_&P`c^S&Eup^F_9+LO^CQ_)nz^^~)e}tk)-gV4?xR&|qYb5ifROBuh zXB5#n{<&+U-Gi{rCB1fLyZGz4YzI^9{TvF(aY{f<){^&=7a#s0SIg)iYuI}BXWn%$ zkRMlzu8y7&CL6MMVdt;}%xF*Tht(3uu0Wfu6~q1t%YD*4iHIxwkBO)BVWzM42x_3Z zHGOtqR&}J8z`ysddRiA|8QV7YN>XFNT9X^?)cRy()b+3b$W((mjjdVZMg-_O63yXyZV*DN z-r)izz8=7@S*YFQg7@+6-Eo6xIj#jd6$N{M9sncMtG z2R|2Ae>EX=;>pu9Sm&o#l3$oVBiA$wt2p`J7=i|shOBTw-E`5^zTq=MkQjw{C$=B$ zQ4pjFHT+c|Bf@;+9i+9|{__R_ke3so&%DW_K|`gXd-OoQ!S}^9n;niDvM2>_(BHq} z`a$>c0O?$~H%^%04|MgvL&+Fi>*}4q8{z(INv^-?1D60C_kAIY!#@+h|78W!_yD>M z<93r_|4j}S5Y@v4%scpA?x^a&L!ttnPf|UupW>hQtp7d#<$uP$ z_w8Dn@c&Hi&)5Hd-rWD$mj81={=YnPd4%|wpB_Xe>V6gIx=CDr4(w9M!$ymHOl-)H zJ6NZ`MLv*i_Ysv2Va|s^ss29yKDsYxQZSudwMTB<1Bc!2d4*>8>x2Tby$Tk0x98 zD2-47z-K*(eDGb;o9-?R4t?HWFVXQgH8F`vNziVyrvF*0|9>adl)3z3;O4t_dS

y%v5lgVDzBbxyI-i1`GT^2 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