From ff2b5d408d13b5eedb104450b723622b06fab6f4 Mon Sep 17 00:00:00 2001 From: Shannon Klaus Date: Thu, 23 Jul 2026 08:10:33 -0600 Subject: [PATCH 1/4] Convert examples to documentation ready code with harness for setup, cleanup, and verification --- .github/workflows/pr-open.yaml | 25 + .../sdk/policy/BehaviorFileMonitor.java | 53 +- .../com/aerospike/client/sdk/SuiteCore.java | 2 + .../BehaviorFileMonitorRestartTest.java | 116 + examples/README.md | 69 + examples/run_examples | 5 + .../aerospike/examples/AelTestSpecRunner.java | 126 +- .../java/com/aerospike/examples/Args.java | 34 + .../com/aerospike/examples/BatchExample.java | 13 +- .../examples/BehaviorHierarchicalExample.java | 23 +- .../examples/BehaviorYamlExample.java | 12 +- .../com/aerospike/examples/CommonExample.java | 19 +- .../examples/CompleteYamlConfigExample.java | 27 +- .../java/com/aerospike/examples/Example.java | 131 +- .../aerospike/examples/ExampleContext.java | 61 + .../aerospike/examples/ExampleDefinition.java | 38 + .../aerospike/examples/ExampleFixture.java | 34 + .../aerospike/examples/ExampleRegistry.java | 112 + .../com/aerospike/examples/ExampleResult.java | 41 + .../aerospike/examples/ExampleRunResult.java | 49 + .../com/aerospike/examples/ExampleRunner.java | 196 ++ .../examples/ExampleSkipException.java | 26 + .../com/aerospike/examples/ExampleStatus.java | 23 + .../examples/JUnitXmlReportWriter.java | 95 + .../java/com/aerospike/examples/Main.java | 59 +- .../examples/MapRemoveByKeyRangeTest.java | 308 ++- .../examples/OperationDifferences.java | 58 +- .../com/aerospike/examples/QueryExamples.java | 1924 ++++++++--------- .../com/aerospike/examples/RosterExample.java | 18 +- .../examples/StudentScoresExample.java | 50 +- .../TransactionProcessingExample.java | 201 +- .../examples/YamlConfigConnectionExample.java | 31 +- .../aerospike/examples/YamlConfigExample.java | 36 +- .../examples/ecommerce/EcommerceExample.java | 124 +- .../examples/fixtures/ExampleAssertions.java | 97 + .../examples/fixtures/ExampleFixtures.java | 263 +++ .../com/aerospike/examples/query/Address.java | 60 + .../examples/query/AddressMapper.java | 54 + .../aerospike/examples/query/Customer.java | 83 + .../examples/query/CustomerMapper.java | 52 + 40 files changed, 3037 insertions(+), 1711 deletions(-) create mode 100644 client/src/test/java/com/aerospike/client/sdk/policy/BehaviorFileMonitorRestartTest.java create mode 100644 examples/README.md create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleContext.java create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleDefinition.java create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleFixture.java create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleRegistry.java create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleResult.java create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleRunResult.java create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleRunner.java create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleSkipException.java create mode 100644 examples/src/main/java/com/aerospike/examples/ExampleStatus.java create mode 100644 examples/src/main/java/com/aerospike/examples/JUnitXmlReportWriter.java create mode 100644 examples/src/main/java/com/aerospike/examples/fixtures/ExampleAssertions.java create mode 100644 examples/src/main/java/com/aerospike/examples/fixtures/ExampleFixtures.java create mode 100644 examples/src/main/java/com/aerospike/examples/query/Address.java create mode 100644 examples/src/main/java/com/aerospike/examples/query/AddressMapper.java create mode 100644 examples/src/main/java/com/aerospike/examples/query/Customer.java create mode 100644 examples/src/main/java/com/aerospike/examples/query/CustomerMapper.java diff --git a/.github/workflows/pr-open.yaml b/.github/workflows/pr-open.yaml index 99692a8d..e9b54d01 100644 --- a/.github/workflows/pr-open.yaml +++ b/.github/workflows/pr-open.yaml @@ -130,6 +130,30 @@ jobs: } } + - name: Build & run examples + run: | + mvn \ + --batch-mode \ + --no-transfer-progress \ + --update-snapshots \ + --fail-at-end \ + --errors \ + -pl examples \ + -am \ + package \ + -DskipTests + + # Run from the examples module so that examples which load YAML via + # relative paths (src/main/resources/*.yml) resolve their config files. + cd examples + java -jar target/*-jar-with-dependencies.jar all \ + -h 127.0.0.1 \ + -p 3000 \ + -n test \ + --report target/example-reports/TEST-examples.xml + env: + MAVEN_OPTS: ${{ vars.MAVEN_OPTS }} + - name: Build & run integration tests run: | mvn \ @@ -151,5 +175,6 @@ jobs: path: | **/target/surefire-reports/** **/target/failsafe-reports/** + examples/target/example-reports/** if-no-files-found: ignore retention-days: ${{ vars.ARTIFACT_RETENTION_DAYS }} diff --git a/client/src/main/java/com/aerospike/client/sdk/policy/BehaviorFileMonitor.java b/client/src/main/java/com/aerospike/client/sdk/policy/BehaviorFileMonitor.java index 6666acb1..99d07f0c 100644 --- a/client/src/main/java/com/aerospike/client/sdk/policy/BehaviorFileMonitor.java +++ b/client/src/main/java/com/aerospike/client/sdk/policy/BehaviorFileMonitor.java @@ -43,12 +43,11 @@ class BehaviorFileMonitor implements Closeable { private static final long DEFAULT_RELOAD_DELAY_MS = 1000; // 1 second delay to avoid multiple reloads private final BehaviorRegistry registry = BehaviorRegistry.getInstance(); - // Use a scheduled executor with 2 threads: one for monitoring, one for reload tasks - private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2, r -> { - Thread t = new Thread(r, "BehaviorFileMonitor"); - t.setDaemon(true); - return t; - }); + // Scheduled executor with 2 threads: one for monitoring, one for reload tasks. + // Created lazily and recreated after shutdown so that monitoring can be + // started, stopped, and started again within the same process (this is a + // singleton, so a single shut-down executor must not permanently disable it). + private ScheduledExecutorService executor; private Path yamlFilePath; private WatchService watchService; @@ -110,11 +109,29 @@ public void startMonitoring(String yamlFilePath, long reloadDelayMs) throws IOEx // Start monitoring thread isMonitoring = true; - executor.submit(this::monitorFile); + ensureExecutor().submit(this::monitorFile); Log.debug("Started monitoring YAML file: " + yamlFilePath); } + /** + * Return the executor, (re)creating it if it has never been started or was + * previously shut down. Because this class is a process-wide singleton, a + * prior {@link #shutdown()} (for example via try-with-resources on the + * {@code Closeable} returned by {@code Behavior.startMonitoringWithResource}) + * must not permanently prevent monitoring from being started again. + */ + private synchronized ScheduledExecutorService ensureExecutor() { + if (executor == null || executor.isShutdown()) { + executor = Executors.newScheduledThreadPool(2, r -> { + Thread t = new Thread(r, "BehaviorFileMonitor"); + t.setDaemon(true); + return t; + }); + } + return executor; + } + /** * Stop monitoring the file */ @@ -220,7 +237,7 @@ private void handleFileChange() { lastModified = currentModified; // Schedule reload with delay to avoid multiple reloads - executor.schedule(this::loadBehaviors, reloadDelayMs, TimeUnit.MILLISECONDS); + ensureExecutor().schedule(this::loadBehaviors, reloadDelayMs, TimeUnit.MILLISECONDS); } catch (Exception e) { Log.error("Error handling file change: " + e.getMessage()); @@ -258,13 +275,25 @@ private void loadBehaviors() { */ public void shutdown() { stopMonitoring(); - executor.shutdown(); + + ScheduledExecutorService exec; + synchronized (this) { + exec = executor; + // Clear the reference so a subsequent startMonitoring() recreates it. + executor = null; + } + + if (exec == null) { + return; + } + + exec.shutdown(); try { - if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { - executor.shutdownNow(); + if (!exec.awaitTermination(5, TimeUnit.SECONDS)) { + exec.shutdownNow(); } } catch (InterruptedException e) { - executor.shutdownNow(); + exec.shutdownNow(); Thread.currentThread().interrupt(); } } diff --git a/client/src/test/java/com/aerospike/client/sdk/SuiteCore.java b/client/src/test/java/com/aerospike/client/sdk/SuiteCore.java index e6afe30e..56d6d6a8 100644 --- a/client/src/test/java/com/aerospike/client/sdk/SuiteCore.java +++ b/client/src/test/java/com/aerospike/client/sdk/SuiteCore.java @@ -20,6 +20,7 @@ import org.junit.platform.suite.api.Suite; import com.aerospike.client.sdk.policy.AsyncRecordStreamTest; +import com.aerospike.client.sdk.policy.BehaviorFileMonitorRestartTest; import com.aerospike.client.sdk.policy.BehaviorTest; import com.aerospike.client.sdk.policy.BehaviorYamlTest; import com.aerospike.client.sdk.policy.QueryProducerCancellationTest; @@ -28,6 +29,7 @@ @Suite @SelectClasses({ AsyncRecordStreamTest.class, + BehaviorFileMonitorRestartTest.class, BehaviorTest.class, BehaviorYamlTest.class, QueryProducerCancellationTest.class, diff --git a/client/src/test/java/com/aerospike/client/sdk/policy/BehaviorFileMonitorRestartTest.java b/client/src/test/java/com/aerospike/client/sdk/policy/BehaviorFileMonitorRestartTest.java new file mode 100644 index 00000000..256b9f9d --- /dev/null +++ b/client/src/test/java/com/aerospike/client/sdk/policy/BehaviorFileMonitorRestartTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.client.sdk.policy; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Regression tests for {@link BehaviorFileMonitor} lifecycle. + * + *

The monitor is a process-wide singleton backed by a scheduled executor. A + * prior bug shut that executor down on {@code close()}/{@code shutdown()} and + * never recreated it, so the second {@code startMonitoring} in a JVM failed with + * {@code RejectedExecutionException}. Running examples in separate JVMs hid this; + * running them in one process (or any real app that stops and restarts + * monitoring) surfaced it. These tests lock in that monitoring is restartable. + */ +@DisplayName("BehaviorFileMonitor Restart Tests") +public class BehaviorFileMonitorRestartTest { + + private static final String YAML = """ + behaviors: + simple: + allOperations: + abandonCallAfter: 5s + maximumNumberOfCallAttempts: 3 + """; + + private Path writeConfig(Path dir) throws IOException { + Path file = dir.resolve("behavior-monitor-test.yml"); + Files.writeString(file, YAML); + return file; + } + + @AfterEach + void tearDown() { + // Reset shared singleton state so tests do not leak into one another. + Behavior.shutdownMonitor(); + } + + @Test + @DisplayName("Should restart monitoring after a try-with-resources close in the same process") + void shouldRestartAfterClose(@TempDir Path tempDir) throws IOException { + Path config = writeConfig(tempDir); + + try (Closeable monitor = Behavior.startMonitoringWithResource(config.toString())) { + assertNotNull(monitor); + assertTrue(Behavior.isMonitoring(), "monitoring should be active during first session"); + } + assertFalse(Behavior.isMonitoring(), "monitoring should stop after close"); + + // Before the fix, this second start reused the terminated singleton + // executor and threw RejectedExecutionException. + assertDoesNotThrow(() -> { + try (Closeable monitor = Behavior.startMonitoringWithResource(config.toString())) { + assertNotNull(monitor); + assertTrue(Behavior.isMonitoring(), "monitoring should be active during second session"); + } + }); + } + + @Test + @DisplayName("Should restart monitoring after an explicit shutdown") + void shouldRestartAfterExplicitShutdown(@TempDir Path tempDir) throws IOException { + Path config = writeConfig(tempDir); + + Behavior.startMonitoring(config.toString()); + assertTrue(Behavior.isMonitoring()); + Behavior.shutdownMonitor(); + assertFalse(Behavior.isMonitoring()); + + assertDoesNotThrow(() -> Behavior.startMonitoring(config.toString())); + assertTrue(Behavior.isMonitoring()); + } + + @Test + @DisplayName("Should support several sequential monitoring sessions") + void shouldSupportSeveralSequentialSessions(@TempDir Path tempDir) throws IOException { + Path config = writeConfig(tempDir); + + assertDoesNotThrow(() -> { + for (int i = 0; i < 3; i++) { + try (Closeable monitor = Behavior.startMonitoringWithResource(config.toString())) { + assertNotNull(monitor); + assertTrue(Behavior.isMonitoring(), "monitoring should be active in session " + i); + } + } + }); + } +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..15ccb39b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,69 @@ +# Aerospike Java SDK Examples + +The examples module contains runnable examples that double as documentation source. Example classes should focus on the SDK usage being demonstrated; the runner owns connection setup, configuration, verification, cleanup, and CI reporting. + +## Run Examples + +Build the examples JAR: + +```bash +mvn -pl examples -am package -DskipTests +``` + +Run one example: + +```bash +./examples/run_examples BatchExample -h localhost -p 3000 -n test +``` + +Run all registered examples: + +```bash +./examples/run_examples all -h localhost -p 3000 -n test +``` + +Write a JUnit XML report: + +```bash +./examples/run_examples all \ + -h localhost \ + -p 3000 \ + -n test \ + --report examples/target/example-reports/TEST-examples.xml +``` + +Useful runner options: + +- `--fail-fast`: stop after the first failed example. +- `--include-tags smoke,records`: run examples with at least one matching tag. +- `--exclude-tags config`: skip examples with matching tags. + +## Add An Example + +1. Create a class in `src/main/java/com/aerospike/examples` that extends `Example`. +2. Implement `runExample()` with no constructor or runner arguments; use `cluster()`, `dataSet()`, `namespace()`, and `console` from `Example` when needed. +3. Use `dataSet()` or `dataSet("set-name")` when the example writes records, unless the example is specifically demonstrating a fixed namespace or set. +4. Register the example in `ExampleRegistry` with tags and an `ExampleFixture`. +5. Add verification in a fixture under `com.aerospike.examples.fixtures`. + +Every example registered for CI should have an explicit fixture decision: + +- Use a fixture with `setup`, `verify`, and `cleanup` when the example changes server state. +- Use `ExampleFixture.NONE` only when there is no meaningful post-run state to verify. +- Throw `ExampleSkipException` for server capability or configuration gates that should be reported as skipped rather than failed. + +## Verification Model + +The runner executes each registered example in this order: + +1. Fixture setup +2. Example body +3. Fixture verification +4. Fixture cleanup +5. Result reporting + +Verification should check state after the example runs. Prefer reusable helpers in `ExampleAssertions` for common checks such as truncating a dataset, counting records, checking that a record exists, and comparing bin values. + +## CI + +PR CI builds the examples module, runs `all` against the Aerospike server provisioned for integration tests, and writes `examples/target/example-reports/TEST-examples.xml`. The report is uploaded with the other test artifacts so example failures are visible as test failures. diff --git a/examples/run_examples b/examples/run_examples index 33d87fe4..63795475 100755 --- a/examples/run_examples +++ b/examples/run_examples @@ -28,5 +28,10 @@ if [ -z "$JAR_FILE" ]; then exit 1 fi +# Run from the examples directory so that examples which load YAML via relative +# paths (src/main/resources/*.yml) resolve their config files regardless of the +# caller's working directory. +cd "$SCRIPT_DIR" || exit 1 + # Run Main with all arguments java -cp "$JAR_FILE" com.aerospike.examples.Main "$@" diff --git a/examples/src/main/java/com/aerospike/examples/AelTestSpecRunner.java b/examples/src/main/java/com/aerospike/examples/AelTestSpecRunner.java index b6444fd2..9631db69 100644 --- a/examples/src/main/java/com/aerospike/examples/AelTestSpecRunner.java +++ b/examples/src/main/java/com/aerospike/examples/AelTestSpecRunner.java @@ -21,8 +21,6 @@ import java.util.Map; import java.util.TreeMap; -import com.aerospike.client.sdk.Cluster; -import com.aerospike.client.sdk.ClusterDefinition; import com.aerospike.client.sdk.DataSet; import com.aerospike.client.sdk.Record; import com.aerospike.client.sdk.RecordStream; @@ -38,49 +36,53 @@ * path functions, transactions, rank-based access, return type variations, * and edge cases. */ -public class AelTestSpecRunner { +public class AelTestSpecRunner extends Example { private static final String SEP = "=".repeat(70); - private static int totalTests = 0; - private static int passedTests = 0; - private static int failedTests = 0; - private static int errorTests = 0; - - public static void main(String[] args) throws Exception { - try (Cluster cluster = new ClusterDefinition("localhost", 3100).connect()) { - Session session = cluster.createSession(Behavior.DEFAULT); - DataSet set = DataSet.of("test", "ael_test_spec"); - - session.truncate(set); - Thread.sleep(200); - setupTestData(session, set); - - testScalarBinAccess(session, set); - testTypeCasting(session, set); - testTypeDerivation(session, set); - testMapAccess(session, set); - testListAccess(session, set); - testNestedCDT(session, set); - testArithmetic(session, set); - testBitwise(session, set); - testComparison(session, set); - testLogical(session, set); - testControlStructures(session, set); - testMetadata(session, set); - testPathFunctions(session, set); - testTransactionScenario(session, set); - testRankBased(session, set); - testReturnTypes(session, set); - testEdgeCases(session, set); - - printSummary(); - } + private int totalTests = 0; + private int passedTests = 0; + private int failedTests = 0; + private int errorTests = 0; + + @Override + public void runExample() throws Exception { + Session session = cluster().createSession(Behavior.DEFAULT); + DataSet set = dataSet("ael_test_spec"); + + // The runner's fixture truncates ael_test_spec before this example runs. + setupTestData(session, set); + + testScalarBinAccess(session, set); + testTypeCasting(session, set); + testTypeDerivation(session, set); + testMapAccess(session, set); + testListAccess(session, set); + testNestedCDT(session, set); + testArithmetic(session, set); + testBitwise(session, set); + testComparison(session, set); + testLogical(session, set); + testControlStructures(session, set); + testMetadata(session, set); + testPathFunctions(session, set); + testTransactionScenario(session, set); + testRankBased(session, set); + testReturnTypes(session, set); + testEdgeCases(session, set); + + printSummary(); + + // This is a diagnostic spec-coverage tool, not a pass/fail conformance + // gate. The failures and errors above are largely documented AEL + // limitations (see the "Known issue" notes and OperationDifferences), so + // the example does not fail CI on them. Inspect the summary to track how + // much of the spec the implementation currently supports. } // ===================================================================== // Test Data Setup // ===================================================================== - static void setupTestData(Session session, DataSet set) { + void setupTestData(Session session, DataSet set) { // Record 1: scalar bins session.upsert(set.id(1)) .bin("intBin").setTo(42) @@ -221,7 +223,7 @@ static void setupTestData(Session session, DataSet set) { // ===================================================================== // 1. Scalar Bin Access // ===================================================================== - static void testScalarBinAccess(Session session, DataSet set) { + void testScalarBinAccess(Session session, DataSet set) { section("1. SCALAR BIN ACCESS"); readCheck("S01", session, set, 1, "$.intBin", 42L); @@ -239,7 +241,7 @@ static void testScalarBinAccess(Session session, DataSet set) { // ===================================================================== // 2. Type Inference and Casting // ===================================================================== - static void testTypeCasting(Session session, DataSet set) { + void testTypeCasting(Session session, DataSet set) { section("2. TYPE INFERENCE AND CASTING"); readCheck("T01", session, set, 1, "$.intBin.asFloat()", 42.0); @@ -260,7 +262,7 @@ static void testTypeCasting(Session session, DataSet set) { // ===================================================================== // 2b. Type Derivation (no explicit get(type:) annotations) // ===================================================================== - static void testTypeDerivation(Session session, DataSet set) { + void testTypeDerivation(Session session, DataSet set) { section("2b. TYPE DERIVATION"); System.out.println(" Record 11: a=10(INT), b=10(INT), c=true(BOOL), d=11(INT),"); System.out.println(" e=3.14(FLOAT), f=\"hello\"(STRING), g=false(BOOL)"); @@ -384,7 +386,7 @@ static void testTypeDerivation(Session session, DataSet set) { // ===================================================================== // 3. Map Access // ===================================================================== - static void testMapAccess(Session session, DataSet set) { + void testMapAccess(Session session, DataSet set) { section("3. MAP ACCESS"); readCheck("M01", session, set, 2, "$.m.alpha.get(type: INT)", 10L); @@ -430,7 +432,7 @@ static void testMapAccess(Session session, DataSet set) { // ===================================================================== // 4. List Access // ===================================================================== - static void testListAccess(Session session, DataSet set) { + void testListAccess(Session session, DataSet set) { section("4. LIST ACCESS"); readCheck("L01", session, set, 2, "$.l.[0].get(type: INT)", 50L); @@ -457,7 +459,7 @@ static void testListAccess(Session session, DataSet set) { // ===================================================================== // 5. Nested CDT Navigation // ===================================================================== - static void testNestedCDT(Session session, DataSet set) { + void testNestedCDT(Session session, DataSet set) { section("5. NESTED CDT NAVIGATION"); readCheck("N01", session, set, 3, "$.profile.address.city.get(type: STRING)", "Austin"); @@ -480,7 +482,7 @@ static void testNestedCDT(Session session, DataSet set) { // ===================================================================== // 6. Arithmetic // ===================================================================== - static void testArithmetic(Session session, DataSet set) { + void testArithmetic(Session session, DataSet set) { section("6. ARITHMETIC"); readCheck("A01", session, set, 7, "$.price + $.qty", 105L); @@ -500,7 +502,7 @@ static void testArithmetic(Session session, DataSet set) { // ===================================================================== // 7. Bitwise Operations // ===================================================================== - static void testBitwise(Session session, DataSet set) { + void testBitwise(Session session, DataSet set) { section("7. BITWISE OPERATIONS"); readCheck("B01", session, set, 1, "$.intBin & 15", 10L); @@ -522,7 +524,7 @@ static void testBitwise(Session session, DataSet set) { // ===================================================================== // 8. Comparison Operators // ===================================================================== - static void testComparison(Session session, DataSet set) { + void testComparison(Session session, DataSet set) { section("8. COMPARISON OPERATORS"); filterCheck("C01", session, set, 1, "$.intBin == 42", true); @@ -545,7 +547,7 @@ static void testComparison(Session session, DataSet set) { // ===================================================================== // 9. Logical Operators // ===================================================================== - static void testLogical(Session session, DataSet set) { + void testLogical(Session session, DataSet set) { section("9. LOGICAL OPERATORS"); filterCheck("LG01", session, set, 1, "$.intBin > 40 and $.strBin == 'hello'", true); @@ -567,7 +569,7 @@ static void testLogical(Session session, DataSet set) { // ===================================================================== // 10. Control Structures // ===================================================================== - static void testControlStructures(Session session, DataSet set) { + void testControlStructures(Session session, DataSet set) { section("10. CONTROL STRUCTURES"); // 10.1 Variable binding (with...do — current implementation) @@ -622,7 +624,7 @@ static void testControlStructures(Session session, DataSet set) { // ===================================================================== // 11. Metadata // ===================================================================== - static void testMetadata(Session session, DataSet set) { + void testMetadata(Session session, DataSet set) { section("11. METADATA"); readPrint("MD01", session, set, 1, "$.ttl()", "TTL in seconds"); @@ -640,7 +642,7 @@ static void testMetadata(Session session, DataSet set) { // ===================================================================== // 12. Path Functions // ===================================================================== - static void testPathFunctions(Session session, DataSet set) { + void testPathFunctions(Session session, DataSet set) { section("12. PATH FUNCTIONS"); readCheck("PF01", session, set, 2, "$.m.alpha.get(type: INT)", 10L); @@ -670,7 +672,7 @@ static void testPathFunctions(Session session, DataSet set) { // ===================================================================== // 13. Transaction Scenario // ===================================================================== - static void testTransactionScenario(Session session, DataSet set) { + void testTransactionScenario(Session session, DataSet set) { section("13. TRANSACTION SCENARIO"); readCheck("TX01", session, set, 8, "$.txns.{}.count()", 12L); @@ -723,7 +725,7 @@ static void testTransactionScenario(Session session, DataSet set) { // ===================================================================== // 14. Rank-Based Access (Record 9) // ===================================================================== - static void testRankBased(Session session, DataSet set) { + void testRankBased(Session session, DataSet set) { section("14. RANK-BASED ACCESS"); System.out.println(" scores rank order: english(78) < math(85) < history(88) < science(92) < art(95)"); System.out.println(); @@ -751,7 +753,7 @@ static void testRankBased(Session session, DataSet set) { // ===================================================================== // 15. Return Type Variations // ===================================================================== - static void testReturnTypes(Session session, DataSet set) { + void testReturnTypes(Session session, DataSet set) { section("15. RETURN TYPE VARIATIONS"); System.out.println(" Using $.m.{alpha,beta,gamma} on Record 2"); System.out.println(); @@ -781,7 +783,7 @@ static void testReturnTypes(Session session, DataSet set) { // ===================================================================== // 16. Edge Cases // ===================================================================== - static void testEdgeCases(Session session, DataSet set) { + void testEdgeCases(Session session, DataSet set) { section("16. EDGE CASES"); readCheck("E01", session, set, 2, "$.l.[0].get(type: INT)", 50L); @@ -811,7 +813,7 @@ static void testEdgeCases(Session session, DataSet set) { // ===================================================================== // Helper Methods // ===================================================================== - static void section(String title) { + void section(String title) { System.out.println(); System.out.println(SEP); System.out.println(title); @@ -822,7 +824,7 @@ static void section(String title) { /** * Execute a read expression and compare to an expected value. */ - static void readCheck(String id, Session session, DataSet set, int pk, + void readCheck(String id, Session session, DataSet set, int pk, String ael, Object expected) { totalTests++; System.out.printf(" [%s] %s%n", id, ael); @@ -859,7 +861,7 @@ static void readCheck(String id, Session session, DataSet set, int pk, /** * Execute a read expression, print the result without checking. */ - static void readPrint(String id, Session session, DataSet set, int pk, + void readPrint(String id, Session session, DataSet set, int pk, String ael, String description) { totalTests++; System.out.printf(" [%s] %s%n", id, ael); @@ -882,7 +884,7 @@ static void readPrint(String id, Session session, DataSet set, int pk, /** * Execute a read expression where we expect an error. */ - static void readExpectError(String id, Session session, DataSet set, int pk, + void readExpectError(String id, Session session, DataSet set, int pk, String ael, String description) { totalTests++; System.out.printf(" [%s] %s%n", id, ael); @@ -906,7 +908,7 @@ static void readExpectError(String id, Session session, DataSet set, int pk, /** * Execute a filter expression and check whether the record is returned. */ - static void filterCheck(String id, Session session, DataSet set, int pk, + void filterCheck(String id, Session session, DataSet set, int pk, String ael, boolean expectFound) { totalTests++; System.out.printf(" [%s] %s%n", id, ael); @@ -936,7 +938,7 @@ static void filterCheck(String id, Session session, DataSet set, int pk, /** * Execute a filter expression and print the result without checking. */ - static void filterPrint(String id, Session session, DataSet set, int pk, + void filterPrint(String id, Session session, DataSet set, int pk, String ael, String description) { totalTests++; System.out.printf(" [%s] %s%n", id, ael); @@ -960,7 +962,7 @@ static String className(Object obj) { return obj == null ? "null" : obj.getClass().getSimpleName(); } - static void printSummary() { + void printSummary() { System.out.println(); System.out.println(SEP); System.out.printf("SUMMARY: %d total | %d passed | %d failed | %d errors%n", diff --git a/examples/src/main/java/com/aerospike/examples/Args.java b/examples/src/main/java/com/aerospike/examples/Args.java index a6a45ec4..e5dbeba1 100644 --- a/examples/src/main/java/com/aerospike/examples/Args.java +++ b/examples/src/main/java/com/aerospike/examples/Args.java @@ -17,6 +17,8 @@ package com.aerospike.examples; import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; @@ -40,6 +42,10 @@ public class Args { public final String clientKeyFile; public final AuthMode authMode; public final boolean useServicesAlternate; + public final String reportPath; + public final boolean failFast; + public final Set includeTags; + public final Set excludeTags; public Args(CommandLine cl) { this.host = cl.getOptionValue("h", "localhost"); @@ -90,6 +96,10 @@ public Args(CommandLine cl) { } this.useServicesAlternate = cl.hasOption("a"); + this.reportPath = cl.getOptionValue("report"); + this.failFast = cl.hasOption("fail-fast"); + this.includeTags = parseTags(cl.getOptionValue("include-tags")); + this.excludeTags = parseTags(cl.getOptionValue("exclude-tags")); } /** @@ -113,6 +123,28 @@ public static void addCommonOptions(Options options) { options.addOption("caFile", "caFile", true, "TLS CA certificate file path"); options.addOption("clientCertFile", "clientCertFile", true, "TLS client certificate file path"); options.addOption("clientKeyFile", "clientKeyFile", true, "TLS client key file path"); + options.addOption(null, "report", true, "Write JUnit XML example report to the given path"); + options.addOption(null, "fail-fast", false, "Stop after the first failed example"); + options.addOption(null, "include-tags", true, "Comma-separated example tags to include"); + options.addOption(null, "exclude-tags", true, "Comma-separated example tags to exclude"); + } + + private static Set parseTags(String value) { + Set tags = new TreeSet<>(); + + if (value == null || value.isBlank()) { + return Set.of(); + } + + for (String tag : value.split(",")) { + String trimmed = tag.trim(); + + if (!trimmed.isEmpty()) { + tags.add(trimmed); + } + } + + return Set.copyOf(tags); } @Override @@ -124,6 +156,8 @@ public String toString() { ", namespace='" + namespace + '\'' + ", set='" + set + '\'' + ", useServicesAlternate=" + useServicesAlternate + + ", includeTags=" + includeTags + + ", excludeTags=" + excludeTags + '}'; } } diff --git a/examples/src/main/java/com/aerospike/examples/BatchExample.java b/examples/src/main/java/com/aerospike/examples/BatchExample.java index 569104fd..d7644c9f 100644 --- a/examples/src/main/java/com/aerospike/examples/BatchExample.java +++ b/examples/src/main/java/com/aerospike/examples/BatchExample.java @@ -16,26 +16,19 @@ */ package com.aerospike.examples; -import com.aerospike.client.sdk.Cluster; import com.aerospike.client.sdk.DataSet; import com.aerospike.client.sdk.Session; import com.aerospike.client.sdk.policy.Behavior; public class BatchExample extends Example { - public BatchExample(Console console) { - super(console); - } - @Override - public void runExample(Cluster cluster, Args args) throws Exception { - Session session = cluster.createSession(Behavior.DEFAULT); + public void runExample() throws Exception { + Session session = cluster().createSession(Behavior.DEFAULT); System.out.println("*************"); System.out.println("* Batch tests"); System.out.println("*************"); - DataSet set = DataSet.of("test", "set"); - - session.truncate(set); + DataSet set = dataSet(); System.out.println("Batch Insert:"); session.insert(set.ids(1,2,3,4,5)) diff --git a/examples/src/main/java/com/aerospike/examples/BehaviorHierarchicalExample.java b/examples/src/main/java/com/aerospike/examples/BehaviorHierarchicalExample.java index 12925da2..b047cbfe 100644 --- a/examples/src/main/java/com/aerospike/examples/BehaviorHierarchicalExample.java +++ b/examples/src/main/java/com/aerospike/examples/BehaviorHierarchicalExample.java @@ -21,7 +21,6 @@ import java.time.LocalTime; import java.time.format.DateTimeFormatter; -import com.aerospike.client.sdk.Cluster; import com.aerospike.client.sdk.policy.Behavior; import com.aerospike.client.sdk.policy.Behavior.OpKind; import com.aerospike.client.sdk.policy.Behavior.OpShape; @@ -53,16 +52,12 @@ public class BehaviorHierarchicalExample extends Example { private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); private static final String DEFAULT_CONFIG_PATH = "src/main/resources/behavior-hierarchical-example.yml"; - public BehaviorHierarchicalExample(Console console) { - super(console); - } - @Override - public void runExample(Cluster cluster, Args args) throws Exception { + public void runExample() throws Exception { demonstrateDynamicReloading(DEFAULT_CONFIG_PATH); } - private void demonstrateDynamicReloading(String yamlFilePath) { + private void demonstrateDynamicReloading(String yamlFilePath) throws IOException { int refreshDelayMs = 5000; int reloadDelayMs = 2000; console.write("=== Behavior Hierarchical Example with Dynamic Reloading ===\n"); @@ -81,21 +76,19 @@ private void demonstrateDynamicReloading(String yamlFilePath) { console.write("\n" + "=".repeat(70)); console.write("Monitoring for changes... Modify the YAML file to see dynamic reloading."); - console.write("Settings will refresh every 5 seconds. Press Ctrl+C to stop."); + console.write("Settings refresh once in the example runner. Re-run locally to observe file changes."); console.write("=".repeat(70)); - // Keep running and show current settings periodically - while (true) { - Thread.sleep(refreshDelayMs); - console.write("\n" + getTimestamp() + " === Current Configuration ==="); - displayCurrentSettings(); - } + Thread.sleep(refreshDelayMs); + console.write("\n" + getTimestamp() + " === Current Configuration ==="); + displayCurrentSettings(); } catch (IOException e) { console.error("Error starting monitoring: " + e.getMessage()); - e.printStackTrace(); + throw e; } catch (InterruptedException e) { console.write("\nMonitoring interrupted"); + Thread.currentThread().interrupt(); } console.write("Monitoring stopped"); } diff --git a/examples/src/main/java/com/aerospike/examples/BehaviorYamlExample.java b/examples/src/main/java/com/aerospike/examples/BehaviorYamlExample.java index ea448dab..62901731 100644 --- a/examples/src/main/java/com/aerospike/examples/BehaviorYamlExample.java +++ b/examples/src/main/java/com/aerospike/examples/BehaviorYamlExample.java @@ -20,7 +20,7 @@ import java.io.IOException; import java.util.Set; -import com.aerospike.client.sdk.Cluster; + import com.aerospike.client.sdk.policy.Behavior; import com.aerospike.client.sdk.policy.ResolvedSettings; @@ -38,12 +38,8 @@ */ public class BehaviorYamlExample extends Example { - public BehaviorYamlExample(Console console) { - super(console); - } - @Override - public void runExample(Cluster cluster, Args args) throws Exception { + public void runExample() throws Exception { String yamlFilePath = "src/main/resources/behavior-example.yml"; try (Closeable monitor = Behavior.startMonitoringWithResource(yamlFilePath)) { @@ -130,9 +126,7 @@ public void runExample(Cluster cluster, Args args) throws Exception { } catch (IOException e) { console.error("Error loading behavior from YAML: " + e.getMessage()); - e.printStackTrace(); - } catch (InterruptedException e) { - console.write("Interrupted"); + throw e; } } diff --git a/examples/src/main/java/com/aerospike/examples/CommonExample.java b/examples/src/main/java/com/aerospike/examples/CommonExample.java index 21e35867..89490d91 100644 --- a/examples/src/main/java/com/aerospike/examples/CommonExample.java +++ b/examples/src/main/java/com/aerospike/examples/CommonExample.java @@ -19,7 +19,6 @@ import java.util.Optional; import com.aerospike.client.sdk.AerospikeException; -import com.aerospike.client.sdk.Cluster; import com.aerospike.client.sdk.DataSet; import com.aerospike.client.sdk.Record; import com.aerospike.client.sdk.RecordResult; @@ -41,10 +40,6 @@ * Examples for common commands. */ public class CommonExample extends Example { - public CommonExample(Console console) { - super(console); - } - public void printRecordStream(String header, RecordStream rs) { if (header != null) { System.out.println("=".repeat(40)); @@ -64,13 +59,9 @@ public void printRecordStream(String header, RecordStream rs) { } } @Override - public void runExample(Cluster cluster, Args args) throws Exception { - Session session = cluster.createSession(Behavior.DEFAULT); - DataSet set = DataSet.of("test", "set"); - - System.out.println("Truncate records"); - - session.truncate(set); + public void runExample() throws Exception { + Session session = cluster().createSession(Behavior.DEFAULT); + DataSet set = dataSet(); System.out.println("Write 1 record"); @@ -284,7 +275,7 @@ public void runExample(Cluster cluster, Args args) throws Exception { System.out.println("Create index"); - session.createIndex(set, "ageidx", "age", IndexType.INTEGER, IndexCollectionType.DEFAULT); + session.createIndex(set, "ageidx", "age", IndexType.INTEGER, IndexCollectionType.DEFAULT).waitTillComplete(); System.out.println("Foreground secondary index query"); @@ -372,7 +363,7 @@ public void runExample(Cluster cluster, Args args) throws Exception { session.query(set).withHint(hint -> hint.queryDuration(QueryDuration.LONG)).recordsPerSecond(20).execute(); - session.backgroundTask().update(set).bin("age").add(1).recordsPerSecond(35).execute(); + session.backgroundTask().update(set).bin("age").add(1).recordsPerSecond(35).execute().waitTillComplete(); // Exp operations - read and write. System.out.println("Read and write operation example"); diff --git a/examples/src/main/java/com/aerospike/examples/CompleteYamlConfigExample.java b/examples/src/main/java/com/aerospike/examples/CompleteYamlConfigExample.java index ef8b2ea3..1858613e 100644 --- a/examples/src/main/java/com/aerospike/examples/CompleteYamlConfigExample.java +++ b/examples/src/main/java/com/aerospike/examples/CompleteYamlConfigExample.java @@ -43,12 +43,8 @@ public class CompleteYamlConfigExample extends Example { private static final String CONFIG_FILE = "src/main/resources/complete-config-example.yml"; - public CompleteYamlConfigExample(Console console) { - super(console); - } - @Override - public void runExample(Cluster cluster, Args args) throws Exception { + public void runExample() throws Exception { console.write("=== Complete YAML Configuration Example ===\n"); try (Closeable monitor = Behavior.startMonitoringWithResource(CONFIG_FILE)) { @@ -58,8 +54,8 @@ public void runExample(Cluster cluster, Args args) throws Exception { displayAllBehaviors(); demonstrateBehaviorInheritance(); - if (args.host != null) { - performClusterOperations(args); + if (host() != null) { + performClusterOperations(); } else { console.write("\n=== Skipping Cluster Operations ==="); console.write("No host specified. Add -h to perform actual operations.\n"); @@ -67,9 +63,7 @@ public void runExample(Cluster cluster, Args args) throws Exception { } catch (IOException e) { console.error("Error loading configuration: " + e.getMessage()); - e.printStackTrace(); - } catch (InterruptedException e) { - console.write("Interrupted"); + throw e; } } @@ -245,21 +239,21 @@ private void demonstrateBehaviorInheritance() { console.write(""); } - private void performClusterOperations(Args args) { + private void performClusterOperations() { console.write("=== CLUSTER OPERATIONS ===\n"); - ClusterDefinition clusterDef = new ClusterDefinition(args.host, args.port) + ClusterDefinition clusterDef = new ClusterDefinition(host(), port()) .appId("complete-yaml-example") .failIfNotConnected(true); - if (args.useServicesAlternate) { + if (useServicesAlternate()) { clusterDef.usingServicesAlternate(); } try (Cluster cluster = clusterDef.connect()) { - console.write("Connected to cluster at " + args.host + ":" + args.port + "\n"); + console.write("Connected to cluster at " + host() + ":" + port() + "\n"); - DataSet dataSet = DataSet.of(args.namespace, "complete-yaml-demo"); + DataSet dataSet = dataSet("complete-yaml-demo"); testWithBehavior(cluster, "high-performance", dataSet); testWithBehavior(cluster, "high-reliability", dataSet); @@ -270,7 +264,7 @@ private void performClusterOperations(Args args) { } catch (Throwable t) { console.error("Cluster operation failed: " + Util.getErrorMessage(t)); - t.printStackTrace(); + throw new RuntimeException(t); } } @@ -308,6 +302,7 @@ private void testWithBehavior(Cluster cluster, String behaviorName, DataSet data } catch (Exception e) { console.error(" Error: " + e.getMessage()); + throw new RuntimeException(e); } console.write(""); diff --git a/examples/src/main/java/com/aerospike/examples/Example.java b/examples/src/main/java/com/aerospike/examples/Example.java index c4942b1e..dc9e8004 100644 --- a/examples/src/main/java/com/aerospike/examples/Example.java +++ b/examples/src/main/java/com/aerospike/examples/Example.java @@ -16,134 +16,69 @@ */ package com.aerospike.examples; -import java.io.File; -import java.lang.reflect.Constructor; -import java.util.List; - import com.aerospike.client.sdk.Cluster; -import com.aerospike.client.sdk.ClusterDefinition; -import com.aerospike.client.sdk.Log; +import com.aerospike.client.sdk.DataSet; /** * Abstract base class for all examples. * - *

Concrete examples should extend this class and implement the {@link #runExample(Args)} method. - * The base class handles example lifecycle (begin/end logging) and provides a console for output. + *

Concrete examples should extend this class and implement the {@link #runExample()} method. + * The base class provides access to runner-managed configuration. Lifecycle logging and the + * setup/verify/cleanup fixture flow are owned by {@link ExampleRunner}. */ public abstract class Example { - private static final String PACKAGE_NAME = "com.aerospike.examples."; - protected Console console; + private ExampleContext context; - public Example(Console console) { - this.console = console; + void initialize(ExampleContext context) { + this.context = context; + this.console = context.console(); } /** - * Run one or more examples. + * Initialize this example with the runner-managed context and run its body. * - * @param console the console for output - * @param args configuration parameters - * @param examples list of example names to run - * @throws Exception if an example fails + * @param context runner-managed example context + * @throws Exception if the example fails */ - public static void runExamples(Console console, Args args, List examples) throws Exception { - ClusterDefinition def = new ClusterDefinition(args.host, args.port) - .withLogLevel(Log.Level.DEBUG) - .clusterName(args.clusterName) - .withSystemSettings(builder -> builder - .circuitBreaker(ops -> ops.maximumErrorsInErrorWindow(200)) - .connections(conn -> conn - .minimumConnectionsPerNode(200) - .maximumConnectionsPerNode(200) - ) - ); - - if (args.tlsName != null) { - String certHome = System.getenv("CERT_HOME"); - - if (certHome == null) { - certHome = ""; - } - - String caFile = resolvePath(certHome, args.caFile); - String clientCertFile = resolvePath(certHome, args.clientCertFile); - String clientKeyFile = resolvePath(certHome, args.clientKeyFile); - - def.withTlsConfig(tls -> tls - .tlsName(args.tlsName) - .caFile(caFile) - .clientCertFile(clientCertFile) - .clientKeyFile(clientKeyFile) - ); - } - - Cluster cluster = def.connect(); + public void run(ExampleContext context) throws Exception { + initialize(context); + runExample(); + } - try { - for (String exampleName : examples) { - runExample(exampleName, cluster, args, console); - } - } - finally { - cluster.close(); - } + protected Cluster cluster() { + return context.cluster(); } - private static String resolvePath(String dir, String path) { - File file = new File(path); + protected String namespace() { + return context.args().namespace; + } - if (file.isAbsolute()) { - return path; - } + protected String host() { + return context.args().host; + } - file = new File(dir, path); - return file.getAbsolutePath(); + protected int port() { + return context.args().port; } - /** - * Run a single example by name using reflection. - * - * @param exampleName the simple name of the example class - * @param cluster cluster - * @param args configuration parameters - * @param console the console for output - * @throws Exception if the example fails or cannot be found - */ - public static void runExample( - String exampleName, Cluster cluster, Args args, Console console - ) throws Exception { - String fullName = PACKAGE_NAME + exampleName; - Class cls = Class.forName(fullName); + protected boolean useServicesAlternate() { + return context.args().useServicesAlternate; + } - if (Example.class.isAssignableFrom(cls)) { - Constructor ctor = cls.getDeclaredConstructor(Console.class); - Example example = (Example) ctor.newInstance(console); - example.run(cluster, args); - } else { - console.error("Invalid example: " + exampleName); - } + protected DataSet dataSet() { + return context.dataSet(); } - /** - * Run this example with lifecycle logging. - * - * @param cluster cluster - * @param args configuration parameters - * @throws Exception if the example fails - */ - public void run(Cluster cluster, Args args) throws Exception { - console.info(this.getClass().getSimpleName() + " Begin"); - runExample(cluster, args); - console.info(this.getClass().getSimpleName() + " End"); + protected DataSet dataSet(String set) { + return context.dataSet(set); } /** * Run the example logic. Subclasses must implement this method. * - * @param params configuration parameters * @throws Exception if the example fails */ - public abstract void runExample(Cluster cluster, Args args) throws Exception; + public abstract void runExample() throws Exception; } diff --git a/examples/src/main/java/com/aerospike/examples/ExampleContext.java b/examples/src/main/java/com/aerospike/examples/ExampleContext.java new file mode 100644 index 00000000..023d417e --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleContext.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +import com.aerospike.client.sdk.Cluster; +import com.aerospike.client.sdk.DataSet; +import com.aerospike.client.sdk.Session; +import com.aerospike.client.sdk.policy.Behavior; + +/** + * Shared state for an example run. + */ +public class ExampleContext { + private final Cluster cluster; + private final Args args; + private final Console console; + + public ExampleContext(Cluster cluster, Args args, Console console) { + this.cluster = cluster; + this.args = args; + this.console = console; + } + + public Cluster cluster() { + return cluster; + } + + public Args args() { + return args; + } + + public Console console() { + return console; + } + + public Session session() { + return cluster.createSession(Behavior.DEFAULT); + } + + public DataSet dataSet() { + return DataSet.of(args.namespace, args.set); + } + + public DataSet dataSet(String set) { + return DataSet.of(args.namespace, set); + } +} diff --git a/examples/src/main/java/com/aerospike/examples/ExampleDefinition.java b/examples/src/main/java/com/aerospike/examples/ExampleDefinition.java new file mode 100644 index 00000000..f8c06727 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleDefinition.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +import java.util.Set; + +/** + * Registry metadata for one runnable example. + */ +public record ExampleDefinition( + String name, + Class exampleClass, + ExampleFixture fixture, + Set tags +) { + public ExampleDefinition { + fixture = fixture == null ? ExampleFixture.NONE : fixture; + tags = tags == null ? Set.of() : Set.copyOf(tags); + } + + public boolean hasTag(String tag) { + return tags.contains(tag); + } +} diff --git a/examples/src/main/java/com/aerospike/examples/ExampleFixture.java b/examples/src/main/java/com/aerospike/examples/ExampleFixture.java new file mode 100644 index 00000000..95ba506e --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleFixture.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +/** + * Hooks that keep setup, verification, and cleanup outside doc-facing examples. + */ +public interface ExampleFixture { + ExampleFixture NONE = new ExampleFixture() { + }; + + default void setup(ExampleContext context) throws Exception { + } + + default void verify(ExampleContext context) throws Exception { + } + + default void cleanup(ExampleContext context) throws Exception { + } +} diff --git a/examples/src/main/java/com/aerospike/examples/ExampleRegistry.java b/examples/src/main/java/com/aerospike/examples/ExampleRegistry.java new file mode 100644 index 00000000..5e40ddaa --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleRegistry.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import com.aerospike.examples.ecommerce.EcommerceExample; +import com.aerospike.examples.fixtures.ExampleFixtures; + +public final class ExampleRegistry { + private static final Map EXAMPLES = createExamples(); + + private ExampleRegistry() { + } + + public static Collection all() { + return EXAMPLES.values(); + } + + public static Optional find(String name) { + return Optional.ofNullable(EXAMPLES.get(name)); + } + + public static String[] names() { + return EXAMPLES.keySet().toArray(String[]::new); + } + + private static Map createExamples() { + Map examples = new LinkedHashMap<>(); + + register(examples, "CommonExample", CommonExample.class, ExampleFixtures.commonExample(), "smoke", "records"); + register(examples, "BatchExample", BatchExample.class, ExampleFixtures.batchExample(), "smoke", "records"); + register(examples, "BehaviorHierarchicalExample", BehaviorHierarchicalExample.class, ExampleFixture.NONE, "config"); + register(examples, "BehaviorYamlExample", BehaviorYamlExample.class, ExampleFixture.NONE, "config"); + register( + examples, + "CompleteYamlConfigExample", + CompleteYamlConfigExample.class, + ExampleFixtures.completeYamlConfigExample(), + "config", + "records"); + register(examples, "YamlConfigExample", YamlConfigExample.class, ExampleFixture.NONE, "config"); + register( + examples, + "YamlConfigConnectionExample", + YamlConfigConnectionExample.class, + ExampleFixtures.yamlConfigConnectionExample(), + "smoke", + "config", + "records"); + register(examples, "StudentScoresExample", StudentScoresExample.class, ExampleFixtures.studentScoresExample(), "smoke", "records"); + register( + examples, + "MapRemoveByKeyRangeTest", + MapRemoveByKeyRangeTest.class, + ExampleFixtures.mapRemoveByKeyRangeTest(), + "smoke", + "records", + "ael"); + register( + examples, + "TransactionProcessingExample", + TransactionProcessingExample.class, + ExampleFixtures.transactionProcessingExample(), + "smoke", + "records"); + register(examples, "AelTestSpecRunner", AelTestSpecRunner.class, ExampleFixtures.aelTestSpecRunner(), "ael", "records"); + register( + examples, + "OperationDifferences", + OperationDifferences.class, + ExampleFixtures.operationDifferences(), + "diagnostic", + "ael", + "records"); + register(examples, "QueryExamples", QueryExamples.class, ExampleFixtures.queryExamples(), "extended", "records"); + register(examples, "EcommerceExample", EcommerceExample.class, ExampleFixtures.ecommerceExample(), "extended", "records"); + register(examples, "RosterExample", RosterExample.class, ExampleFixture.NONE, "server-specific"); + + return Collections.unmodifiableMap(examples); + } + + private static void register( + Map examples, + String name, + Class exampleClass, + ExampleFixture fixture, + String... tags + ) { + examples.put(name, new ExampleDefinition(name, exampleClass, fixture, Set.copyOf(List.of(tags)))); + } +} diff --git a/examples/src/main/java/com/aerospike/examples/ExampleResult.java b/examples/src/main/java/com/aerospike/examples/ExampleResult.java new file mode 100644 index 00000000..9c054e21 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleResult.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +public record ExampleResult( + String name, + ExampleStatus status, + long durationMillis, + String message, + Throwable error +) { + public static ExampleResult passed(String name, long durationMillis) { + return new ExampleResult(name, ExampleStatus.PASSED, durationMillis, null, null); + } + + public static ExampleResult skipped(String name, long durationMillis, String message) { + return new ExampleResult(name, ExampleStatus.SKIPPED, durationMillis, message, null); + } + + public static ExampleResult failed(String name, long durationMillis, Throwable error) { + return new ExampleResult(name, ExampleStatus.FAILED, durationMillis, error.getMessage(), error); + } + + public boolean failed() { + return status == ExampleStatus.FAILED; + } +} diff --git a/examples/src/main/java/com/aerospike/examples/ExampleRunResult.java b/examples/src/main/java/com/aerospike/examples/ExampleRunResult.java new file mode 100644 index 00000000..df0e84ef --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleRunResult.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +import java.util.List; + +public record ExampleRunResult(List results) { + public ExampleRunResult { + results = List.copyOf(results); + } + + public boolean hasFailures() { + return results.stream().anyMatch(ExampleResult::failed); + } + + public int exitCode() { + return hasFailures() ? 1 : 0; + } + + public long passedCount() { + return count(ExampleStatus.PASSED); + } + + public long failedCount() { + return count(ExampleStatus.FAILED); + } + + public long skippedCount() { + return count(ExampleStatus.SKIPPED); + } + + private long count(ExampleStatus status) { + return results.stream().filter(result -> result.status() == status).count(); + } +} diff --git a/examples/src/main/java/com/aerospike/examples/ExampleRunner.java b/examples/src/main/java/com/aerospike/examples/ExampleRunner.java new file mode 100644 index 00000000..7b538830 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleRunner.java @@ -0,0 +1,196 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import com.aerospike.client.sdk.Cluster; +import com.aerospike.client.sdk.ClusterDefinition; +import com.aerospike.client.sdk.Log; + +public class ExampleRunner { + private final Console console; + private final Args args; + + public ExampleRunner(Console console, Args args) { + this.console = console; + this.args = args; + } + + public ExampleRunResult run(List exampleNames) throws Exception { + List definitions = resolveExamples(exampleNames); + List results = new ArrayList<>(); + + try (Cluster cluster = createCluster()) { + ExampleContext context = new ExampleContext(cluster, args, console); + + for (ExampleDefinition definition : definitions) { + ExampleResult result = runOne(definition, context); + results.add(result); + + if (args.failFast && result.failed()) { + break; + } + } + } + + ExampleRunResult runResult = new ExampleRunResult(results); + logSummary(runResult); + writeReport(runResult); + return runResult; + } + + private List resolveExamples(List exampleNames) { + List definitions = new ArrayList<>(); + + for (String name : exampleNames) { + ExampleDefinition definition = ExampleRegistry.find(name) + .orElseThrow(() -> new IllegalArgumentException("Unknown example: " + name)); + + if (matchesTags(definition)) { + definitions.add(definition); + } + else { + console.info("Skipping %s because it does not match the tag filters", definition.name()); + } + } + + return definitions; + } + + private boolean matchesTags(ExampleDefinition definition) { + if (!args.includeTags.isEmpty() && args.includeTags.stream().noneMatch(definition::hasTag)) { + return false; + } + + return args.excludeTags.isEmpty() || args.excludeTags.stream().noneMatch(definition::hasTag); + } + + private ExampleResult runOne(ExampleDefinition definition, ExampleContext context) { + long start = System.nanoTime(); + ExampleResult result; + console.info(definition.name() + " Begin"); + + try { + definition.fixture().setup(context); + instantiate(definition).run(context); + definition.fixture().verify(context); + console.info(definition.name() + " Passed"); + result = ExampleResult.passed(definition.name(), elapsedMillis(start)); + } + catch (ExampleSkipException e) { + console.warn("%s Skipped: %s", definition.name(), e.getMessage()); + result = ExampleResult.skipped(definition.name(), elapsedMillis(start), e.getMessage()); + } + catch (Throwable t) { + console.error("%s Failed: %s", definition.name(), t.getMessage()); + result = ExampleResult.failed(definition.name(), elapsedMillis(start), t); + } + finally { + console.info(definition.name() + " End"); + } + + try { + definition.fixture().cleanup(context); + } + catch (Throwable t) { + console.error("%s Cleanup failed: %s", definition.name(), t.getMessage()); + + // Only a passing example is downgraded by a cleanup failure. A skip or an + // existing failure keeps its original, more meaningful result. + if (result.status() == ExampleStatus.PASSED) { + result = ExampleResult.failed(definition.name(), elapsedMillis(start), t); + } + } + + return result; + } + + private Example instantiate(ExampleDefinition definition) throws Exception { + return definition.exampleClass().getDeclaredConstructor().newInstance(); + } + + private Cluster createCluster() { + ClusterDefinition def = new ClusterDefinition(args.host, args.port) + .withLogLevel(Log.Level.DEBUG) + .clusterName(args.clusterName) + .withSystemSettings(builder -> builder + .circuitBreaker(ops -> ops.maximumErrorsInErrorWindow(200)) + .connections(conn -> conn + .minimumConnectionsPerNode(200) + .maximumConnectionsPerNode(200) + ) + ); + + if (args.tlsName != null) { + String certHome = System.getenv("CERT_HOME"); + + if (certHome == null) { + certHome = ""; + } + + String caFile = resolvePath(certHome, args.caFile); + String clientCertFile = resolvePath(certHome, args.clientCertFile); + String clientKeyFile = resolvePath(certHome, args.clientKeyFile); + + def.withTlsConfig(tls -> tls + .tlsName(args.tlsName) + .caFile(caFile) + .clientCertFile(clientCertFile) + .clientKeyFile(clientKeyFile) + ); + } + + return def.connect(); + } + + private static String resolvePath(String dir, String path) { + File file = new File(path); + + if (file.isAbsolute()) { + return path; + } + + file = new File(dir, path); + return file.getAbsolutePath(); + } + + private void logSummary(ExampleRunResult runResult) { + console.info( + "Examples complete: %d passed, %d failed, %d skipped", + runResult.passedCount(), + runResult.failedCount(), + runResult.skippedCount()); + } + + private void writeReport(ExampleRunResult runResult) throws Exception { + if (args.reportPath == null || args.reportPath.isBlank()) { + return; + } + + Path reportPath = Path.of(args.reportPath); + JUnitXmlReportWriter.write(reportPath, runResult); + console.info("Wrote example report to " + reportPath); + } + + private static long elapsedMillis(long startNanos) { + return (System.nanoTime() - startNanos) / 1_000_000L; + } +} diff --git a/examples/src/main/java/com/aerospike/examples/ExampleSkipException.java b/examples/src/main/java/com/aerospike/examples/ExampleSkipException.java new file mode 100644 index 00000000..262943ee --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleSkipException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +/** + * Signals that an example is not applicable to the current server/configuration. + */ +public class ExampleSkipException extends Exception { + public ExampleSkipException(String message) { + super(message); + } +} diff --git a/examples/src/main/java/com/aerospike/examples/ExampleStatus.java b/examples/src/main/java/com/aerospike/examples/ExampleStatus.java new file mode 100644 index 00000000..fa82772d --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/ExampleStatus.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +public enum ExampleStatus { + PASSED, + FAILED, + SKIPPED +} diff --git a/examples/src/main/java/com/aerospike/examples/JUnitXmlReportWriter.java b/examples/src/main/java/com/aerospike/examples/JUnitXmlReportWriter.java new file mode 100644 index 00000000..686da237 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/JUnitXmlReportWriter.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public final class JUnitXmlReportWriter { + private JUnitXmlReportWriter() { + } + + public static void write(Path reportPath, ExampleRunResult runResult) throws IOException { + Path parent = reportPath.getParent(); + + if (parent != null) { + Files.createDirectories(parent); + } + + List results = runResult.results(); + long elapsedMillis = results.stream().mapToLong(ExampleResult::durationMillis).sum(); + + StringBuilder xml = new StringBuilder(); + xml.append("\n"); + xml.append("\n"); + + for (ExampleResult result : results) { + xml.append(" "); + + if (result.status() == ExampleStatus.FAILED) { + xml.append("\n "); + xml.append(escape(stackTrace(result.error()))); + xml.append("\n "); + } + else if (result.status() == ExampleStatus.SKIPPED) { + xml.append("\n \n "); + } + + xml.append("\n"); + } + + xml.append("\n"); + Files.writeString(reportPath, xml.toString(), StandardCharsets.UTF_8); + } + + private static String seconds(long millis) { + return String.format("%.3f", millis / 1000.0); + } + + private static String stackTrace(Throwable error) { + if (error == null) { + return ""; + } + + StringWriter sw = new StringWriter(); + error.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + private static String nullToEmpty(String value) { + return value == null ? "" : value; + } + + private static String escape(String value) { + return value + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} diff --git a/examples/src/main/java/com/aerospike/examples/Main.java b/examples/src/main/java/com/aerospike/examples/Main.java index 63db4494..63d67ca4 100644 --- a/examples/src/main/java/com/aerospike/examples/Main.java +++ b/examples/src/main/java/com/aerospike/examples/Main.java @@ -19,6 +19,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.apache.commons.cli.CommandLine; @@ -37,33 +38,20 @@ * ./run_examples -u * } * - *

Available Examples

- * + *

The set of available examples is defined by {@link ExampleRegistry}; run with + * {@code -u} to print the current list. */ public class Main { - - private static final String[] EXAMPLE_NAMES = new String[] { - "CommonExample", - "BatchExample", - "BehaviorHierarchicalExample", - "BehaviorYamlExample", - "CompleteYamlConfigExample", - "YamlConfigExample", - "YamlConfigConnectionExample" - }; - public static String[] getAllExampleNames() { - return EXAMPLE_NAMES; + return ExampleRegistry.names(); } /** * Main entry point. */ public static void main(String[] args) { + int exitCode = 1; + try { Options options = new Options(); Args.addCommonOptions(options); @@ -73,6 +61,7 @@ public static void main(String[] args) { if (args.length == 0 || cl.hasOption("u")) { logUsage(options); + exitCode = 0; return; } @@ -81,23 +70,21 @@ public static void main(String[] args) { if (exampleNames.length == 0) { logUsage(options); + exitCode = 0; return; } - // Check for "all" - for (String exampleName : exampleNames) { - if (exampleName.equalsIgnoreCase("all")) { - exampleNames = EXAMPLE_NAMES; - break; - } - } - Console console = new Console(); - runExamples(console, arguments, exampleNames); + exitCode = runExamples(console, arguments, exampleNames); } catch (Exception ex) { ex.printStackTrace(); } + finally { + if (exitCode != 0) { + System.exit(exitCode); + } + } } /** @@ -112,7 +99,7 @@ private static void logUsage(Options options) { System.out.println(sw.toString()); System.out.println("examples:"); - for (String name : EXAMPLE_NAMES) { + for (String name : ExampleRegistry.names()) { System.out.println(" " + name); } System.out.println(); @@ -122,11 +109,19 @@ private static void logUsage(Options options) { /** * Run one or more examples. */ - public static void runExamples(Console console, Args args, String[] examples) throws Exception { - List exampleList = new ArrayList<>(); + public static int runExamples(Console console, Args args, String[] examples) throws Exception { + List exampleList = expandExamples(examples); + ExampleRunResult result = new ExampleRunner(console, args).run(exampleList); + return result.exitCode(); + } + + private static List expandExamples(String[] examples) { for (String example : examples) { - exampleList.add(example); + if (example.equalsIgnoreCase("all")) { + return Arrays.asList(ExampleRegistry.names()); + } } - Example.runExamples(console, args, exampleList); + + return new ArrayList<>(Arrays.asList(examples)); } } diff --git a/examples/src/main/java/com/aerospike/examples/MapRemoveByKeyRangeTest.java b/examples/src/main/java/com/aerospike/examples/MapRemoveByKeyRangeTest.java index 5db7eb2e..00e1537c 100644 --- a/examples/src/main/java/com/aerospike/examples/MapRemoveByKeyRangeTest.java +++ b/examples/src/main/java/com/aerospike/examples/MapRemoveByKeyRangeTest.java @@ -18,10 +18,7 @@ import java.util.TreeMap; -import com.aerospike.client.sdk.Cluster; -import com.aerospike.client.sdk.ClusterDefinition; import com.aerospike.client.sdk.DataSet; -import com.aerospike.client.sdk.Record; import com.aerospike.client.sdk.RecordStream; import com.aerospike.client.sdk.Session; import com.aerospike.client.sdk.cdt.MapReturnType; @@ -42,167 +39,152 @@ *

This test creates a known map and calls removeByKeyRange as a read expression * to see what is actually returned. */ -public class MapRemoveByKeyRangeTest { - - public static void main(String[] args) throws Exception { - try (Cluster cluster = new ClusterDefinition("localhost", 3100).connect()) { - Session session = cluster.createSession(Behavior.DEFAULT); - DataSet set = DataSet.of("test", "map_remove_test"); - - session.truncate(set); - - // Seed: key-ordered map {a=1, b=2, c=3, d=4, e=5} - TreeMap sourceMap = new TreeMap<>(); - sourceMap.put("a", 1L); - sourceMap.put("b", 2L); - sourceMap.put("c", 3L); - sourceMap.put("d", 4L); - sourceMap.put("e", 5L); - - session.upsert(set.id(1)) - .bin("m").setTo(sourceMap) +public class MapRemoveByKeyRangeTest extends Example { + + @Override + public void runExample() throws Exception { + Session session = cluster().createSession(Behavior.DEFAULT); + DataSet set = dataSet("map_remove_test"); + + // Seed: key-ordered map {a=1, b=2, c=3, d=4, e=5} + TreeMap sourceMap = new TreeMap<>(); + sourceMap.put("a", 1L); + sourceMap.put("b", 2L); + sourceMap.put("c", 3L); + sourceMap.put("d", 4L); + sourceMap.put("e", 5L); + + session.upsert(set.id(1)) + .bin("m").setTo(sourceMap) + .execute(); + + System.out.println("Source map: " + sourceMap); + System.out.println(); + + // removeByKeyRange("b", "e") removes keys b, c, d (b inclusive, e exclusive) + // That is 3 items removed, leaving {a=1, e=5} + + // --- Test 1: returnType = NONE --- + System.out.println("=== Test 1: MapExp.removeByKeyRange(NONE, \"b\", \"e\") ==="); + System.out.println("Javadoc says NONE is valid. Class doc says modify expressions return the bin's value."); + System.out.println("Expected: the modified map {a=1, e=5}"); + try { + Exp removeNone = MapExp.removeByKeyRange( + MapReturnType.NONE, + Exp.val("b"), Exp.val("e"), + Exp.mapBin("m")); + + RecordStream rs = session.query(set.id(1)) + .bin("result").selectFrom(removeNone) .execute(); - - System.out.println("Source map: " + sourceMap); - System.out.println(); - - // removeByKeyRange("b", "e") removes keys b, c, d (b inclusive, e exclusive) - // That is 3 items removed, leaving {a=1, e=5} - - // --- Test 1: returnType = NONE --- - System.out.println("=== Test 1: MapExp.removeByKeyRange(NONE, \"b\", \"e\") ==="); - System.out.println("Javadoc says NONE is valid. Class doc says modify expressions return the bin's value."); - System.out.println("Expected: the modified map {a=1, e=5}"); - try { - Exp removeNone = MapExp.removeByKeyRange( - MapReturnType.NONE, - Exp.val("b"), Exp.val("e"), - Exp.mapBin("m")); - - RecordStream rs = session.query(set.id(1)) - .bin("result").selectFrom(removeNone) - .execute(); - Record rec = rs.getFirst().orElseThrow().recordOrThrow(); - Object result = rec.bins.get("result"); - System.out.println("Actual: " + result); - System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); - } catch (Exception e) { - System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - System.out.println(); - - // --- Test 2: returnType = INVERTED --- - System.out.println("=== Test 2: MapExp.removeByKeyRange(INVERTED, \"b\", \"e\") ==="); - System.out.println("Javadoc says INVERTED is valid. Inverted means remove everything OUTSIDE the range."); - System.out.println("Expected: remove {a=1, e=5} (outside range b..e), leaving {b=2, c=3, d=4}"); - try { - Exp removeInverted = MapExp.removeByKeyRange( - MapReturnType.INVERTED, - Exp.val("b"), Exp.val("e"), - Exp.mapBin("m")); - - RecordStream rs = session.query(set.id(1)) - .bin("result").selectFrom(removeInverted) - .execute(); - Record rec = rs.getFirst().orElseThrow().recordOrThrow(); - Object result = rec.bins.get("result"); - System.out.println("Actual: " + result); - System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); - } catch (Exception e) { - System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - System.out.println(); - - // --- Test 3: returnType = COUNT (Javadoc says this is NOT valid) --- - System.out.println("=== Test 3: MapExp.removeByKeyRange(COUNT, \"b\", \"e\") ==="); - System.out.println("Javadoc says only NONE and INVERTED are valid."); - System.out.println("If the doc is wrong, this might return the count of removed items (3)."); - try { - Exp removeCount = MapExp.removeByKeyRange( - MapReturnType.COUNT, - Exp.val("b"), Exp.val("e"), - Exp.mapBin("m")); - - RecordStream rs = session.query(set.id(1)) - .bin("result").selectFrom(removeCount) - .execute(); - Record rec = rs.getFirst().orElseThrow().recordOrThrow(); - Object result = rec.bins.get("result"); - System.out.println("Actual: " + result); - System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); - } catch (Exception e) { - System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - System.out.println(); - - // --- Test 4: returnType = KEY (Javadoc says this is NOT valid) --- - System.out.println("=== Test 4: MapExp.removeByKeyRange(KEY, \"b\", \"e\") ==="); - System.out.println("If the doc is wrong, this might return the removed keys [b, c, d]."); - try { - Exp removeKey = MapExp.removeByKeyRange( - MapReturnType.KEY, - Exp.val("b"), Exp.val("e"), - Exp.mapBin("m")); - - RecordStream rs = session.query(set.id(1)) - .bin("result").selectFrom(removeKey) - .execute(); - Record rec = rs.getFirst().orElseThrow().recordOrThrow(); - Object result = rec.bins.get("result"); - System.out.println("Actual: " + result); - System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); - } catch (Exception e) { - System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - System.out.println(); - - // --- Test 5: returnType = VALUE (Javadoc says this is NOT valid) --- - System.out.println("=== Test 5: MapExp.removeByKeyRange(VALUE, \"b\", \"e\") ==="); - System.out.println("If the doc is wrong, this might return the removed values [2, 3, 4]."); - try { - Exp removeValue = MapExp.removeByKeyRange( - MapReturnType.VALUE, - Exp.val("b"), Exp.val("e"), - Exp.mapBin("m")); - - RecordStream rs = session.query(set.id(1)) - .bin("result").selectFrom(removeValue) - .execute(); - Record rec = rs.getFirst().orElseThrow().recordOrThrow(); - Object result = rec.bins.get("result"); - System.out.println("Actual: " + result); - System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); - } catch (Exception e) { - System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - System.out.println(); - - // --- Test 6: returnType = KEY_VALUE (Javadoc says this is NOT valid) --- - System.out.println("=== Test 6: MapExp.removeByKeyRange(KEY_VALUE, \"b\", \"e\") ==="); - System.out.println("If the doc is wrong, this might return the removed entries {b=2, c=3, d=4}."); - try { - Exp removeKV = MapExp.removeByKeyRange( - MapReturnType.KEY_VALUE, - Exp.val("b"), Exp.val("e"), - Exp.mapBin("m")); - - RecordStream rs = session.query(set.id(1)) - .bin("result").selectFrom(removeKV) - .execute(); - Record rec = rs.getFirst().orElseThrow().recordOrThrow(); - Object result = rec.bins.get("result"); - System.out.println("Actual: " + result); - System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); - } catch (Exception e) { - System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - System.out.println(); - - // Verify original record is unchanged (read expressions don't mutate) - System.out.println("=== Verify original map is unchanged ==="); - RecordStream rs = session.query(set.id(1)).execute(); - Record rec = rs.getFirst().orElseThrow().recordOrThrow(); - System.out.println("Original map after all tests: " + rec.bins.get("m")); + Object result = rs.getFirst().orElseThrow().recordOrThrow().bins.get("result"); + System.out.println("Actual: " + result); + System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); + } catch (Exception e) { + System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + System.out.println(); + + // --- Test 2: returnType = INVERTED --- + System.out.println("=== Test 2: MapExp.removeByKeyRange(INVERTED, \"b\", \"e\") ==="); + System.out.println("Javadoc says INVERTED is valid. Inverted means remove everything OUTSIDE the range."); + System.out.println("Expected: remove {a=1, e=5} (outside range b..e), leaving {b=2, c=3, d=4}"); + try { + Exp removeInverted = MapExp.removeByKeyRange( + MapReturnType.INVERTED, + Exp.val("b"), Exp.val("e"), + Exp.mapBin("m")); + + RecordStream rs = session.query(set.id(1)) + .bin("result").selectFrom(removeInverted) + .execute(); + Object result = rs.getFirst().orElseThrow().recordOrThrow().bins.get("result"); + System.out.println("Actual: " + result); + System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); + } catch (Exception e) { + System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + System.out.println(); + + // --- Test 3: returnType = COUNT (Javadoc says this is NOT valid) --- + System.out.println("=== Test 3: MapExp.removeByKeyRange(COUNT, \"b\", \"e\") ==="); + System.out.println("Javadoc says only NONE and INVERTED are valid."); + System.out.println("If the doc is wrong, this might return the count of removed items (3)."); + try { + Exp removeCount = MapExp.removeByKeyRange( + MapReturnType.COUNT, + Exp.val("b"), Exp.val("e"), + Exp.mapBin("m")); + + RecordStream rs = session.query(set.id(1)) + .bin("result").selectFrom(removeCount) + .execute(); + Object result = rs.getFirst().orElseThrow().recordOrThrow().bins.get("result"); + System.out.println("Actual: " + result); + System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); + } catch (Exception e) { + System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + System.out.println(); + + // --- Test 4: returnType = KEY (Javadoc says this is NOT valid) --- + System.out.println("=== Test 4: MapExp.removeByKeyRange(KEY, \"b\", \"e\") ==="); + System.out.println("If the doc is wrong, this might return the removed keys [b, c, d]."); + try { + Exp removeKey = MapExp.removeByKeyRange( + MapReturnType.KEY, + Exp.val("b"), Exp.val("e"), + Exp.mapBin("m")); + + RecordStream rs = session.query(set.id(1)) + .bin("result").selectFrom(removeKey) + .execute(); + Object result = rs.getFirst().orElseThrow().recordOrThrow().bins.get("result"); + System.out.println("Actual: " + result); + System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); + } catch (Exception e) { + System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + System.out.println(); + + // --- Test 5: returnType = VALUE (Javadoc says this is NOT valid) --- + System.out.println("=== Test 5: MapExp.removeByKeyRange(VALUE, \"b\", \"e\") ==="); + System.out.println("If the doc is wrong, this might return the removed values [2, 3, 4]."); + try { + Exp removeValue = MapExp.removeByKeyRange( + MapReturnType.VALUE, + Exp.val("b"), Exp.val("e"), + Exp.mapBin("m")); + + RecordStream rs = session.query(set.id(1)) + .bin("result").selectFrom(removeValue) + .execute(); + Object result = rs.getFirst().orElseThrow().recordOrThrow().bins.get("result"); + System.out.println("Actual: " + result); + System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); + } catch (Exception e) { + System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + System.out.println(); + + // --- Test 6: returnType = KEY_VALUE (Javadoc says this is NOT valid) --- + System.out.println("=== Test 6: MapExp.removeByKeyRange(KEY_VALUE, \"b\", \"e\") ==="); + System.out.println("If the doc is wrong, this might return the removed entries {b=2, c=3, d=4}."); + try { + Exp removeKV = MapExp.removeByKeyRange( + MapReturnType.KEY_VALUE, + Exp.val("b"), Exp.val("e"), + Exp.mapBin("m")); + + RecordStream rs = session.query(set.id(1)) + .bin("result").selectFrom(removeKV) + .execute(); + Object result = rs.getFirst().orElseThrow().recordOrThrow().bins.get("result"); + System.out.println("Actual: " + result); + System.out.println("Type: " + (result == null ? "null" : result.getClass().getName())); + } catch (Exception e) { + System.out.println("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage()); } + System.out.println(); } } diff --git a/examples/src/main/java/com/aerospike/examples/OperationDifferences.java b/examples/src/main/java/com/aerospike/examples/OperationDifferences.java index 397efcb2..5214ae16 100644 --- a/examples/src/main/java/com/aerospike/examples/OperationDifferences.java +++ b/examples/src/main/java/com/aerospike/examples/OperationDifferences.java @@ -20,8 +20,6 @@ import java.util.List; import java.util.Map; -import com.aerospike.client.sdk.Cluster; -import com.aerospike.client.sdk.ClusterDefinition; import com.aerospike.client.sdk.DataSet; import com.aerospike.client.sdk.Record; import com.aerospike.client.sdk.RecordStream; @@ -40,38 +38,37 @@ *

  • 2e: Mutation path functions (sort, remove, etc.) silently ignored
  • * */ -public class OperationDifferences { +public class OperationDifferences extends Example { private static final String SEPARATOR = "=".repeat(70); private static final String PASS = "PASS"; private static final String FAIL = "** FAIL **"; - private static int totalTests = 0; - private static int failedTests = 0; + private int totalTests = 0; + private int failedTests = 0; - public static void main(String[] args) throws Exception { - try (Cluster cluster = new ClusterDefinition("localhost", 3100).connect()) { - Session session = cluster.createSession(Behavior.DEFAULT); - DataSet set = DataSet.of("test", "ael_diff_test"); + @Override + public void runExample() throws Exception { + Session session = cluster().createSession(Behavior.DEFAULT); + DataSet set = dataSet("ael_diff_test"); - session.truncate(set); - setupTestData(session, set); + // The runner's fixture truncates ael_diff_test before this example runs. + setupTestData(session, set); -// test2a_LetThenVsWithDo(session, set); - test2b_NameIdentifierTooPermissive(session, set); - test2c_RightShiftReversed(session, set); - test2d_ExistsSilentlyIgnored(session, set); - test2e_MutationOperationsIgnored(session, set); - testCasting_AsIntAsFloat(session, set); +// test2a_LetThenVsWithDo(session, set); + test2b_NameIdentifierTooPermissive(session, set); + test2c_RightShiftReversed(session, set); + test2d_ExistsSilentlyIgnored(session, set); + test2e_MutationOperationsIgnored(session, set); + testCasting_AsIntAsFloat(session, set); - System.out.println(SEPARATOR); - System.out.printf("SUMMARY: %d/%d tests show spec/implementation differences%n", - failedTests, totalTests); - System.out.println(SEPARATOR); - } + System.out.println(SEPARATOR); + System.out.printf("SUMMARY: %d/%d tests show spec/implementation differences%n", + failedTests, totalTests); + System.out.println(SEPARATOR); } - static void setupTestData(Session session, DataSet set) { + void setupTestData(Session session, DataSet set) { // Record 1: integer bin for shift operator tests session.upsert(set.id(1)) .bin("intBin").setTo(-8) @@ -123,7 +120,8 @@ static void setupTestData(Session session, DataSet set) { // ========================================================================= // 2a: Spec says `let...then`, implementation uses `let...then` // ========================================================================= - static void test2a_LetThenVsWithDo(Session session, DataSet set) { + @SuppressWarnings("unused") + void test2a_LetThenVsWithDo(Session session, DataSet set) { System.out.println(SEPARATOR); System.out.println("TEST 2a: Spec keyword 'let...then' vs implementation 'let...then'"); System.out.println(SEPARATOR); @@ -176,7 +174,7 @@ static void test2a_LetThenVsWithDo(Session session, DataSet set) { // 2b: NAME_IDENTIFIER allows digit-starting tokens, so $.m.1 uses // string key "1" instead of integer key 1 // ========================================================================= - static void test2b_NameIdentifierTooPermissive(Session session, DataSet set) { + void test2b_NameIdentifierTooPermissive(Session session, DataSet set) { System.out.println(SEPARATOR); System.out.println("TEST 2b: NAME_IDENTIFIER treats bare integers as string map keys"); System.out.println(SEPARATOR); @@ -232,7 +230,7 @@ static void test2b_NameIdentifierTooPermissive(Session session, DataSet set) { // 2c: >> operator performs logical right shift (zero-fill) instead of // arithmetic right shift (sign-preserving) as per spec and Java convention // ========================================================================= - static void test2c_RightShiftReversed(Session session, DataSet set) { + void test2c_RightShiftReversed(Session session, DataSet set) { System.out.println(SEPARATOR); System.out.println("TEST 2c: >> operator semantics reversed"); System.out.println(SEPARATOR); @@ -328,7 +326,7 @@ static void test2c_RightShiftReversed(Session session, DataSet set) { // implement visitPathFunctionExists, so it silently falls through // and the exists() call is dropped from the expression. // ========================================================================= - static void test2d_ExistsSilentlyIgnored(Session session, DataSet set) { + void test2d_ExistsSilentlyIgnored(Session session, DataSet set) { System.out.println(SEPARATOR); System.out.println("TEST 2d: exists() path function silently ignored"); System.out.println(SEPARATOR); @@ -419,7 +417,7 @@ static void test2d_ExistsSilentlyIgnored(Session session, DataSet set) { // increment) are defined in the grammar but have no visitor // implementation -- they are silently ignored or cause errors. // ========================================================================= - static void test2e_MutationOperationsIgnored(Session session, DataSet set) { + void test2e_MutationOperationsIgnored(Session session, DataSet set) { System.out.println(SEPARATOR); System.out.println("TEST 2e: Mutation path functions silently ignored"); System.out.println(SEPARATOR); @@ -540,7 +538,7 @@ static void test2e_MutationOperationsIgnored(Session session, DataSet set) { // arithmetic. Both operands must be the same type; the spec provides // asInt() and asFloat() path functions for explicit conversion. // ========================================================================= - static void testCasting_AsIntAsFloat(Session session, DataSet set) { + void testCasting_AsIntAsFloat(Session session, DataSet set) { System.out.println(SEPARATOR); System.out.println("TEST: asInt() / asFloat() type casting for arithmetic"); System.out.println(SEPARATOR); @@ -680,7 +678,7 @@ static void testCasting_AsIntAsFloat(Session session, DataSet set) { System.out.println(); } - private static void check(String testId, boolean passed, String description) { + private void check(String testId, boolean passed, String description) { totalTests++; String status; if (passed) { diff --git a/examples/src/main/java/com/aerospike/examples/QueryExamples.java b/examples/src/main/java/com/aerospike/examples/QueryExamples.java index 993a5140..fcc06088 100644 --- a/examples/src/main/java/com/aerospike/examples/QueryExamples.java +++ b/examples/src/main/java/com/aerospike/examples/QueryExamples.java @@ -28,21 +28,18 @@ import com.aerospike.client.sdk.AerospikeException; import com.aerospike.client.sdk.AerospikeException.BinOpInvalidException; import com.aerospike.client.sdk.Cluster; -import com.aerospike.client.sdk.ClusterDefinition; import com.aerospike.client.sdk.DataSet; import com.aerospike.client.sdk.DefaultRecordMappingFactory; import com.aerospike.client.sdk.ErrorStrategy; import com.aerospike.client.sdk.Key; import com.aerospike.client.sdk.NavigatableRecordStream; import com.aerospike.client.sdk.Record; -import com.aerospike.client.sdk.RecordMapper; import com.aerospike.client.sdk.RecordResult; import com.aerospike.client.sdk.RecordStream; import com.aerospike.client.sdk.ResultCode; import com.aerospike.client.sdk.Session; import com.aerospike.client.sdk.SpecialValue; import com.aerospike.client.sdk.TypeSafeDataSet; -import com.aerospike.client.sdk.Log.Level; import com.aerospike.client.sdk.ael.Ael; import com.aerospike.client.sdk.cdt.ListOrder; import com.aerospike.client.sdk.cdt.MapOrder; @@ -54,167 +51,23 @@ import com.aerospike.client.sdk.query.SortDir; import com.aerospike.client.sdk.query.SortProperties; import com.aerospike.client.sdk.task.ExecuteTask; -import com.aerospike.client.sdk.util.MapUtil; - -public class QueryExamples { - public static class Address { - private final String line1; - private final String city; - private final String state; - private final String country; - private final String zipCode; - - public Address(String line1, String city, String state, String country, String zipCode) { - super(); - this.line1 = line1; - this.city = city; - this.state = state; - this.country = country; - this.zipCode = zipCode; - } - - public String getLine1() { - return line1; - } - public String getCity() { - return city; - } - - public String getState() { - return state; - } - public String getCountry() { - return country; - } - public String getZipCode() { - return zipCode; - } - @Override - public String toString() { - return "Address [line1=" + line1 + ", city=" + city + ", state=" + state + ", country=" + country + ", zipCode=" - + zipCode + "]"; - } - } - - public static class Customer { - private long id; - private String name; - private int age; - private Date dob; - private Address address; - - public Customer() { - super(); - } - public Customer(long id, String name, int age, Date dob) { - this(id, name, age, dob, null); - } - public Customer(long id, String name, int age, Date dob, Address address) { - super(); - this.id = id; - this.name = name; - this.age = age; - this.dob = dob; - this.address = address; - } - - public long getId() { - return id; - } - public void setId(long id) { - this.id = id; - } - public String getName() { - return name; - } - public void setName(String name) { - this.name = name; - } - public int getAge() { - return age; - } - public void setAge(int age) { - this.age = age; - } - public Date getDob() { - return dob; - } - public void setDob(Date dob) { - this.dob = dob; - } - - public Address getAddress() { - return address; - } - public void setAddress(Address address) { - this.address = address; - } - - @Override - public String toString() { - return "Customer [id=" + id + ", name=" + name + ", age=" + age + ", dob=" + dob + ", address=" + address + "]"; - } - } - - public static class AddressMapper implements RecordMapper
    { - - @Override - public Address fromMap(Map map, Key recordKey, int generation) { - Address result = new Address( - MapUtil.asString(map, "line1"), - MapUtil.asString(map, "city"), - MapUtil.asString(map, "state"), - MapUtil.asString(map, "country"), - MapUtil.asString(map, "zip")); - return result; - } - - @Override - public Map toMap(Address addr) { - return MapUtil.buildMap() - .add("line1", addr.getLine1()) - .add("city", addr.getCity()) - .add("state", addr.getState()) - .add("country", addr.getCountry()) - .add("zip", addr.getZipCode()) - .done(); - } +import com.aerospike.examples.query.Address; +import com.aerospike.examples.query.AddressMapper; +import com.aerospike.examples.query.Customer; +import com.aerospike.examples.query.CustomerMapper; - @Override - public Object id(Address element) { - return null; - } - - } - - public static class CustomerMapper implements RecordMapper { - @Override - public Customer fromMap(Map map, Key recordKey, int generation) { - Customer result = new Customer(); - result.setId(recordKey.userKey.toLong()); - result.setAge(MapUtil.asInt(map, "age")); - result.setDob(MapUtil.asDateFromLong(map, "dob")); - result.setName(MapUtil.asString(map, "name")); - result.setAddress(MapUtil.asObjectFromMap(map, "address", new AddressMapper())); - return result; - } - - @Override - public Map toMap(Customer customer) { - return MapUtil.buildMap() - .add("id", customer.getId()) - .add("age", customer.getAge()) - .addAsLong("dob", customer.getDob()) - .add("name", customer.getName()) - .add("address", customer.getAddress(), new AddressMapper()) - .done(); - } +/** + * Broad tour of the query, batch, CDT, expression and object-mapping surface of the SDK. + * + *

    The example intentionally exercises a wide range of operations end-to-end. To keep the + * narrative readable, each logical area is factored into its own {@code demonstrate...} method + * that shares the same {@link Session} and {@link TypeSafeDataSet}. The data model + * ({@link Customer}, {@link Address} and their mappers) lives in the + * {@code com.aerospike.examples.query} package. + */ +@SuppressWarnings("unused") +public class QueryExamples extends Example { - @Override - public Object id(Customer customer) { - return customer.getId(); - } - } public static void print(RecordStream recordStream) { int count = 0; while (recordStream.hasNext()) { @@ -223,105 +76,120 @@ public static void print(RecordStream recordStream) { } } - @SuppressWarnings("unused") - public static void main(String[] args) { - try (Cluster cluster = new ClusterDefinition("localhost", 3100) - .usingServicesAlternate() - .withNativeCredentials("admin", "password123") - .preferringRacks(1) - .withLogLevel(Level.DEBUG) - .withSystemSettings(builder -> builder - .circuitBreaker(ops -> ops.maximumErrorsInErrorWindow(200)) - .connections(conn -> conn - .minimumConnectionsPerNode(200) - .maximumConnectionsPerNode(200) - ) - ) - .connect()) { - - CustomerMapper customerMapper = new CustomerMapper(); + @Override + public void runExample() throws Exception { + Cluster cluster = cluster(); - cluster.setRecordMappingFactory(new DefaultRecordMappingFactory(Map.of( - Customer.class, customerMapper, - Address.class, new AddressMapper() - ) - )); + CustomerMapper customerMapper = new CustomerMapper(); - Behavior newBehavior = Behavior.DEFAULT.deriveWithChanges("newBehavior", builder -> - builder.on(Selectors.all(), ops -> ops - .waitForSocketResponseAfterCallFails(Duration.ofSeconds(3)) - ) - .on(Selectors.reads().ap(), ops -> ops - .waitForCallToComplete(Duration.ofMillis(25)) - .abandonCallAfter(Duration.ofMillis(100)) - .maximumNumberOfCallAttempts(3) - ) - .on(Selectors.reads().query(), ops -> ops - .waitForCallToComplete(Duration.ofSeconds(2)) - .abandonCallAfter(Duration.ofSeconds(30)) - ) - .on(Selectors.reads().batch(), ops -> ops - .maximumNumberOfCallAttempts(7) - .allowInlineMemoryAccess(true) - ) - ); - Behavior childBehavior = newBehavior.deriveWithChanges("child", builder -> - builder.on(Selectors.writes().batch(), ops -> ops - .allowInlineSsdAccess(true) - .maxConcurrentNodes(5) - ) - .on(Selectors.reads().ap(), ops -> ops - .maximumNumberOfCallAttempts(8) + cluster.setRecordMappingFactory(new DefaultRecordMappingFactory(Map.of( + Customer.class, customerMapper, + Address.class, new AddressMapper() ) - ); - Behavior nonExceptionBehvaior = Behavior.DEFAULT.deriveWithChanges("nonException", builder -> - builder.on(Selectors.all(), ops -> ops.stackTraceOnException(false))); - - TypeSafeDataSet customerDataSet = TypeSafeDataSet.of("test", "person", Customer.class); -// DataSet customerDataSet = DataSet.of("test", "person"); - - Session session = cluster.createSession(newBehavior); - Session sessionWithoutExceptions = cluster.createSession(nonExceptionBehvaior); - - Set namespaces = session.info().namespaces(); - namespaces.forEach(ns -> { - Optional details = session.info().namespaceDetails(ns); - details.ifPresent(System.out::println); - }); + )); + + Behavior newBehavior = Behavior.DEFAULT.deriveWithChanges("newBehavior", builder -> + builder.on(Selectors.all(), ops -> ops + .waitForSocketResponseAfterCallFails(Duration.ofSeconds(3)) + ) + .on(Selectors.reads().ap(), ops -> ops + .waitForCallToComplete(Duration.ofMillis(25)) + .abandonCallAfter(Duration.ofMillis(100)) + .maximumNumberOfCallAttempts(3) + ) + .on(Selectors.reads().query(), ops -> ops + .waitForCallToComplete(Duration.ofSeconds(2)) + .abandonCallAfter(Duration.ofSeconds(30)) + ) + .on(Selectors.reads().batch(), ops -> ops + .maximumNumberOfCallAttempts(7) + .allowInlineMemoryAccess(true) + ) + ); + Behavior childBehavior = newBehavior.deriveWithChanges("child", builder -> + builder.on(Selectors.writes().batch(), ops -> ops + .allowInlineSsdAccess(true) + .maxConcurrentNodes(5) + ) + .on(Selectors.reads().ap(), ops -> ops + .maximumNumberOfCallAttempts(8) + ) + ); + Behavior nonExceptionBehvaior = Behavior.DEFAULT.deriveWithChanges("nonException", builder -> + builder.on(Selectors.all(), ops -> ops.stackTraceOnException(false))); + + TypeSafeDataSet customerDataSet = TypeSafeDataSet.of(namespace(), "person", Customer.class); +// DataSet customerDataSet = dataSet("person"); + + Session session = cluster.createSession(newBehavior); + Session sessionWithoutExceptions = cluster.createSession(nonExceptionBehvaior); + + DataSet users = dataSet("users"); + + printClusterInfo(session); + + List customers = sampleCustomers(); + + demonstrateBasicWritesAndErrors(session, customerDataSet); + seedData(session, customerDataSet, customerMapper, customers); + demonstrateConditionalUpdates(session, customerDataSet, customerMapper, customers); + demonstrateBatchReads(session, customerDataSet); + demonstrateAsyncFilteredUpdates(session, customerDataSet); + demonstratePointAndHeaderReads(session, customerDataSet); + demonstrateObjectListAndChunking(session, customerDataSet, customerMapper); + demonstrateSortingAndPagination(session, customerDataSet, customerMapper); + demonstrateTtl(session, customerDataSet); + demonstrateReadWriteExpressions(session, customerDataSet); + demonstrateQueryHints(session, customerDataSet); + demonstrateBackgroundQuery(session, customerDataSet); + demonstrateMultiOperationBatches(session, customerDataSet, users); + Customer readCustomer = demonstrateObjectMapping(session, customerDataSet, customerMapper); + demonstrateGenerationCheck(session, customerDataSet, readCustomer); + demonstrateComplexCdt(session, customerDataSet); + } - List sindexes = session.info().secondaryIndexes(); - System.out.println(sindexes); - sindexes.forEach(sindex -> { - System.out.printf("Secondary index: %s\n", sindex); - System.out.println(" " + session.info().secondaryIndexDetails(sindex)); - }); + private void printClusterInfo(Session session) { + Set namespaces = session.info().namespaces(); + namespaces.forEach(ns -> { + Optional details = session.info().namespaceDetails(ns); + details.ifPresent(System.out::println); + }); + + List sindexes = session.info().secondaryIndexes(); + System.out.println(sindexes); + sindexes.forEach(sindex -> { + System.out.printf("Secondary index: %s\n", sindex); + System.out.println(" " + session.info().secondaryIndexDetails(sindex)); + }); + } - session.truncate(customerDataSet); + private void demonstrateBasicWritesAndErrors(Session session, TypeSafeDataSet customerDataSet) { + session.truncate(customerDataSet); - try { - session.update(customerDataSet.id(1)).execute(); - } - catch (AerospikeException ae) { - System.out.printf("Exception caught as expected: %s (%s)\n", ae.getMessage(), ae.getClass()); - } - try { - session.update(customerDataSet.id(1)).bin("bob").setTo(5).execute(); - } - catch (AerospikeException ae) { - System.out.printf("Exception caught as expected on node %s: %s (%s)\n", ae.getNode(), ae.getMessage(), ae.getClass()); - } - RecordStream rss = session.update(customerDataSet.id(1)).bin("bob").setTo(5).executeAsync(ErrorStrategy.IN_STREAM); + try { + session.update(customerDataSet.id(1)).execute(); + } + catch (AerospikeException ae) { + System.out.printf("Exception caught as expected: %s (%s)\n", ae.getMessage(), ae.getClass()); + } + try { + session.update(customerDataSet.id(1)).bin("bob").setTo(5).execute(); + } + catch (AerospikeException ae) { + System.out.printf("Exception caught as expected on node %s: %s (%s)\n", ae.getNode(), ae.getMessage(), ae.getClass()); + } + RecordStream rss = session.update(customerDataSet.id(1)).bin("bob").setTo(5).executeAsync(ErrorStrategy.IN_STREAM); // System.out.println(rss.getFirst()); - session.insert(customerDataSet.id(1)) - .bin("Name").setTo("test1") - .bin("i1").setTo(1) - .bin("i2").setTo(2) - .bin("f1").setTo(1.1) - .bin("f2").setTo(2.2) - .bin("s1").setTo("hello ") - .bin("s2").setTo("world") - .execute(); + session.insert(customerDataSet.id(1)) + .bin("Name").setTo("test1") + .bin("i1").setTo(1) + .bin("i2").setTo(2) + .bin("f1").setTo(1.1) + .bin("f2").setTo(2.2) + .bin("s1").setTo("hello ") + .bin("s2").setTo("world") + .execute(); // print(session.query(customerDataSet.id(1)) // .bin("calc0").selectFrom("1.0 == $.f1") @@ -341,332 +209,351 @@ Address.class, new AddressMapper() // .execute()); // System.exit(0); - session.upsert(customerDataSet.id("bob")).bin("A").setTo(2).bin("B").setTo(2.2).execute(); - RecordStream rs1 = session.query(customerDataSet.id("bob")) - .readingOnlyBins("name") - .execute(); - System.out.println(rs1.getFirst()); + session.upsert(customerDataSet.id("bob")).bin("A").setTo(2).bin("B").setTo(2.2).execute(); + RecordStream rs1 = session.query(customerDataSet.id("bob")) + .readingOnlyBins("name") + .execute(); + System.out.println(rs1.getFirst()); + } - session.upsert(customerDataSet.ids(1,2,3,4,5)).bin("holdings").add(1).execute(); - session.upsert(customerDataSet) - .bins("name", "age") - .id(1).values("Tim", 312) - .id(2).values("Bob", 25) - .id(3).values("Jane", 46) - .execute(); + private void seedData(Session session, TypeSafeDataSet customerDataSet, CustomerMapper customerMapper, + List customers) { + session.upsert(customerDataSet.ids(1,2,3,4,5)).bin("holdings").add(1).execute(); + session.upsert(customerDataSet) + .bins("name", "age") + .id(1).values("Tim", 312) + .id(2).values("Bob", 25) + .id(3).values("Jane", 46) + .execute(); - System.out.printf("id(2) exists: %b\n", session.exists(customerDataSet.ids(2)).execute().getFirst()); - session.delete(customerDataSet.ids(2)).withoutDurableDelete().execute(); + System.out.printf("id(2) exists: %b\n", session.exists(customerDataSet.ids(2)).execute().getFirst()); + session.delete(customerDataSet.ids(2)).withoutDurableDelete().execute(); // System.out.printf("id(2) exists: %b\n", session.exists(customerDataSet.ids(2)).execute().getFirst()); - DataSet users = DataSet.of("test", "users"); - - RecordStream result = session.upsert(customerDataSet.id(80)) - .bin("name").setTo("Tim") - .bin("age").setTo(342) - .execute(); - System.out.println(result.getFirst()); + RecordStream result = session.upsert(customerDataSet.id(80)) + .bin("name").setTo("Tim") + .bin("age").setTo(342) + .execute(); + System.out.println(result.getFirst()); - session.upsert(customerDataSet.ids(81, 82)) - .bin("name").setTo("Tim") - .bin("age").setTo(343) - .execute(); - session.upsert(customerDataSet) - .bins("name", "age") - .id(83).values("Tim", 342) - .execute(); - session.upsert(customerDataSet) - .bins("name", "age") - .id(84).values("Tim", 342) - .id(85).values("Fred", 37) - .execute(); + session.upsert(customerDataSet.ids(81, 82)) + .bin("name").setTo("Tim") + .bin("age").setTo(343) + .execute(); + session.upsert(customerDataSet) + .bins("name", "age") + .id(83).values("Tim", 342) + .execute(); + session.upsert(customerDataSet) + .bins("name", "age") + .id(84).values("Tim", 342) + .id(85).values("Fred", 37) + .execute(); // sessionWithoutExceptions.insert(customerDataSet.id(80)) // .bin("name").setTo("Bob") // .execute() // .getFirst(); - session.upsert(customerDataSet.id(100)) - .bin("name").setTo("Tim") - .bin("age").setTo(312) - .bin("dob").setTo(new Date().getTime()) - .bin("id2").setTo(100) - .expireRecordAt(LocalDateTime.of(2030, 1, 1, 0, 0)) - .execute(); - - session.delete(customerDataSet.ids(900, 901, 902, 903, 904, 905)).execute(); - - session.insert(customerDataSet) - .bins("name", "age", "hair", "dob") - .id(899).values("Tim", 312, "brown", new Date().getTime()); - - RecordStream values = session.insert(customerDataSet) - .bins("name", "age", "hair", "dob") - .id(900).values("Tim", 312, "brown", new Date().getTime()) - .id(901).values("Jane", 28, "blonde", new Date().getTime()) - .id(902).values("Bob", 54, "brown", new Date().getTime()).expireRecordAfter(Duration.ofDays(5)) - .id(903).values("Jordan", 45, "red", new Date().getTime()) - .id(904).values("Alex", 67, "blonde", new Date().getTime()) - .id(905).values("Sam", 24, "brown", new Date().getTime()) - .defaultExpireRecordAfter(Duration.ofDays(30)) - .execute(); - values.forEach(kr -> System.out.printf("%s -> %s\n", kr.key(), kr.recordOrThrow())); - - for (int i = 0; i < 15; i++) { - session.upsert(customerDataSet.id(i)) - .bin("name").setTo("Tim-" + i) - .bin("age").setTo(312+i) - .bin("hair").setTo("brown") - .bin("dob").setTo(new Date().getTime()) - .execute(); - - session.upsert(customerDataSet) - .bins("name", "age", "hair", "dob") - .id(1000+i).values("Tim-"+i, 312+i, "brown", new Date().getTime()) - .expireRecordAfter(Duration.ofDays(30)) - .execute(); -// .values("Jane", 28, "blonde", new Date().getTime()) - } - - session.delete(customerDataSet.ids(1,2,3,5,7,11,13,17)).execute(); - - session.delete(customerDataSet.id(102)).execute(); - - session.insert(customerDataSet.id(102)) - .bin("name").setTo("Sue") - .bin("age").setTo(27) - .bin("id").setTo(102) + session.upsert(customerDataSet.id(100)) + .bin("name").setTo("Tim") + .bin("age").setTo(312) .bin("dob").setTo(new Date().getTime()) + .bin("id2").setTo(100) + .expireRecordAt(LocalDateTime.of(2030, 1, 1, 0, 0)) .execute(); - session.update(customerDataSet.id(102)) - .bin("age").setTo(26) - .execute(); - - session.delete(customerDataSet.id(102)).execute(); - - session.upsert(customerDataSet.id(102)) - .bin("name").setTo("Sue") - .bin("age").setTo(27) - .bin("id").setTo(102) - .bin("dob").setTo(new Date().getTime()) - .bin("rooms").setTo(Map.of( - "room1", Map.of("occupied", false, "rates", Map.of(1, 100, 2, 150, 3, -1)), - "room2", Map.of("occupied", true, "rates", Map.of(1, 90, 2, -1, 3, -1)), - "room3", Map.of("occupied", false, "rates", Map.of(1, 67, 2, 200, 3, 99)), - "room4", Map.of("occupied", true, "rates", Map.of(1, 98, 2, -1, 3, -1)), - "room5", Map.of("occupied", false, "rates", Map.of(1, 98, 2, -1, 3, -1)), - "room6", Map.of("occupied", true, "rates", Map.of(1, 98, 2, -1, 3, -1)) - )) - .bin("rooms2").setTo(Map.of("test", true)) + session.delete(customerDataSet.ids(900, 901, 902, 903, 904, 905)).execute(); + + session.insert(customerDataSet) + .bins("name", "age", "hair", "dob") + .id(899).values("Tim", 312, "brown", new Date().getTime()); + + RecordStream values = session.insert(customerDataSet) + .bins("name", "age", "hair", "dob") + .id(900).values("Tim", 312, "brown", new Date().getTime()) + .id(901).values("Jane", 28, "blonde", new Date().getTime()) + .id(902).values("Bob", 54, "brown", new Date().getTime()).expireRecordAfter(Duration.ofDays(5)) + .id(903).values("Jordan", 45, "red", new Date().getTime()) + .id(904).values("Alex", 67, "blonde", new Date().getTime()) + .id(905).values("Sam", 24, "brown", new Date().getTime()) + .defaultExpireRecordAfter(Duration.ofDays(30)) .execute(); + values.forEach(kr -> System.out.printf("%s -> %s\n", kr.key(), kr.recordOrThrow())); - - RecordStream results = session.upsert(customerDataSet.id(102)) - .bin("name").setTo("Bob") - .bin("age").setTo(30) - .bin("id").get() + for (int i = 0; i < 15; i++) { + session.upsert(customerDataSet.id(i)) + .bin("name").setTo("Tim-" + i) + .bin("age").setTo(312+i) + .bin("hair").setTo("brown") .bin("dob").setTo(new Date().getTime()) -// .bin("rooms").onMapKey("room1").update("Hello") - .bin("rooms").onMapIndex(2).getValues() - .bin("rooms").onMapKeyRange("room1", "room2").countAllOthers() - .bin("rooms").onMapKey("room1").getValues() - .bin("rooms").onMapKeyRange("room1", "room3").count() - .bin("rooms").onMapKey("room1").onMapKey("rates").onMapKey(1).setTo(110) - .bin("rooms").onMapKey("room2").mapClear() - .bin("rooms").onMapKeyRange("room4", "room9").remove() - .bin("rooms").onMapKey("room1").onMapKey("rates").onMapKey(1).add(5) - .bin("rooms").onMapKeyRelativeIndexRange("bob", -1, 1).getKeysAndValues() - .bin("rooms2").mapClear() - .bin("rooms2").onMapKey("child", MapOrder.KEY_ORDERED).onMapKey("subChild").setTo(5) - // TODO: How to insert an element into a list which doesn't exist? - // TODO: Should complex operations return one item per call? -// .bin("rooms2").onMapKey("child", MapOrder.KEY_ORDERED).onListIndex(0, ListOrder.UNORDERED, false).listAdd(5) .execute(); - System.out.println(results.getFirst()); - System.out.println(session.query(customerDataSet.id(102)).execute().getFirst()); - session.update(customerDataSet.id(102)) - .bin("name").append("-test") - .bin("age").add(1) - .execute(); - System.out.println(session.query(customerDataSet.id(102)).execute().getFirst()); - - - session.upsert(customerDataSet.id(102)) - .bin("name").setTo("Sue") - .bin("age").setTo(26) - .bin("dob").setTo(new Date().getTime()) + session.upsert(customerDataSet) + .bins("name", "age", "hair", "dob") + .id(1000+i).values("Tim-"+i, 312+i, "brown", new Date().getTime()) + .expireRecordAfter(Duration.ofDays(30)) .execute(); +// .values("Jane", 28, "blonde", new Date().getTime()) + } - List customers = List.of( - new Customer(20, "Jordan", 36, new Date()), - new Customer(21, "Alex", 27, new Date()), - new Customer(22, "Betty", 27, new Date()), - new Customer(23, "Bob", 33, new Date()), - new Customer(24, "Fred", 6, new Date()), - new Customer(25, "Alex", 28, new Date()), - new Customer(26, "Alex", 26, new Date()), - new Customer(27, "Jordan", 19, new Date()), - new Customer(28, "Gruper", 28, new Date()), - new Customer(29, "Bree", 24, new Date()), - new Customer(30, "Perry", 44, new Date()), - new Customer(31, "Alex", 27, new Date()), - new Customer(32, "Betty", 27, new Date()), - new Customer(33, "Wilma", 18, new Date()), - new Customer(34, "Joran", 82, new Date()), - new Customer(35, "Alex", 27, new Date()), - new Customer(36, "Fred", 99, new Date()), - new Customer(37, "Sydney", 22, new Date()), - new Customer(38, "Ita", 99, new Date()), - new Customer(39, "Rupert", 83, new Date()), - new Customer(40, "Dominic", 53, new Date()), - new Customer(41, "Tim", 27, new Date()), - new Customer(42, "Tim", 29, new Date()), - new Customer(43, "Tim", 31, new Date()), - new Customer(44, "Tim", 30, new Date()), - new Customer(45, "Tim", 33, new Date()), - new Customer(46, "Tim", 35, new Date()) - ); - - session.insert(customerDataSet) - .objects(customers) - .using(customerMapper) - .execute(); + session.delete(customerDataSet.ids(1,2,3,5,7,11,13,17)).execute(); + + session.delete(customerDataSet.id(102)).execute(); + + session.insert(customerDataSet.id(102)) + .bin("name").setTo("Sue") + .bin("age").setTo(27) + .bin("id").setTo(102) + .bin("dob").setTo(new Date().getTime()) + .execute(); + + session.update(customerDataSet.id(102)) + .bin("age").setTo(26) + .execute(); + + session.delete(customerDataSet.id(102)).execute(); + + session.upsert(customerDataSet.id(102)) + .bin("name").setTo("Sue") + .bin("age").setTo(27) + .bin("id").setTo(102) + .bin("dob").setTo(new Date().getTime()) + .bin("rooms").setTo(Map.of( + "room1", Map.of("occupied", false, "rates", Map.of(1, 100, 2, 150, 3, -1)), + "room2", Map.of("occupied", true, "rates", Map.of(1, 90, 2, -1, 3, -1)), + "room3", Map.of("occupied", false, "rates", Map.of(1, 67, 2, 200, 3, 99)), + "room4", Map.of("occupied", true, "rates", Map.of(1, 98, 2, -1, 3, -1)), + "room5", Map.of("occupied", false, "rates", Map.of(1, 98, 2, -1, 3, -1)), + "room6", Map.of("occupied", true, "rates", Map.of(1, 98, 2, -1, 3, -1)) + )) + .bin("rooms2").setTo(Map.of("test", true)) + .execute(); - System.out.println("Updating all customers called Tim"); - print(session.update(customerDataSet) - .where("$.name == 'Tim'") - .objects(customers) - .using(customerMapper) - .execute()); - System.out.printf("Customer 46 age before scan: %d\n", - session.query(customerDataSet.id(46)).execute().getFirstRecord().getInt("age")); + RecordStream results = session.upsert(customerDataSet.id(102)) + .bin("name").setTo("Bob") + .bin("age").setTo(30) + .bin("id").get() + .bin("dob").setTo(new Date().getTime()) +// .bin("rooms").onMapKey("room1").update("Hello") + .bin("rooms").onMapIndex(2).getValues() + .bin("rooms").onMapKeyRange("room1", "room2").countAllOthers() + .bin("rooms").onMapKey("room1").getValues() + .bin("rooms").onMapKeyRange("room1", "room3").count() + .bin("rooms").onMapKey("room1").onMapKey("rates").onMapKey(1).setTo(110) + .bin("rooms").onMapKey("room2").mapClear() + .bin("rooms").onMapKeyRange("room4", "room9").remove() + .bin("rooms").onMapKey("room1").onMapKey("rates").onMapKey(1).add(5) + .bin("rooms").onMapKeyRelativeIndexRange("bob", -1, 1).getKeysAndValues() + .bin("rooms2").mapClear() + .bin("rooms2").onMapKey("child", MapOrder.KEY_ORDERED).onMapKey("subChild").setTo(5) + // TODO: How to insert an element into a list which doesn't exist? + // TODO: Should complex operations return one item per call? +// .bin("rooms2").onMapKey("child", MapOrder.KEY_ORDERED).onListIndex(0, ListOrder.UNORDERED, false).listAdd(5) + .execute(); + + System.out.println(results.getFirst()); + System.out.println(session.query(customerDataSet.id(102)).execute().getFirst()); + session.update(customerDataSet.id(102)) + .bin("name").append("-test") + .bin("age").add(1) + .execute(); + System.out.println(session.query(customerDataSet.id(102)).execute().getFirst()); + + + session.upsert(customerDataSet.id(102)) + .bin("name").setTo("Sue") + .bin("age").setTo(26) + .bin("dob").setTo(new Date().getTime()) + .execute(); + + session.insert(customerDataSet) + .objects(customers) + .using(customerMapper) + .execute(); + } - ExecuteTask task = session.backgroundTask().update(customerDataSet) - .bin("age").add(1) - .execute(); + /** + * The seed set of customers inserted as mapped objects. Referenced again later for + * conditional (where-filtered) updates. + */ + private static List sampleCustomers() { + return List.of( + new Customer(20, "Jordan", 36, new Date()), + new Customer(21, "Alex", 27, new Date()), + new Customer(22, "Betty", 27, new Date()), + new Customer(23, "Bob", 33, new Date()), + new Customer(24, "Fred", 6, new Date()), + new Customer(25, "Alex", 28, new Date()), + new Customer(26, "Alex", 26, new Date()), + new Customer(27, "Jordan", 19, new Date()), + new Customer(28, "Gruper", 28, new Date()), + new Customer(29, "Bree", 24, new Date()), + new Customer(30, "Perry", 44, new Date()), + new Customer(31, "Alex", 27, new Date()), + new Customer(32, "Betty", 27, new Date()), + new Customer(33, "Wilma", 18, new Date()), + new Customer(34, "Joran", 82, new Date()), + new Customer(35, "Alex", 27, new Date()), + new Customer(36, "Fred", 99, new Date()), + new Customer(37, "Sydney", 22, new Date()), + new Customer(38, "Ita", 99, new Date()), + new Customer(39, "Rupert", 83, new Date()), + new Customer(40, "Dominic", 53, new Date()), + new Customer(41, "Tim", 27, new Date()), + new Customer(42, "Tim", 29, new Date()), + new Customer(43, "Tim", 31, new Date()), + new Customer(44, "Tim", 30, new Date()), + new Customer(45, "Tim", 33, new Date()), + new Customer(46, "Tim", 35, new Date()) + ); + } - System.out.printf("task id = %d\n", task.getTaskId()); - task.waitTillComplete(); - System.out.printf("Customer 46 age after scan: %d\n", - session.query(customerDataSet.id(46)).execute().getFirstRecord().getInt("age")); + private void demonstrateConditionalUpdates(Session session, TypeSafeDataSet customerDataSet, + CustomerMapper customerMapper, List customers) { + System.out.println("Updating all customers called Tim"); + print(session.update(customerDataSet) + .where("$.name == 'Tim'") + .objects(customers) + .using(customerMapper) + .execute()); + + System.out.printf("Customer 46 age before scan: %d\n", + session.query(customerDataSet.id(46)).execute().getFirstRecord().getInt("age")); + + ExecuteTask task = session.backgroundTask().update(customerDataSet) + .bin("age").add(1) + .execute(); + + System.out.printf("task id = %d\n", task.getTaskId()); + task.waitTillComplete(); + System.out.printf("Customer 46 age after scan: %d\n", + session.query(customerDataSet.id(46)).execute().getFirstRecord().getInt("age")); + } - // Batch partition filter test - List keys = customerDataSet.ids(IntStream.rangeClosed(20, 48).toArray()); - System.out.println("Read 25 records, but only those in partitions 0->2047"); - print(session.query(keys) - .onPartitionRange(0, 2048) - .execute()); + private void demonstrateBatchReads(Session session, TypeSafeDataSet customerDataSet) { + // Batch partition filter test + List keys = customerDataSet.ids(IntStream.rangeClosed(20, 48).toArray()); + System.out.println("Read 25 records, but only those in partitions 0->2047"); + print(session.query(keys) + .onPartitionRange(0, 2048) + .execute()); - System.out.println("Full batch read:"); - print(session.query(keys).execute()); + System.out.println("Full batch read:"); + print(session.query(keys).execute()); - System.out.println("\nBatchRead where name = 'Tim':"); - print(session.query(keys).where("$.name == 'Tim'").execute()); + System.out.println("\nBatchRead where name = 'Tim':"); + print(session.query(keys).where("$.name == 'Tim'").execute()); - System.out.println("\nBatchRead where name = 'Tim':"); - print(session.query(keys).includeMissingKeys().where("$.name == 'Tim'").execute()); + System.out.println("\nBatchRead where name = 'Tim':"); + print(session.query(keys).includeMissingKeys().where("$.name == 'Tim'").execute()); - System.out.println("\nBatchRead where name = 'Tim':"); - print(session.query(keys).where("$.name == 'Tim'").includeMissingKeys().failOnFilteredOut().execute()); + System.out.println("\nBatchRead where name = 'Tim':"); + print(session.query(keys).where("$.name == 'Tim'").includeMissingKeys().failOnFilteredOut().execute()); // System.out.println("Read the set, limit 6, test than includeMissingKeys() gives a compile error"); // print(session.query(customerDataSet).includeMissingKeys().execute()); // print(session.query(customerDataSet).failOnFilteredOut().execute()); - System.out.println("Read the set, limit 6"); - print(session.query(customerDataSet).limit(6).execute()); + System.out.println("Read the set, limit 6"); + print(session.query(customerDataSet).limit(6).execute()); + } - List keyList2 = customerDataSet.ids(20,21,22,23,24,25,26,27); - RecordStream thisStream = session.update(keyList2) - .bin("age").add(1) - .execute(); + private void demonstrateAsyncFilteredUpdates(Session session, TypeSafeDataSet customerDataSet) { + List keyList2 = customerDataSet.ids(20,21,22,23,24,25,26,27); + RecordStream thisStream = session.update(keyList2) + .bin("age").add(1) + .execute(); - System.out.println("Showing results before guaranteeing execution has finished."); - print(session.query(keyList2).execute()); - System.out.println("Showing async results"); - print(thisStream); - System.out.println("Showing results now execution has finished."); - print(session.query(keyList2).execute()); + System.out.println("Showing results before guaranteeing execution has finished."); + print(session.query(keyList2).execute()); + System.out.println("Showing async results"); + print(thisStream); + System.out.println("Showing results now execution has finished."); + print(session.query(keyList2).execute()); - System.out.printf("Update people in list whose age is < 35 (%s)\n", keyList2); - print(session.update(keyList2) - .bin("age").add(1) - .where("$.age < 35") - .execute()); - print(session.query(keyList2).execute()); + System.out.printf("Update people in list whose age is < 35 (%s)\n", keyList2); + print(session.update(keyList2) + .bin("age").add(1) + .where("$.age < 35") + .execute()); + print(session.query(keyList2).execute()); - session.update(keyList2) - .bin("age").add(1) - .where("$.age < 35") - .failOnFilteredOut() - .execute(); - print(session.query(keyList2).execute()); + session.update(keyList2) + .bin("age").add(1) + .where("$.age < 35") + .failOnFilteredOut() + .execute(); + print(session.query(keyList2).execute()); + } - // Query contract: - // - If a list of ids is provided and there is not "sort" clause, the records in the stream are returned in the order which the ids are specified - // - If we're processing the records with notifiers, we cannot also get them back in a stream - // Should there be a KeyRecord style class with an error code on it and inDoubt? (Similar to Batch Record, but this violates SOLID by being used both for - // input and output) + private void demonstratePointAndHeaderReads(Session session, TypeSafeDataSet customerDataSet) { + // Query contract: + // - If a list of ids is provided and there is not "sort" clause, the records in the stream are returned in the order which the ids are specified + // - If we're processing the records with notifiers, we cannot also get them back in a stream + // Should there be a KeyRecord style class with an error code on it and inDoubt? (Similar to Batch Record, but this violates SOLID by being used both for + // input and output) - // Need a class to turn a resultcode into an exception similar to SQLExceptionTranslator in Spring Boot. These are customizable in `sql-error-codes.xml` - // file, eg: - // - // PostgreSQL - // 23505 - // 23000,23502,23503 - // + // Need a class to turn a resultcode into an exception similar to SQLExceptionTranslator in Spring Boot. These are customizable in `sql-error-codes.xml` + // file, eg: + // + // PostgreSQL + // 23505 + // 23000,23502,23503 + // - // Threads are really cheap in JDK 21+ so all calls could notify async via a new thread and an ArrayBlockingQueue for example? + // Threads are really cheap in JDK 21+ so all calls could notify async via a new thread and an ArrayBlockingQueue for example? // session.query(customerDataSet) // .onRecordArrival(keyRecord -> System.out.println(keyRecord)) // .onRecordError() // .onDone(() -> System.out.println("Done")) // .execute(); - System.out.println("\nRead point records - in the same order as the keys, limit to 3"); - print(session.query(customerDataSet.ids(1,3,5,7)).limit(3).execute()); - - System.out.println("\nSingle point record"); - print(session.query(customerDataSet.ids(6)).execute()); + System.out.println("\nRead point records - in the same order as the keys, limit to 3"); + print(session.query(customerDataSet.ids(1,3,5,7)).limit(3).execute()); - System.out.println("Read the set, output as stream, limit of 5"); - session.query(customerDataSet).limit(5).execute() - .stream() - .map(keyRec -> "Name: " + keyRec.recordOrThrow().getString("name")) - .forEach(str -> System.out.println(str)); + System.out.println("\nSingle point record"); + print(session.query(customerDataSet.ids(6)).execute()); - System.out.println("Read header, point read"); - print(session.query(customerDataSet.id(6)).withNoBins().execute()); - System.out.println("Read header, batch read"); - print(session.query(customerDataSet.ids(6,7,8)).withNoBins().execute()); - System.out.println("Read header, set read"); - print(session.query(customerDataSet).withNoBins().execute()); - - System.out.println("Read with select bins, point read"); - print(session.query(customerDataSet.ids(6)).readingOnlyBins("name", "age").execute()); - System.out.println("Read with select bins, batch read"); - print(session.query(customerDataSet.ids(6,7,8)).readingOnlyBins("name", "age").execute()); - System.out.println("Read with select bins, set read"); - print(session.query(customerDataSet).readingOnlyBins("name", "age").execute()); + System.out.println("Read the set, output as stream, limit of 5"); + session.query(customerDataSet).limit(5).execute() + .stream() + .map(keyRec -> "Name: " + keyRec.recordOrThrow().getString("name")) + .forEach(str -> System.out.println(str)); + + System.out.println("Read header, point read"); + print(session.query(customerDataSet.id(6)).withNoBins().execute()); + System.out.println("Read header, batch read"); + print(session.query(customerDataSet.ids(6,7,8)).withNoBins().execute()); + System.out.println("Read header, set read"); + print(session.query(customerDataSet).withNoBins().execute()); + + System.out.println("Read with select bins, point read"); + print(session.query(customerDataSet.ids(6)).readingOnlyBins("name", "age").execute()); + System.out.println("Read with select bins, batch read"); + print(session.query(customerDataSet.ids(6,7,8)).readingOnlyBins("name", "age").execute()); + System.out.println("Read with select bins, set read"); + print(session.query(customerDataSet).readingOnlyBins("name", "age").execute()); // session.update(customerDataSet.ids(1,2,3,4)) // .bin - // Throw an exception - try { - print(session.query(customerDataSet.ids(6,7,8)).readingOnlyBins("name", "age").withNoBins().execute()); - } - catch (Exception e) { - e.printStackTrace(); - } + // Throw an exception + try { + print(session.query(customerDataSet.ids(6,7,8)).readingOnlyBins("name", "age").withNoBins().execute()); + } + catch (Exception e) { + e.printStackTrace(); + } + } - // TODO: Put transaction control into policies + private void demonstrateObjectListAndChunking(Session session, TypeSafeDataSet customerDataSet, + CustomerMapper customerMapper) { + // TODO: Put transaction control into policies // session.doInTransaction(txnSession -> { // Optional recResult = txnSession.query(customerDataSet.id(1)).execute().getFirst(); // if (true) { @@ -676,528 +563,551 @@ Address.class, new AddressMapper() // txnSession.insert(customerDataSet.id(3)).notInAnyTransaction().execute(); // }); - customers = session.query(customerDataSet.ids(20, 21)).execute().toObjectList(customerMapper); - System.out.println(customers); + List customers = session.query(customerDataSet.ids(20, 21)).execute().toObjectList(customerMapper); + System.out.println(customers); - // Records-per-second check - RecordStream queryResults = session.query(customerDataSet).recordsPerSecond(1).execute(); - queryResults.forEach(rr -> System.out.println(rr.recordOrThrow())); - // session.query(customerDataSet.id(1)).recordsPerSecond(100).execute(); + // Records-per-second check + RecordStream queryResults = session.query(customerDataSet).recordsPerSecond(1).execute(); + queryResults.forEach(rr -> System.out.println(rr.recordOrThrow())); + // session.query(customerDataSet.id(1)).recordsPerSecond(100).execute(); - // Server-side chunking example - fetch records in chunks of 20 - customers = session.query(customerDataSet).chunkSize(20).execute().toObjectList(customerMapper); - System.out.println(customers); + // Server-side chunking example - fetch records in chunks of 20 + customers = session.query(customerDataSet).chunkSize(20).execute().toObjectList(customerMapper); + System.out.println(customers); - // Server-side chunking example - process records in chunks of 10 - RecordStream rs = session.query(customerDataSet).chunkSize(10).execute(); - int chunk = 0; - while (rs.hasMoreChunks()) { - System.out.println("Chunk: " + (++chunk)); - rs.forEach(rec -> System.out.println(rec)); - } + // Server-side chunking example - process records in chunks of 10 + RecordStream rs = session.query(customerDataSet).chunkSize(10).execute(); + int chunk = 0; + while (rs.hasMoreChunks()) { + System.out.println("Chunk: " + (++chunk)); + rs.forEach(rec -> System.out.println(rec)); + } - int total = session.query(customerDataSet) + int total = session.query(customerDataSet) + .execute() + .stream() + .mapToInt(kr -> kr.recordOrThrow().getInt("quantity")) + .sum(); + } + + private void demonstrateSortingAndPagination(Session session, TypeSafeDataSet customerDataSet, + CustomerMapper customerMapper) { + System.out.println("\n\nSorting customers by Name with a where clause using NavigatableRecordStream"); + List customers = session.query(customerDataSet) + .where("$.name == 'Tim' and $.age > 30") + .limit(1000) .execute() - .stream() - .mapToInt(kr -> kr.recordOrThrow().getInt("quantity")) - .sum(); - System.out.println("\n\nSorting customers by Name with a where clause using NavigatableRecordStream"); - customers = session.query(customerDataSet) - .where("$.name == 'Tim' and $.age > 30") - .limit(1000) - .execute() - .asNavigatableStream() - .sortBy("name", SortDir.SORT_ASC, true) - .toObjectList(customerMapper); - - for (Customer customer : customers) { - System.out.println(customer); - } + .asNavigatableStream() + .sortBy("name", SortDir.SORT_ASC, true) + .toObjectList(customerMapper); - System.out.println("End sorting customers by Name with a where clause\n"); - - customers = session.query(customerDataSet) - .where(Ael.stringBin("name").eq("Tim").and(Ael.longBin("age").gt(30))) - .limit(1000) - .execute() - .asNavigatableStream() - .sortBy("name", SortDir.SORT_ASC, true) - .toObjectList(customerMapper); - for (Customer customer : customers) { - System.out.println(customer); - } + for (Customer customer : customers) { + System.out.println(customer); + } - System.out.println("---- End sort ---"); + System.out.println("End sorting customers by Name with a where clause\n"); + customers = session.query(customerDataSet) + .where(Ael.stringBin("name").eq("Tim").and(Ael.longBin("age").gt(30))) + .limit(1000) + .execute() + .asNavigatableStream() + .sortBy("name", SortDir.SORT_ASC, true) + .toObjectList(customerMapper); + for (Customer customer : customers) { + System.out.println(customer); + } - System.out.println("\n\nSorting customers by Age (desc) then name (asc), using NavigatableRecordStream for client-side pagination"); - try (NavigatableRecordStream navStream = session.query(customerDataSet) - .limit(13) - .execute() - .asNavigatableStream() - .sortBy(List.of( - SortProperties.descending("age"), - SortProperties.ascendingIgnoreCase("name") - )) - .pageSize(5)) { - - int page = 0; - while (navStream.hasMorePages()) { - System.out.println("---- Page " + (++page) + " -----"); - customers = navStream.toObjectList(customerMapper); - customers.forEach(cust -> System.out.println(cust)); - } - System.out.println("---- End sort ---"); - - // Jump to page 2 - System.out.println("--- Setting page to 2 ---"); - navStream.setPageTo(2); - navStream.forEach(rec -> System.out.println(rec)); - System.out.println("--- done with page 2 ---"); - - // Now re-sort by name only - System.out.println("Re-sorting records by name"); - navStream.sortBy(SortProperties.ascending("name")); - int pageNum = 0; - while (navStream.hasMorePages()) { - System.out.println("---- Page " + (++pageNum) + " -----"); - List custList = navStream.toObjectList(customerMapper); - custList.forEach(cust -> System.out.println(cust)); - } - System.out.println("---- End sort ---"); - } + System.out.println("---- End sort ---"); - // ------------- - // UDF Calls - // ------------- -// Object udfResult = session.executeUdf(customerDataSet.id(1)).function("pkg", "myFunc").execute().getFirstUdfResult(); -// System.out.println("UDF Result = " + udfResult); - // --------------------------- - // TTL Test - // --------------------------- - System.out.println("--- Test TTL ---"); - session.delete(customerDataSet.id(1)).execute(); + System.out.println("\n\nSorting customers by Age (desc) then name (asc), using NavigatableRecordStream for client-side pagination"); + try (NavigatableRecordStream navStream = session.query(customerDataSet) + .limit(13) + .execute() + .asNavigatableStream() + .sortBy(List.of( + SortProperties.descending("age"), + SortProperties.ascendingIgnoreCase("name") + )) + .pageSize(5)) { - session.upsert(customerDataSet) - .bins("binA") - .id(1).values(5) - .expireRecordAfterSeconds(5) - .execute(); - System.out.println("Initial read, should be there"); - System.out.println(session.query(customerDataSet.id(1)).execute().getFirst()); - try { - Thread.sleep(Duration.ofSeconds(6)); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + int page = 0; + while (navStream.hasMorePages()) { + System.out.println("---- Page " + (++page) + " -----"); + customers = navStream.toObjectList(customerMapper); + customers.forEach(cust -> System.out.println(cust)); } - System.out.println("Read after TTL exires, should not be there"); - System.out.println(session.query(customerDataSet.id(1)).execute().getFirst()); - - // --------------------------- - // Read and write expressions - // --------------------------- - System.out.println("--- Expression testing ---"); - session.replace(customerDataSet.id(223)) - .bin("age").setTo(500) - .bin("value").setTo(123) - .execute(); - - System.out.printf("Base record: %s\n", - session.query(customerDataSet.id(223)).execute().getFirst()); - - print(session.query(customerDataSet.id(223)) - .bin("age").get() - .execute()); - System.out.println("Using a read expression"); - - rs = session.query(customerDataSet.ids(223)) - .bin("bob").selectFrom("$.age + $.value", arg -> arg.ignoreEvalFailure()) - .execute(); - print(rs); + System.out.println("---- End sort ---"); - System.out.println("Using a write expression"); - session.upsert(customerDataSet.id(1)) - .bin("age").setTo(50) - .bin("value").setTo(10) - .bin("c2").upsertFrom("$.age + 2 * $.value", arg -> arg.ignoreOpFailure().deleteIfNull()) - .execute(); - session.update(customerDataSet.id(223)) - .bin("bob").upsertFrom("$.age + 2 * $.value") - .execute(); - System.out.printf("Modified record: %s\n", - session.query(customerDataSet.id(223)).execute().getFirst()); + // Jump to page 2 + System.out.println("--- Setting page to 2 ---"); + navStream.setPageTo(2); + navStream.forEach(rec -> System.out.println(rec)); + System.out.println("--- done with page 2 ---"); + + // Now re-sort by name only + System.out.println("Re-sorting records by name"); + navStream.sortBy(SortProperties.ascending("name")); + int pageNum = 0; + while (navStream.hasMorePages()) { + System.out.println("---- Page " + (++pageNum) + " -----"); + List custList = navStream.toObjectList(customerMapper); + custList.forEach(cust -> System.out.println(cust)); + } + System.out.println("---- End sort ---"); + } + } + private void demonstrateTtl(Session session, TypeSafeDataSet customerDataSet) { + // ------------- + // UDF Calls + // ------------- +// Object udfResult = session.executeUdf(customerDataSet.id(1)).function("pkg", "myFunc").execute().getFirstUdfResult(); +// System.out.println("UDF Result = " + udfResult); - session.query(customerDataSet) - .readingOnlyBins("name", "age") - .execute(); + // --------------------------- + // TTL Test + // --------------------------- + System.out.println("--- Test TTL ---"); + session.delete(customerDataSet.id(1)).execute(); + + session.upsert(customerDataSet) + .bins("binA") + .id(1).values(5) + .expireRecordAfterSeconds(5) + .execute(); + System.out.println("Initial read, should be there"); + System.out.println(session.query(customerDataSet.id(1)).execute().getFirst()); + try { + Thread.sleep(Duration.ofSeconds(6)); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + System.out.println("Read after TTL exires, should not be there"); + System.out.println(session.query(customerDataSet.id(1)).execute().getFirst()); + } - // --------------------------- - // Query hints - // --------------------------- - System.out.println("\n--- Query hints ---"); + private void demonstrateReadWriteExpressions(Session session, TypeSafeDataSet customerDataSet) { + // --------------------------- + // Read and write expressions + // --------------------------- + System.out.println("--- Expression testing ---"); + session.replace(customerDataSet.id(223)) + .bin("age").setTo(500) + .bin("value").setTo(123) + .execute(); + + System.out.printf("Base record: %s\n", + session.query(customerDataSet.id(223)).execute().getFirst()); + + print(session.query(customerDataSet.id(223)) + .bin("age").get() + .execute()); + System.out.println("Using a read expression"); + + RecordStream rs = session.query(customerDataSet.ids(223)) + .bin("bob").selectFrom("$.age + $.value", arg -> arg.ignoreEvalFailure()) + .execute(); + print(rs); + + System.out.println("Using a write expression"); + session.upsert(customerDataSet.id(1)) + .bin("age").setTo(50) + .bin("value").setTo(10) + .bin("c2").upsertFrom("$.age + 2 * $.value", arg -> arg.ignoreOpFailure().deleteIfNull()) + .execute(); + session.update(customerDataSet.id(223)) + .bin("bob").upsertFrom("$.age + 2 * $.value") + .execute(); + System.out.printf("Modified record: %s\n", + session.query(customerDataSet.id(223)).execute().getFirst()); + + + session.query(customerDataSet) + .readingOnlyBins("name", "age") + .execute(); + } - // Hint with index name: tell the server to use a specific secondary index - session.query(customerDataSet) - .where("$.age > 30") - .withHint(hint -> hint.forIndex("age_idx")) - .execute(); + private void demonstrateQueryHints(Session session, TypeSafeDataSet customerDataSet) { + // --------------------------- + // Query hints + // --------------------------- + System.out.println("\n--- Query hints ---"); + + // Hint with index name: tell the server to use a specific secondary index + session.query(customerDataSet) + .where("$.age > 30") + .withHint(hint -> hint.forIndex("age_idx")) + .execute(); + + // Hint with bin name: prefer the secondary index on a given bin + session.query(customerDataSet) + .where("$.age > 30") + .withHint(hint -> hint.forBin("age")) + .execute(); + + // Hint with query duration only: override expected duration + session.query(customerDataSet) + .where("$.age > 30") + .withHint(hint -> hint.queryDuration(QueryDuration.SHORT)) + .execute(); + + // Hint combining index name and query duration + session.query(customerDataSet) + .where("$.age > 30") + .withHint(hint -> hint.forIndex("age_idx").queryDuration(QueryDuration.SHORT)) + .execute(); + + // Hint combining query duration first, then bin name + session.query(customerDataSet) + .where("$.age > 30") + .withHint(hint -> hint.queryDuration(QueryDuration.SHORT).forBin("age")) + .execute(); + } - // Hint with bin name: prefer the secondary index on a given bin - session.query(customerDataSet) - .where("$.age > 30") - .withHint(hint -> hint.forBin("age")) - .execute(); + private void demonstrateBackgroundQuery(Session session, TypeSafeDataSet customerDataSet) { + // --------------------------- + // Background query operations + // --------------------------- + session.backgroundTask().update(customerDataSet) + .bin("age").add(1) + .bin("bob").upsertFrom("$.age + $.value") + .where("$.state == 'nsw'") + .execute() + .waitTillComplete(); + } - // Hint with query duration only: override expected duration - session.query(customerDataSet) - .where("$.age > 30") - .withHint(hint -> hint.queryDuration(QueryDuration.SHORT)) + private void demonstrateMultiOperationBatches(Session session, TypeSafeDataSet customerDataSet, DataSet users) { + // ------------------------ + // Multi operation batches + // ------------------------ + RecordStream rsStream = session + .update(customerDataSet.ids(1000, 1001)) + .bin("age").add(1) + .bin("dob").setTo(new Date().getTime()) +// .where("$.age > 100") + .expireRecordAfter(Duration.ofMinutes(5)) + .exists(customerDataSet.ids(1000,1001)) + .query(customerDataSet.ids(10,12)) + .delete(customerDataSet.id(1003)) + .notInAnyTransaction() +// .defaultWhere("$.value > 200") + .defaultExpireRecordAfter(Duration.ofMinutes(20)) + .execute(); + System.out.println("Multi operations:"); + print(rsStream); + + rsStream = session.query(customerDataSet.ids(1,2,3)) + .bin("name").get() + .bin("map").onMapKeyRange(5, 10).getKeysAndValues() + .update(customerDataSet.ids(1)) + .bin("age").add(1) .execute(); - // Hint combining index name and query duration - session.query(customerDataSet) - .where("$.age > 30") - .withHint(hint -> hint.forIndex("age_idx").queryDuration(QueryDuration.SHORT)) + rsStream = session + .update(customerDataSet.ids(1,2,3)) + .bin("age").add(1) + .bin("updated").setTo(true) + .where("$.age < 21") + .delete(customerDataSet.ids(11,12,13,14,15)) + .update(customerDataSet.ids(5,6,7)) + .bin("luckyWinner").setTo("true") + .defaultWhere("$.updated == false") .execute(); - // Hint combining query duration first, then bin name - session.query(customerDataSet) - .where("$.age > 30") - .withHint(hint -> hint.queryDuration(QueryDuration.SHORT).forBin("age")) + rsStream = session.query(customerDataSet.ids(1,2,3)) + .limit(2) + .update(customerDataSet.ids(4,5,6)) + .bin("name").setTo("bob") + .expireRecordAfterSeconds(500) + .query(customerDataSet.id(7)) .execute(); - // --------------------------- - // Background query operations - // --------------------------- - session.backgroundTask().update(customerDataSet) - .bin("age").add(1) - .bin("bob").upsertFrom("$.age + $.value") - .where("$.state == 'nsw'") - .execute(); + //AEL: Clean, readable syntax + RecordStream stream = session.query(users) + .where("$.status == 'active' and $.age >= 21") + .execute(); + stream.forEach(res -> { + if (res.isOk()) { + Record row = res.recordOrThrow(); + // Process row + } + }); + stream.close(); + } - // ------------------------ - // Multi operation batches - // ------------------------ - RecordStream rsStream = session - .update(customerDataSet.ids(1000, 1001)) - .bin("age").add(1) - .bin("dob").setTo(new Date().getTime()) -// .where("$.age > 100") - .expireRecordAfter(Duration.ofMinutes(5)) - .exists(customerDataSet.ids(1000,1001)) - .query(customerDataSet.ids(10,12)) - .delete(customerDataSet.id(1003)) - .notInAnyTransaction() -// .defaultWhere("$.value > 200") - .defaultExpireRecordAfter(Duration.ofMinutes(20)) + private Customer demonstrateObjectMapping(Session session, TypeSafeDataSet customerDataSet, + CustomerMapper customerMapper) { + // -------------------- + // Object mapping + // -------------------- + // Insert then read back a customer with an address + System.out.println("\n--- Object mapping test ----"); + Customer sampleCust = new Customer(999, "sample", 456, new Date(), new Address("123 Main St", "Denver", "CO", "USA", "80112")); + System.out.println("Reference customer: " + sampleCust); + + session.delete(customerDataSet.id(999)).execute(); + session.insert(customerDataSet).object(sampleCust).execute(); + Customer readCustomer = session.query(customerDataSet.id(999)).execute().toObjectList(customerMapper).get(0); + System.out.println("Customer read back: " + readCustomer); + System.out.println("--- End object mapping test ----"); + + try { + session.query(customerDataSet).where("$.name == 'Tim'") + .bin("fred").get() .execute(); - System.out.println("Multi operations:"); - print(rsStream); - - rsStream = session.query(customerDataSet.ids(1,2,3)) - .bin("name").get() - .bin("map").onMapKeyRange(5, 10).getKeysAndValues() - .update(customerDataSet.ids(1)) - .bin("age").add(1) - .execute(); - - rsStream = session - .update(customerDataSet.ids(1,2,3)) - .bin("age").add(1) - .bin("updated").setTo(true) - .where("$.age < 21") - .delete(customerDataSet.ids(11,12,13,14,15)) - .update(customerDataSet.ids(5,6,7)) - .bin("luckyWinner").setTo("true") - .defaultWhere("$.updated == false") - .execute(); + System.err.println("Expected query with bin operations to throw an exception for now"); + } + catch (BinOpInvalidException boie) { + // Do nothing, expected. + } + // ----------------------- + // Object mapping (nested) + // ----------------------- + Address address1 = new Address("123 Main St", "Denver", "CO", "USA", "80000"); + Customer customer = new Customer(1, "Bob", 37, new Date(), null); + session.replace(customerDataSet).object(customer).using(customerMapper).execute(); + session.upsert(customerDataSet.id(customer.getId())).bin("addrs").onMapKey("home").upsert(address1, new AddressMapper()); + + return readCustomer; + } - rsStream = session.query(customerDataSet.ids(1,2,3)) - .limit(2) - .update(customerDataSet.ids(4,5,6)) - .bin("name").setTo("bob") - .expireRecordAfterSeconds(500) - .query(customerDataSet.id(7)) + private void demonstrateGenerationCheck(Session session, TypeSafeDataSet customerDataSet, Customer readCustomer) { + // ---------------- + // Generation check + // ---------------- + System.out.println("\n--- Generation check test ----"); + + RecordStream data = session.query(customerDataSet.id(999)).execute(); + data.getFirst().ifPresent(keyRecord -> { + int generation = keyRecord.recordOrThrow().generation; + System.out.println(" Read record with generation of " + generation); + session.update(customerDataSet.id(999)) + .bin("gen").setTo(generation) + .ensureGenerationIs(generation) .execute(); - - //AEL: Clean, readable syntax - RecordStream stream = session.query(users) - .where("$.status == 'active' and $.age >= 21") - .execute(); - stream.forEach(res -> { - if (res.isOk()) { - Record row = res.recordOrThrow(); - // Process row - } - }); - stream.close(); - - // -------------------- - // Object mapping - // -------------------- - // Insert then read back a customer with an address - System.out.println("\n--- Object mapping test ----"); - Customer sampleCust = new Customer(999, "sample", 456, new Date(), new Address("123 Main St", "Denver", "CO", "USA", "80112")); - System.out.println("Reference customer: " + sampleCust); - - session.delete(customerDataSet.id(999)).execute(); - session.insert(customerDataSet).object(sampleCust).execute(); - Customer readCustomer = session.query(customerDataSet.id(999)).execute().toObjectList(customerMapper).get(0); - System.out.println("Customer read back: " + readCustomer); - System.out.println("--- End object mapping test ----"); + System.out.println(" First update was successful"); try { - session.query(customerDataSet).where("$.name == 'Tim'") - .bin("fred").get() + // Second update should fail with a generation exception + session.update(customerDataSet) + .object(readCustomer) + .ensureGenerationIs(generation) .execute(); - System.err.println("Expected query with bin operations to throw an exception for now"); + System.out.println(" Second update was successful -- this is an error"); + } - catch (BinOpInvalidException boie) { - // Do nothing, expected. + catch (AerospikeException ae) { + System.out.println(" Second update failed as expected"); + System.out.println(ae.getResultCode() == ResultCode.GENERATION_ERROR); } - // ----------------------- - // Object mapping (nested) - // ----------------------- - Address address1 = new Address("123 Main St", "Denver", "CO", "USA", "80000"); - Customer customer = new Customer(1, "Bob", 37, new Date(), null); - session.replace(customerDataSet).object(customer).using(customerMapper).execute(); - session.upsert(customerDataSet.id(customer.getId())).bin("addrs").onMapKey("home").upsert(address1, new AddressMapper()); - - // ---------------- - // Generation check - // ---------------- - System.out.println("\n--- Generation check test ----"); - - RecordStream data = session.query(customerDataSet.id(999)).execute(); - data.getFirst().ifPresent(keyRecord -> { - int generation = keyRecord.recordOrThrow().generation; - System.out.println(" Read record with generation of " + generation); - session.update(customerDataSet.id(999)) - .bin("gen").setTo(generation) - .ensureGenerationIs(generation) - .execute(); - System.out.println(" First update was successful"); - - try { - // Second update should fail with a generation exception - session.update(customerDataSet) - .object(readCustomer) - .ensureGenerationIs(generation) - .execute(); - System.out.println(" Second update was successful -- this is an error"); - - } - catch (AerospikeException ae) { - System.out.println(" Second update failed as expected"); - System.out.println(ae.getResultCode() == ResultCode.GENERATION_ERROR); - } - }); - - // ---------------------------- - // Complex CDT operations - // ---------------------------- - System.out.println("\n--- Complex CDT operations ---"); - - session.delete(customerDataSet.id(500)).execute(); - session.upsert(customerDataSet.id(500)) - .bin("name").setTo("CDT-Test") - .bin("scores").setTo(List.of(95, 82, 73, 88, 91)) - .bin("tags").setTo(List.of("java", "python", "rust")) - .bin("inventory").setTo(Map.of( - "apples", 10, - "bananas", 5, - "cherries", 20 - )) - .bin("nested").setTo(Map.of( - "team1", Map.of("members", List.of("Alice", "Bob", "Charlie")), - "team2", Map.of("members", List.of("Dave", "Eve")) - )) - .execute(); - - // --- Read-only operations via query path (top-level) --- - - RecordStream cdtResults = session.query(customerDataSet.id(500)) - .bin("scores").listSize() - .execute(); - System.out.println("List size of 'scores': " + cdtResults.getFirst()); - - cdtResults = session.query(customerDataSet.id(500)) - .bin("inventory").mapSize() - .execute(); - System.out.println("Map size of 'inventory': " + cdtResults.getFirst()); - - cdtResults = session.query(customerDataSet.id(500)) - .bin("scores").listGet(0) - .execute(); - System.out.println("First score: " + cdtResults.getFirst()); - - cdtResults = session.query(customerDataSet.id(500)) - .bin("scores").listGetRange(1, 3) - .execute(); - System.out.println("Scores [1..3]: " + cdtResults.getFirst()); - - cdtResults = session.query(customerDataSet.id(500)) - .bin("scores").listGetRange(3) - .execute(); - System.out.println("Scores from index 3 onward: " + cdtResults.getFirst()); - - // --- Read-only operations via query path with CDT navigation --- - - cdtResults = session.query(customerDataSet.id(500)) - .bin("nested").onMapKey("team1").onMapKey("members").listSize() - .execute(); - System.out.println("Team1 member count: " + cdtResults.getFirst()); - - cdtResults = session.query(customerDataSet.id(500)) - .bin("nested").onMapKey("team1").onMapKey("members").listGet(1) - .execute(); - System.out.println("Team1 second member: " + cdtResults.getFirst()); - - cdtResults = session.query(customerDataSet.id(500)) - .bin("nested").onMapKey("team2").onMapKey("members").listGetRange(0, 2) - .execute(); - System.out.println("Team2 members [0..1]: " + cdtResults.getFirst()); - - // --- Write operations: list mutations --- - - session.update(customerDataSet.id(500)) - .bin("scores").listAppendItems(List.of(77, 65, 99)) - .execute(); - System.out.println("After listAppendItems: " + - session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("tags").listInsert(1, "go") - .execute(); - System.out.println("After listInsert(1, 'go'): " + - session.query(customerDataSet.id(500)).bin("tags").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("tags").listSet(0, "kotlin") - .execute(); - System.out.println("After listSet(0, 'kotlin'): " + - session.query(customerDataSet.id(500)).bin("tags").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("scores").listInsertItems(2, List.of(100, 200)) - .execute(); - System.out.println("After listInsertItems(2, [100, 200]): " + - session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("scores").listIncrement(0, 5) - .execute(); - System.out.println("After listIncrement(0, 5): " + - session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("scores").listSort() - .execute(); - System.out.println("After listSort: " + - session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("scores").listRemove(0) - .execute(); - System.out.println("After listRemove(0): " + - session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("scores").listRemoveRange(4, 2) - .execute(); - System.out.println("After listRemoveRange(4, 2): " + - session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); - - cdtResults = session.update(customerDataSet.id(500)) - .bin("scores").listPop(0) - .execute(); - System.out.println("Popped element: " + cdtResults.getFirst()); - - session.update(customerDataSet.id(500)) - .bin("scores").listTrim(0, 3) - .execute(); - System.out.println("After listTrim(0, 3): " + - session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("tags").listClear() - .execute(); - System.out.println("After listClear on tags: " + - session.query(customerDataSet.id(500)).bin("tags").get().execute().getFirst()); - - // --- Write operations: map mutations --- - - session.update(customerDataSet.id(500)) - .bin("inventory").mapUpsertItems(Map.of("dates", 15, "elderberries", 8)) - .execute(); - System.out.println("After mapUpsertItems: " + - session.query(customerDataSet.id(500)).bin("inventory").get().execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("inventory").mapSetPolicy(MapOrder.KEY_ORDERED) - .execute(); - System.out.println("After mapSetPolicy(KEY_ORDERED): " + - session.query(customerDataSet.id(500)).bin("inventory").get().execute().getFirst()); - - // --- Write operations: CDT navigation with new methods --- - - session.update(customerDataSet.id(500)) - .bin("nested").onMapKey("team1").onMapKey("members").listAppend("Diana") - .execute(); - System.out.println("After nested listAppend('Diana') to team1: " + - session.query(customerDataSet.id(500)) - .bin("nested").onMapKey("team1").onMapKey("members").listSize() - .execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("nested").onMapKey("team2").onMapKey("members").listInsert(0, "Zara") - .execute(); - System.out.println("After nested listInsert to team2: " + - session.query(customerDataSet.id(500)) - .bin("nested").onMapKey("team2").onMapKey("members").listGetRange(0) - .execute().getFirst()); - - session.update(customerDataSet.id(500)) - .bin("nested").onMapKey("team1").onMapKey("members").listSort() - .execute(); - System.out.println("After sorting team1 members: " + - session.query(customerDataSet.id(500)) - .bin("nested").onMapKey("team1").onMapKey("members").listGetRange(0) - .execute().getFirst()); - - // Create a new ordered list sub-element via CDT navigation - session.update(customerDataSet.id(500)) - .bin("nested").onMapKey("team3", MapOrder.KEY_ORDERED) - .onMapKey("members").listCreate(ListOrder.ORDERED) - .execute(); - session.update(customerDataSet.id(500)) - .bin("nested").onMapKey("team3").onMapKey("members").listAddItems(List.of("Ivy", "Frank", "Grace")) - .execute(); - System.out.println("Team3 members (ordered): " + - session.query(customerDataSet.id(500)) - .bin("nested").onMapKey("team3").onMapKey("members").listGetRange(0) - .execute().getFirst()); - - // --- Combined multi-bin CDT operations in one call --- + }); + } - cdtResults = session.update(customerDataSet.id(500)) - .bin("scores").listAppendItems(List.of(50, 60, 70)) - .bin("inventory").mapUpsertItems(Map.of("figs", 12)) - .bin("nested").onMapKey("team2").onMapKey("members").listAppend("Quinn") + private void demonstrateComplexCdt(Session session, TypeSafeDataSet customerDataSet) { + // ---------------------------- + // Complex CDT operations + // ---------------------------- + System.out.println("\n--- Complex CDT operations ---"); + + session.delete(customerDataSet.id(500)).execute(); + session.upsert(customerDataSet.id(500)) + .bin("name").setTo("CDT-Test") + .bin("scores").setTo(List.of(95, 82, 73, 88, 91)) + .bin("tags").setTo(List.of("java", "python", "rust")) + .bin("inventory").setTo(Map.of( + "apples", 10, + "bananas", 5, + "cherries", 20 + )) + .bin("nested").setTo(Map.of( + "team1", Map.of("members", List.of("Alice", "Bob", "Charlie")), + "team2", Map.of("members", List.of("Dave", "Eve")) + )) + .execute(); + + // --- Read-only operations via query path (top-level) --- + + RecordStream cdtResults = session.query(customerDataSet.id(500)) + .bin("scores").listSize() + .execute(); + System.out.println("List size of 'scores': " + cdtResults.getFirst()); + + cdtResults = session.query(customerDataSet.id(500)) + .bin("inventory").mapSize() + .execute(); + System.out.println("Map size of 'inventory': " + cdtResults.getFirst()); + + cdtResults = session.query(customerDataSet.id(500)) + .bin("scores").listGet(0) + .execute(); + System.out.println("First score: " + cdtResults.getFirst()); + + cdtResults = session.query(customerDataSet.id(500)) + .bin("scores").listGetRange(1, 3) + .execute(); + System.out.println("Scores [1..3]: " + cdtResults.getFirst()); + + cdtResults = session.query(customerDataSet.id(500)) + .bin("scores").listGetRange(3) + .execute(); + System.out.println("Scores from index 3 onward: " + cdtResults.getFirst()); + + // --- Read-only operations via query path with CDT navigation --- + + cdtResults = session.query(customerDataSet.id(500)) + .bin("nested").onMapKey("team1").onMapKey("members").listSize() + .execute(); + System.out.println("Team1 member count: " + cdtResults.getFirst()); + + cdtResults = session.query(customerDataSet.id(500)) + .bin("nested").onMapKey("team1").onMapKey("members").listGet(1) + .execute(); + System.out.println("Team1 second member: " + cdtResults.getFirst()); + + cdtResults = session.query(customerDataSet.id(500)) + .bin("nested").onMapKey("team2").onMapKey("members").listGetRange(0, 2) + .execute(); + System.out.println("Team2 members [0..1]: " + cdtResults.getFirst()); + + // --- Write operations: list mutations --- + + session.update(customerDataSet.id(500)) + .bin("scores").listAppendItems(List.of(77, 65, 99)) + .execute(); + System.out.println("After listAppendItems: " + + session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("tags").listInsert(1, "go") + .execute(); + System.out.println("After listInsert(1, 'go'): " + + session.query(customerDataSet.id(500)).bin("tags").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("tags").listSet(0, "kotlin") + .execute(); + System.out.println("After listSet(0, 'kotlin'): " + + session.query(customerDataSet.id(500)).bin("tags").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("scores").listInsertItems(2, List.of(100, 200)) + .execute(); + System.out.println("After listInsertItems(2, [100, 200]): " + + session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("scores").listIncrement(0, 5) + .execute(); + System.out.println("After listIncrement(0, 5): " + + session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("scores").listSort() + .execute(); + System.out.println("After listSort: " + + session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("scores").listRemove(0) + .execute(); + System.out.println("After listRemove(0): " + + session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("scores").listRemoveRange(4, 2) + .execute(); + System.out.println("After listRemoveRange(4, 2): " + + session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); + + cdtResults = session.update(customerDataSet.id(500)) + .bin("scores").listPop(0) + .execute(); + System.out.println("Popped element: " + cdtResults.getFirst()); + + session.update(customerDataSet.id(500)) + .bin("scores").listTrim(0, 3) + .execute(); + System.out.println("After listTrim(0, 3): " + + session.query(customerDataSet.id(500)).bin("scores").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("tags").listClear() + .execute(); + System.out.println("After listClear on tags: " + + session.query(customerDataSet.id(500)).bin("tags").get().execute().getFirst()); + + // --- Write operations: map mutations --- + + session.update(customerDataSet.id(500)) + .bin("inventory").mapUpsertItems(Map.of("dates", 15, "elderberries", 8)) + .execute(); + System.out.println("After mapUpsertItems: " + + session.query(customerDataSet.id(500)).bin("inventory").get().execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("inventory").mapSetPolicy(MapOrder.KEY_ORDERED) + .execute(); + System.out.println("After mapSetPolicy(KEY_ORDERED): " + + session.query(customerDataSet.id(500)).bin("inventory").get().execute().getFirst()); + + // --- Write operations: CDT navigation with new methods --- + + session.update(customerDataSet.id(500)) + .bin("nested").onMapKey("team1").onMapKey("members").listAppend("Diana") + .execute(); + System.out.println("After nested listAppend('Diana') to team1: " + + session.query(customerDataSet.id(500)) .bin("nested").onMapKey("team1").onMapKey("members").listSize() - .execute(); - System.out.println("Combined CDT result: " + cdtResults.getFirst()); - - System.out.println("Final state: " + - session.query(customerDataSet.id(500)).execute().getFirst()); - System.out.println("--- End Complex CDT operations ---"); - - session.upsert(customerDataSet.id(1)) - .bin("test").onMapKeyRange(5, SpecialValue.INFINITY).getKeys() - .execute(); - } + .execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("nested").onMapKey("team2").onMapKey("members").listInsert(0, "Zara") + .execute(); + System.out.println("After nested listInsert to team2: " + + session.query(customerDataSet.id(500)) + .bin("nested").onMapKey("team2").onMapKey("members").listGetRange(0) + .execute().getFirst()); + + session.update(customerDataSet.id(500)) + .bin("nested").onMapKey("team1").onMapKey("members").listSort() + .execute(); + System.out.println("After sorting team1 members: " + + session.query(customerDataSet.id(500)) + .bin("nested").onMapKey("team1").onMapKey("members").listGetRange(0) + .execute().getFirst()); + + // Create a new ordered list sub-element via CDT navigation + session.update(customerDataSet.id(500)) + .bin("nested").onMapKey("team3", MapOrder.KEY_ORDERED) + .onMapKey("members").listCreate(ListOrder.ORDERED) + .execute(); + session.update(customerDataSet.id(500)) + .bin("nested").onMapKey("team3").onMapKey("members").listAddItems(List.of("Ivy", "Frank", "Grace")) + .execute(); + System.out.println("Team3 members (ordered): " + + session.query(customerDataSet.id(500)) + .bin("nested").onMapKey("team3").onMapKey("members").listGetRange(0) + .execute().getFirst()); + + // --- Combined multi-bin CDT operations in one call --- + + cdtResults = session.update(customerDataSet.id(500)) + .bin("scores").listAppendItems(List.of(50, 60, 70)) + .bin("inventory").mapUpsertItems(Map.of("figs", 12)) + .bin("nested").onMapKey("team2").onMapKey("members").listAppend("Quinn") + .bin("nested").onMapKey("team1").onMapKey("members").listSize() + .execute(); + System.out.println("Combined CDT result: " + cdtResults.getFirst()); + + System.out.println("Final state: " + + session.query(customerDataSet.id(500)).execute().getFirst()); + System.out.println("--- End Complex CDT operations ---"); + + session.upsert(customerDataSet.id(1)) + .bin("test").onMapKeyRange(5, SpecialValue.INFINITY).getKeys() + .execute(); } } diff --git a/examples/src/main/java/com/aerospike/examples/RosterExample.java b/examples/src/main/java/com/aerospike/examples/RosterExample.java index b1268f92..e77fa759 100644 --- a/examples/src/main/java/com/aerospike/examples/RosterExample.java +++ b/examples/src/main/java/com/aerospike/examples/RosterExample.java @@ -44,26 +44,28 @@ * * */ -public class RosterExample extends Example{ - - public RosterExample(Console console) { - super(console); - } +public class RosterExample extends Example { @Override - public void runExample(Cluster cluster, Args args) throws Exception { + public void runExample() throws Exception { + Cluster cluster = cluster(); Node[] nodes = cluster.getNodes(); - String response = Info.request(nodes[0], format("roster:namespace=%s", args.namespace)); + String response = Info.request(nodes[0], format("roster:namespace=%s", namespace())); System.out.printf("Current roster: %s\n", response); + if (response == null || response.toLowerCase().contains("error") || !response.contains("observed_nodes=")) { + throw new ExampleSkipException( + "Roster commands require a strong-consistency namespace with observed_nodes; response: " + response); + } + // Parse observed nodes from response String observedNodes = parseObservedNodes(response); System.out.printf("Observed nodes: %s\n", observedNodes); for (Node node : nodes) { String setResponse = Info.request(node, - format("roster-set:namespace=%s;nodes=%s", args.namespace, observedNodes)); + format("roster-set:namespace=%s;nodes=%s", namespace(), observedNodes)); System.out.printf("roster-set on %s: %s\n", node.getHost(), setResponse); } diff --git a/examples/src/main/java/com/aerospike/examples/StudentScoresExample.java b/examples/src/main/java/com/aerospike/examples/StudentScoresExample.java index d5ca7f03..168762d3 100644 --- a/examples/src/main/java/com/aerospike/examples/StudentScoresExample.java +++ b/examples/src/main/java/com/aerospike/examples/StudentScoresExample.java @@ -4,16 +4,14 @@ import java.util.Map; import java.util.Random; -import com.aerospike.client.sdk.Cluster; -import com.aerospike.client.sdk.ClusterDefinition; import com.aerospike.client.sdk.DataSet; import com.aerospike.client.sdk.Session; import com.aerospike.client.sdk.policy.Behavior; -public class StudentScoresExample { +public class StudentScoresExample extends Example { private static final String[] SUBJECTS = {"math", "english", "science", "history", "art"}; - private static final Random random = new Random(42); // fixed seed for reproducibility - private static Map generateScores(int studentNum) { + + private static Map generateScores(Random random) { Map scores = new HashMap<>(); for (String subject : SUBJECTS) { scores.put(subject, 55 + random.nextInt(46)); // scores between 55 and 100 @@ -21,30 +19,26 @@ private static Map generateScores(int studentNum) { return scores; } - public static void main(String[] args) { - // -- Connect -- - try (Cluster cluster = new ClusterDefinition("localhost", 3100) - .withNativeCredentials("admin", "admin123") - .connect()) { - - Session session = cluster.createSession(Behavior.DEFAULT); - DataSet class10a = DataSet.of("test", "class10a"); - // -- Write 30 student records -- - for (int i = 1; i <= 30; i++) { - session.upsert(class10a.id("student-" + i)) - .bin("name").setTo("Student " + i) - .bin("scores").setTo(generateScores(i)) - .execute(); - } - - // -- Query: students with any score >= 90 -- - session.query(class10a) - .where("$.scores.{=90:}.count() > 0") - .execute() - .forEach(r -> System.out.printf("%s: %s%n", - r.recordOrThrow().getString("name"), - r.recordOrThrow().getMap("scores"))); + @Override + public void runExample() { + Session session = cluster().createSession(Behavior.DEFAULT); + DataSet class10a = dataSet("class10a"); + Random random = new Random(42); + // -- Write 30 student records -- + for (int i = 1; i <= 30; i++) { + session.upsert(class10a.id("student-" + i)) + .bin("name").setTo("Student " + i) + .bin("scores").setTo(generateScores(random)) + .execute(); } + + // -- Query: students with any score >= 90 -- + session.query(class10a) + .where("$.scores.{=90:}.count() > 0") + .execute() + .forEach(r -> System.out.printf("%s: %s%n", + r.recordOrThrow().getString("name"), + r.recordOrThrow().getMap("scores"))); } } diff --git a/examples/src/main/java/com/aerospike/examples/TransactionProcessingExample.java b/examples/src/main/java/com/aerospike/examples/TransactionProcessingExample.java index f59e40c1..62a6f733 100644 --- a/examples/src/main/java/com/aerospike/examples/TransactionProcessingExample.java +++ b/examples/src/main/java/com/aerospike/examples/TransactionProcessingExample.java @@ -18,8 +18,6 @@ import java.util.Map; -import com.aerospike.client.sdk.Cluster; -import com.aerospike.client.sdk.ClusterDefinition; import com.aerospike.client.sdk.Key; import com.aerospike.client.sdk.RecordMapper; import com.aerospike.client.sdk.Session; @@ -27,7 +25,7 @@ import com.aerospike.client.sdk.policy.Behavior; import com.aerospike.client.sdk.util.MapUtil; -public class TransactionProcessingExample { +public class TransactionProcessingExample extends Example { public static class Transaction { private String id; private String desc; @@ -182,113 +180,112 @@ public Object id(Account acct) { } } - public static void main(String[] args) { - try (Cluster cluster = new ClusterDefinition("localhost", 3100).connect()) { - Session session = cluster.createSession(Behavior.DEFAULT); + @Override + public void runExample() { + Session session = cluster().createSession(Behavior.DEFAULT); - TypeSafeDataSet customerDataSet = TypeSafeDataSet.of("test", "customers", Customer.class); - TypeSafeDataSet accountDataSet = TypeSafeDataSet.of("test", "accounts", Account.class); - TypeSafeDataSet txnDataSet = TypeSafeDataSet.of("test", "txns", Transaction.class); + TypeSafeDataSet customerDataSet = TypeSafeDataSet.of(namespace(), "customers", Customer.class); + TypeSafeDataSet accountDataSet = TypeSafeDataSet.of(namespace(), "accounts", Account.class); + TypeSafeDataSet txnDataSet = TypeSafeDataSet.of(namespace(), "txns", Transaction.class); - CustomerMapper customerMapper = new CustomerMapper(); - AccountMapper accountMapper = new AccountMapper(); - TransactionMapper txnMapper = new TransactionMapper(); + CustomerMapper customerMapper = new CustomerMapper(); + AccountMapper accountMapper = new AccountMapper(); + TransactionMapper txnMapper = new TransactionMapper(); - Customer customer = new Customer(); - customer.setCustomerId("CUST-10042"); - customer.setFirstName("Jane"); - customer.setLastName("Morrison"); - customer.setEmail("jane.morrison@example.com"); - customer.setPhone("+1-555-867-5309"); - customer.setTotalSpendInCents(0); - customer.setStatusLevel("BRONZE"); + Customer customer = new Customer(); + customer.setCustomerId("CUST-10042"); + customer.setFirstName("Jane"); + customer.setLastName("Morrison"); + customer.setEmail("jane.morrison@example.com"); + customer.setPhone("+1-555-867-5309"); + customer.setTotalSpendInCents(0); + customer.setStatusLevel("BRONZE"); - session.insert(customerDataSet) - .object(customer) - .using(customerMapper) - .execute(); + session.insert(customerDataSet) + .object(customer) + .using(customerMapper) + .execute(); - Account account = new Account(); - account.setPan("4532015112830366"); - account.setCustomerId(customer.getCustomerId()); - account.setExpiryDate("03/28"); - account.setBalanceInCents(0); - account.setCreditLimitInCents(500000); - account.setStatus("ACTIVE"); + Account account = new Account(); + account.setPan("4532015112830366"); + account.setCustomerId(customer.getCustomerId()); + account.setExpiryDate("03/28"); + account.setBalanceInCents(0); + account.setCreditLimitInCents(500000); + account.setStatus("ACTIVE"); - session.insert(accountDataSet) - .object(account) - .using(accountMapper) - .execute(); + session.insert(accountDataSet) + .object(account) + .using(accountMapper) + .execute(); - Transaction txn = new Transaction(); - txn.setId("TXN-00001"); - txn.setDesc("Car repairs"); - txn.setAmountInCents(45000); - txn.setDate(System.currentTimeMillis()); + Transaction txn = new Transaction(); + txn.setId("TXN-00001"); + txn.setDesc("Car repairs"); + txn.setAmountInCents(45000); + txn.setDate(System.currentTimeMillis()); - // ====================================================================== - // Old style: Standard Aerospike Java Client - // ====================================================================== - // - // Key txnKey = new Key("test", "txns", txn.getId()); - // Key accountKey = new Key("test", "accounts", account.getPan()); - // Key customerKey = new Key("test", "customers", customer.getCustomerId()); - // - // BatchWritePolicy insertPolicy = new BatchWritePolicy(); - // insertPolicy.recordExistsAction = RecordExistsAction.CREATE_ONLY; - // - // BatchWritePolicy updatePolicy = new BatchWritePolicy(); - // updatePolicy.recordExistsAction = RecordExistsAction.UPDATE_ONLY; - // - // Expression statusExp = Exp.build( - // Exp.cond( - // Exp.gt(Exp.intBin("totalSpend"), Exp.val(100000)), Exp.val("PLATINUM"), - // Exp.gt(Exp.intBin("totalSpend"), Exp.val(10000)), Exp.val("GOLD"), - // Exp.gt(Exp.intBin("totalSpend"), Exp.val(100)), Exp.val("SILVER"), - // Exp.val("BRONZE") - // ) - // ); - // - // List batchRecords = new ArrayList<>(); - // - // batchRecords.add(new BatchWrite( - // insertPolicy, txnKey, - // Operation.put(new Bin("id", txn.getId())), - // Operation.put(new Bin("desc", txn.getDesc())), - // Operation.put(new Bin("amountInCents", txn.getAmountInCents())), - // Operation.put(new Bin("date", txn.getDate())) - // )); - // - // batchRecords.add(new BatchWrite( - // updatePolicy, accountKey, - // Operation.add(new Bin("balanceCents", txn.getAmountInCents())) - // )); - // - // batchRecords.add(new BatchWrite( - // updatePolicy, customerKey, - // Operation.add(new Bin("totalSpend", txn.getAmountInCents())), - // ExpOperation.write("statusLevel", statusExp, ExpWriteFlags.DEFAULT) - // )); - // - // client.operate(new BatchPolicy(), batchRecords); + // ====================================================================== + // Old style: Standard Aerospike Java Client + // ====================================================================== + // + // Key txnKey = new Key("test", "txns", txn.getId()); + // Key accountKey = new Key("test", "accounts", account.getPan()); + // Key customerKey = new Key("test", "customers", customer.getCustomerId()); + // + // BatchWritePolicy insertPolicy = new BatchWritePolicy(); + // insertPolicy.recordExistsAction = RecordExistsAction.CREATE_ONLY; + // + // BatchWritePolicy updatePolicy = new BatchWritePolicy(); + // updatePolicy.recordExistsAction = RecordExistsAction.UPDATE_ONLY; + // + // Expression statusExp = Exp.build( + // Exp.cond( + // Exp.gt(Exp.intBin("totalSpend"), Exp.val(100000)), Exp.val("PLATINUM"), + // Exp.gt(Exp.intBin("totalSpend"), Exp.val(10000)), Exp.val("GOLD"), + // Exp.gt(Exp.intBin("totalSpend"), Exp.val(100)), Exp.val("SILVER"), + // Exp.val("BRONZE") + // ) + // ); + // + // List batchRecords = new ArrayList<>(); + // + // batchRecords.add(new BatchWrite( + // insertPolicy, txnKey, + // Operation.put(new Bin("id", txn.getId())), + // Operation.put(new Bin("desc", txn.getDesc())), + // Operation.put(new Bin("amountInCents", txn.getAmountInCents())), + // Operation.put(new Bin("date", txn.getDate())) + // )); + // + // batchRecords.add(new BatchWrite( + // updatePolicy, accountKey, + // Operation.add(new Bin("balanceCents", txn.getAmountInCents())) + // )); + // + // batchRecords.add(new BatchWrite( + // updatePolicy, customerKey, + // Operation.add(new Bin("totalSpend", txn.getAmountInCents())), + // ExpOperation.write("statusLevel", statusExp, ExpWriteFlags.DEFAULT) + // )); + // + // client.operate(new BatchPolicy(), batchRecords); - // ====================================================================== - // New style: API - // ====================================================================== - session - .insert(txnDataSet) - .object(txn) - .using(txnMapper) - .update(accountDataSet.id(account.getPan())) - .bin("balanceCents").add(txn.getAmountInCents()) - .update(customerDataSet.id(customer.getCustomerId())) - .bin("totalSpend").add(txn.getAmountInCents()) - .bin("statusLevel").upsertFrom("when ($.totalSpend > 100000 => 'PLATINUM', " - + "$.totalSpend > 10000 => 'GOLD', " - + "$.totalSpend > 100 => 'SILVER', " - + "default => 'BRONZE')") - .execute(); - } + // ====================================================================== + // New style: API + // ====================================================================== + session + .insert(txnDataSet) + .object(txn) + .using(txnMapper) + .update(accountDataSet.id(account.getPan())) + .bin("balanceCents").add(txn.getAmountInCents()) + .update(customerDataSet.id(customer.getCustomerId())) + .bin("totalSpend").add(txn.getAmountInCents()) + .bin("statusLevel").upsertFrom("when ($.totalSpend > 100000 => 'PLATINUM', " + + "$.totalSpend > 10000 => 'GOLD', " + + "$.totalSpend > 100 => 'SILVER', " + + "default => 'BRONZE')") + .execute(); } } diff --git a/examples/src/main/java/com/aerospike/examples/YamlConfigConnectionExample.java b/examples/src/main/java/com/aerospike/examples/YamlConfigConnectionExample.java index 08f1d4cb..01d0255f 100644 --- a/examples/src/main/java/com/aerospike/examples/YamlConfigConnectionExample.java +++ b/examples/src/main/java/com/aerospike/examples/YamlConfigConnectionExample.java @@ -24,7 +24,6 @@ import com.aerospike.client.sdk.Session; import com.aerospike.client.sdk.policy.Behavior; import com.aerospike.client.sdk.policy.ResolvedSettings; -import com.aerospike.client.sdk.util.Util; /** * Example demonstrating how to connect to an Aerospike cluster using a custom @@ -71,12 +70,8 @@ public class YamlConfigConnectionExample extends Example { private static final String ENV_CONFIG_URL = "AEROSPIKE_CLIENT_CONFIG_URL"; - public YamlConfigConnectionExample(Console console) { - super(console); - } - @Override - public void runExample(Cluster clusterNotUsed, Args args) throws Exception { + public void runExample() throws Exception { // Check if the configuration environment variable is set String configPath = System.getenv(ENV_CONFIG_URL); @@ -92,20 +87,19 @@ public void runExample(Cluster clusterNotUsed, Args args) throws Exception { console.write("The client will load behaviors and settings from this file.\n"); } - console.write("Connection settings: " + args); - console.write("Connecting to Aerospike cluster at " + args.host + ":" + args.port); - if (args.useServicesAlternate) { + console.write("Connecting to Aerospike cluster at " + host() + ":" + port()); + if (useServicesAlternate()) { console.write("Using alternate services for cluster discovery"); } // Create cluster definition // When connect() is called, it automatically checks for AEROSPIKE_CLIENT_CONFIG_URL // and loads the configuration if the environment variable is set - ClusterDefinition clusterDef = new ClusterDefinition(args.host, args.port) + ClusterDefinition clusterDef = new ClusterDefinition(host(), port()) .appId("yaml-config-example") .failIfNotConnected(true); - if (args.useServicesAlternate) { + if (useServicesAlternate()) { clusterDef.usingServicesAlternate(); } @@ -122,14 +116,11 @@ public void runExample(Cluster clusterNotUsed, Args args) throws Exception { console.write("Using behavior: " + behavior.name() + "\n"); // Perform example operations - performExampleOperations(session, args); + performExampleOperations(session); // Demonstrate using different behaviors for different operations demonstrateBehaviorSwitching(cluster); - } catch (Throwable t) { - console.error("Error: " + Util.getErrorMessage(t)); - t.printStackTrace(); } finally { // Clean up monitoring if it was started if (Behavior.isMonitoring()) { @@ -185,10 +176,10 @@ private Behavior getBehaviorOrDefault(String behaviorName) { /** * Perform example CRUD operations using the session */ - private void performExampleOperations(Session session, Args args) { + private void performExampleOperations(Session session) throws Exception { console.write("=== Performing Example Operations ==="); - DataSet dataSet = DataSet.of(args.namespace, "yaml-config-demo"); + DataSet dataSet = dataSet("yaml-config-demo"); try { // Write a record @@ -223,13 +214,9 @@ private void performExampleOperations(Session session, Args args) { var existsResults = session.exists(dataSet.ids("user-001", "user-002", "user-999")).execute(); console.write(" Exists results: " + existsResults); - // Clean up - delete test records - console.write("Cleaning up test records..."); - session.delete(dataSet.ids("user-001", "user-002", "user-003", "user-004")).execute(); - console.write(" Records deleted."); - } catch (Exception e) { console.error("Operation failed: " + e.getMessage()); + throw e; } console.write(""); diff --git a/examples/src/main/java/com/aerospike/examples/YamlConfigExample.java b/examples/src/main/java/com/aerospike/examples/YamlConfigExample.java index 4130e5cb..2d309ddf 100644 --- a/examples/src/main/java/com/aerospike/examples/YamlConfigExample.java +++ b/examples/src/main/java/com/aerospike/examples/YamlConfigExample.java @@ -17,13 +17,13 @@ package com.aerospike.examples; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; -import com.aerospike.client.sdk.Cluster; import com.aerospike.client.sdk.SystemSettingsRegistry; import com.aerospike.client.sdk.policy.Behavior; import com.aerospike.client.sdk.policy.BehaviorYamlLoader; @@ -53,12 +53,8 @@ */ public class YamlConfigExample extends Example { - public YamlConfigExample(Console console) { - super(console); - } - @Override - public void runExample(Cluster clusterNotUsed, Args args) throws Exception { + public void runExample() throws Exception { // Example 1: Load from a file loadFromFile(); @@ -78,21 +74,21 @@ private void loadFromFile() throws IOException { System.out.println("=== Loading from file ==="); File configFile = new File("src/main/resources/example-config.yml"); - if (configFile.exists()) { - Map behaviors = BehaviorYamlLoader.loadBehaviorsFromFile(configFile); + if (!configFile.exists()) { + throw new FileNotFoundException("Config file not found: " + configFile.getAbsolutePath()); + } - System.out.println("Loaded " + behaviors.size() + " behaviors:"); - for (String name : behaviors.keySet()) { - System.out.println(" - " + name); - } + Map behaviors = BehaviorYamlLoader.loadBehaviorsFromFile(configFile); - // System settings are automatically registered in SystemSettingsRegistry - SystemSettingsRegistry registry = SystemSettingsRegistry.getInstance(); - System.out.println("Default system settings: " + registry.getDefaultSettings()); - System.out.println("Production cluster settings: " + registry.getClusterSettings("production")); - } else { - System.out.println("Config file not found: " + configFile.getAbsolutePath()); + System.out.println("Loaded " + behaviors.size() + " behaviors:"); + for (String name : behaviors.keySet()) { + System.out.println(" - " + name); } + + // System settings are automatically registered in SystemSettingsRegistry + SystemSettingsRegistry registry = SystemSettingsRegistry.getInstance(); + System.out.println("Default system settings: " + registry.getDefaultSettings()); + System.out.println("Production cluster settings: " + registry.getClusterSettings("production")); } /** @@ -186,14 +182,14 @@ private void useWithCluster() throws IOException { // Example of how you would use it with a real cluster: /* - try (Cluster cluster = new ClusterDefinition("localhost", 3000) + try (Cluster cluster = new ClusterDefinition(host(), port()) .connect()) { // Create a session using the loaded behavior Session session = cluster.createSession(productionBehavior); // Use the session for operations - DataSet myDataSet = DataSet.of("test", "mySet"); + DataSet myDataSet = dataSet("mySet"); session.upsert(myDataSet.id(1)) .bin("name").setTo("example") .execute(); diff --git a/examples/src/main/java/com/aerospike/examples/ecommerce/EcommerceExample.java b/examples/src/main/java/com/aerospike/examples/ecommerce/EcommerceExample.java index 3617d9ee..3fbb6e17 100644 --- a/examples/src/main/java/com/aerospike/examples/ecommerce/EcommerceExample.java +++ b/examples/src/main/java/com/aerospike/examples/ecommerce/EcommerceExample.java @@ -22,8 +22,6 @@ import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicInteger; -import com.aerospike.client.sdk.Cluster; -import com.aerospike.client.sdk.ClusterDefinition; import com.aerospike.client.sdk.ErrorStrategy; import com.aerospike.client.sdk.Key; import com.aerospike.client.sdk.RecordResult; @@ -32,6 +30,7 @@ import com.aerospike.client.sdk.TypeSafeDataSet; import com.aerospike.client.sdk.policy.Behavior; import com.aerospike.client.sdk.task.ExecuteTask; +import com.aerospike.examples.Example; /** * E-commerce order fulfillment example demonstrating CompletableFuture chaining, @@ -41,72 +40,71 @@ * product stock, create the order, and decrement inventory -- all composed with * CompletableFuture. Then we scan products using Flow.Publisher with backpressure.

    */ -public class EcommerceExample { +public class EcommerceExample extends Example { static final CustomerMapper CUSTOMER_MAPPER = new CustomerMapper(); static final ProductMapper PRODUCT_MAPPER = new ProductMapper(); static final OrderMapper ORDER_MAPPER = new OrderMapper(); - public static void main(String[] args) throws Exception { - try (Cluster cluster = new ClusterDefinition("localhost", 3100).connect()) { - Session session = cluster.createSession(Behavior.DEFAULT); - - TypeSafeDataSet customers = TypeSafeDataSet.of("test", "customers", Customer.class); - TypeSafeDataSet products = TypeSafeDataSet.of("test", "products", Product.class); - TypeSafeDataSet orders = TypeSafeDataSet.of("test", "orders", Order.class); - - // ========================================== - // 1. Seed 20 customers, 100 products, and - // 54 orders into Aerospike - // ========================================== - SeedData.seed(session, customers, products, orders); - - // ========================================== - // 2. Place an order using CompletableFuture - // (async lookup -> validate -> batch write) - // ========================================== - placeOrder(session, customers, products, orders, "C-100", "SKU-LAP01", 1); - - // ========================================== - // 3. Demonstrate error handling on a - // non-existent customer - // ========================================== - placeOrderWithErrorHandling(session, customers, products, orders, - "C-MISSING", "SKU-LAP01", 1); - - // ========================================== - // 4. Stream orders for a customer using - // Flow.Publisher with backpressure - // ========================================== - streamOrders(session, orders, "C-100"); - - // ========================================== - // 5. Batch query: top-spender dashboard - // ========================================== - topSpenderDashboard(session, customers, orders); - - // ========================================== - // 6. Map operations: product ratings - // ========================================== - productRatings(session, products); - - // ========================================== - // 7. Scan for affordable, well-stocked - // products using Flow.Publisher - // ========================================== - scanAffordableProducts(session, products); - - // ========================================== - // 8. Background scan: apply sale prices to - // overstocked, cheap products - // ========================================== - applySalePrices(session, products); - - // ========================================== - // 9. Re-display stock after sale prices - // ========================================== - scanAffordableProducts(session, products); - } + @Override + public void runExample() throws Exception { + Session session = cluster().createSession(Behavior.DEFAULT); + + TypeSafeDataSet customers = TypeSafeDataSet.of(namespace(), "customers", Customer.class); + TypeSafeDataSet products = TypeSafeDataSet.of(namespace(), "products", Product.class); + TypeSafeDataSet orders = TypeSafeDataSet.of(namespace(), "orders", Order.class); + + // ========================================== + // 1. Seed 20 customers, 95 products, and + // 54 orders into Aerospike + // ========================================== + SeedData.seed(session, customers, products, orders); + + // ========================================== + // 2. Place an order using CompletableFuture + // (async lookup -> validate -> batch write) + // ========================================== + placeOrder(session, customers, products, orders, "C-100", "SKU-LAP01", 1); + + // ========================================== + // 3. Demonstrate error handling on a + // non-existent customer + // ========================================== + placeOrderWithErrorHandling(session, customers, products, orders, + "C-MISSING", "SKU-LAP01", 1); + + // ========================================== + // 4. Stream orders for a customer using + // Flow.Publisher with backpressure + // ========================================== + streamOrders(session, orders, "C-100"); + + // ========================================== + // 5. Batch query: top-spender dashboard + // ========================================== + topSpenderDashboard(session, customers, orders); + + // ========================================== + // 6. Map operations: product ratings + // ========================================== + productRatings(session, products); + + // ========================================== + // 7. Scan for affordable, well-stocked + // products using Flow.Publisher + // ========================================== + scanAffordableProducts(session, products); + + // ========================================== + // 8. Background scan: apply sale prices to + // overstocked, cheap products + // ========================================== + applySalePrices(session, products); + + // ========================================== + // 9. Re-display stock after sale prices + // ========================================== + scanAffordableProducts(session, products); } // ------------------------------------------------------------------ diff --git a/examples/src/main/java/com/aerospike/examples/fixtures/ExampleAssertions.java b/examples/src/main/java/com/aerospike/examples/fixtures/ExampleAssertions.java new file mode 100644 index 00000000..50c48ada --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/fixtures/ExampleAssertions.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples.fixtures; + +import java.util.List; +import java.util.Objects; + +import com.aerospike.client.sdk.DataSet; +import com.aerospike.client.sdk.Record; +import com.aerospike.client.sdk.RecordResult; +import com.aerospike.client.sdk.RecordStream; +import com.aerospike.client.sdk.Session; + +public final class ExampleAssertions { + private ExampleAssertions() { + } + + public static void truncate(Session session, DataSet dataSet) { + session.truncate(dataSet); + } + + public static long count(Session session, DataSet dataSet) { + long count = 0; + + try (RecordStream stream = session.query(dataSet).execute()) { + while (stream.hasNext()) { + stream.next().orThrow(); + count++; + } + } + + return count; + } + + public static void assertCount(Session session, DataSet dataSet, long expected) { + long actual = count(session, dataSet); + + if (actual != expected) { + throw new AssertionError("Expected " + expected + " records in " + dataSet + " but found " + actual); + } + } + + public static Record assertRecordExists(Session session, DataSet dataSet, Object id) { + try (RecordStream stream = session.query(dataSet.ids(List.of(id))).execute()) { + if (!stream.hasNext()) { + throw new AssertionError("Expected record id " + id + " in " + dataSet + " but no result was returned"); + } + + RecordResult result = stream.next().orThrow(); + Record record = result.recordOrThrow(); + + if (record == null) { + throw new AssertionError("Expected record id " + id + " in " + dataSet + " but record payload was null"); + } + + return record; + } + } + + public static void assertRecordMissing(Session session, DataSet dataSet, Object id) { + try (RecordStream stream = session.query(dataSet.ids(List.of(id))).execute()) { + if (!stream.hasNext()) { + return; + } + + RecordResult result = stream.next(); + + if (result.isOk()) { + throw new AssertionError("Expected record id " + id + " in " + dataSet + " to be missing"); + } + } + } + + public static void assertBinEquals(Session session, DataSet dataSet, Object id, String binName, Object expected) { + Record record = assertRecordExists(session, dataSet, id); + Object actual = record.getValue(binName); + + if (!Objects.equals(expected, actual)) { + throw new AssertionError( + "Expected " + dataSet + " id " + id + " bin " + binName + " to be " + expected + " but found " + actual); + } + } +} diff --git a/examples/src/main/java/com/aerospike/examples/fixtures/ExampleFixtures.java b/examples/src/main/java/com/aerospike/examples/fixtures/ExampleFixtures.java new file mode 100644 index 00000000..f5020ed7 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/fixtures/ExampleFixtures.java @@ -0,0 +1,263 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples.fixtures; + +import java.util.List; +import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Function; + +import com.aerospike.client.sdk.AerospikeException; +import com.aerospike.client.sdk.DataSet; +import com.aerospike.client.sdk.Session; +import com.aerospike.examples.ExampleContext; +import com.aerospike.examples.ExampleFixture; + +public final class ExampleFixtures { + private static final String COMMON_INDEX_NAME = "ageidx"; + + private ExampleFixtures() { + } + + // ===================================================================== + // Generic builders + // + // Most fixtures follow the same shape: truncate one or more sets before + // and after the example, then assert something in verify. These builders + // capture that shape so each fixture only has to express what varies (the + // sets it owns and the verification), keeping the doc-facing examples free + // of setup/teardown noise. + // ===================================================================== + + /** + * Fixture that truncates the sets resolved from {@code sets} before and after the example + * runs, and executes {@code verify} once the example completes. + */ + private static ExampleFixture truncating( + Function> sets, + Consumer verify + ) { + return new ExampleFixture() { + @Override + public void setup(ExampleContext context) { + truncateAll(context, sets); + } + + @Override + public void verify(ExampleContext context) { + verify.accept(context); + } + + @Override + public void cleanup(ExampleContext context) { + truncateAll(context, sets); + } + }; + } + + /** Convenience builder for a fixture that owns a single named set. */ + private static ExampleFixture truncating(String set, Consumer verify) { + return truncating(context -> List.of(context.dataSet(set)), verify); + } + + private static void truncateAll(ExampleContext context, Function> sets) { + Session session = context.session(); + + for (DataSet dataSet : sets.apply(context)) { + ExampleAssertions.truncate(session, dataSet); + } + } + + // ===================================================================== + // Fixtures + // ===================================================================== + + public static ExampleFixture batchExample() { + return truncating( + context -> List.of(context.dataSet()), + context -> { + Session session = context.session(); + DataSet dataSet = context.dataSet(); + + ExampleAssertions.assertCount(session, dataSet, 7); + ExampleAssertions.assertRecordMissing(session, dataSet, 1); + ExampleAssertions.assertBinEquals(session, dataSet, 2, "value", 15L); + ExampleAssertions.assertBinEquals(session, dataSet, 6, "name", "Wilma"); + }); + } + + public static ExampleFixture commonExample() { + // Not expressed via truncating(...) because this example also creates a + // secondary index that must be dropped in setup and cleanup. + return new ExampleFixture() { + @Override + public void setup(ExampleContext context) { + Session session = context.session(); + DataSet dataSet = context.dataSet(); + + dropIndexIfExists(session, dataSet, COMMON_INDEX_NAME); + ExampleAssertions.truncate(session, dataSet); + } + + @Override + public void verify(ExampleContext context) { + Session session = context.session(); + DataSet dataSet = context.dataSet(); + + ExampleAssertions.assertRecordMissing(session, dataSet, 118); + ExampleAssertions.assertBinEquals(session, dataSet, 1, "name", "Tim"); + ExampleAssertions.assertBinEquals(session, dataSet, 1, "writeBin", 342L); + } + + @Override + public void cleanup(ExampleContext context) { + Session session = context.session(); + DataSet dataSet = context.dataSet(); + + dropIndexIfExists(session, dataSet, COMMON_INDEX_NAME); + ExampleAssertions.truncate(session, dataSet); + } + }; + } + + public static ExampleFixture yamlConfigConnectionExample() { + return truncating( + "yaml-config-demo", + context -> { + Session session = context.session(); + DataSet dataSet = context.dataSet("yaml-config-demo"); + + ExampleAssertions.assertCount(session, dataSet, 4); + ExampleAssertions.assertBinEquals(session, dataSet, "user-001", "name", "Alice"); + ExampleAssertions.assertBinEquals(session, dataSet, "user-004", "age", 28L); + }); + } + + public static ExampleFixture studentScoresExample() { + return truncating( + "class10a", + context -> { + Session session = context.session(); + DataSet dataSet = context.dataSet("class10a"); + + ExampleAssertions.assertCount(session, dataSet, 30); + ExampleAssertions.assertBinEquals(session, dataSet, "student-1", "name", "Student 1"); + ExampleAssertions.assertBinEquals(session, dataSet, "student-30", "name", "Student 30"); + }); + } + + public static ExampleFixture mapRemoveByKeyRangeTest() { + return truncating( + "map_remove_test", + context -> { + TreeMap expected = new TreeMap<>(); + expected.put("a", 1L); + expected.put("b", 2L); + expected.put("c", 3L); + expected.put("d", 4L); + expected.put("e", 5L); + + Session session = context.session(); + DataSet dataSet = context.dataSet("map_remove_test"); + + ExampleAssertions.assertCount(session, dataSet, 1); + ExampleAssertions.assertBinEquals(session, dataSet, 1, "m", expected); + }); + } + + public static ExampleFixture transactionProcessingExample() { + return truncating( + context -> List.of( + context.dataSet("customers"), + context.dataSet("accounts"), + context.dataSet("txns")), + context -> { + Session session = context.session(); + DataSet customers = context.dataSet("customers"); + DataSet accounts = context.dataSet("accounts"); + DataSet txns = context.dataSet("txns"); + + ExampleAssertions.assertCount(session, customers, 1); + ExampleAssertions.assertCount(session, accounts, 1); + ExampleAssertions.assertCount(session, txns, 1); + ExampleAssertions.assertBinEquals(session, customers, "CUST-10042", "totalSpend", 45000L); + ExampleAssertions.assertBinEquals(session, customers, "CUST-10042", "statusLevel", "GOLD"); + ExampleAssertions.assertBinEquals(session, accounts, "4532015112830366", "balanceCents", 45000L); + ExampleAssertions.assertBinEquals(session, txns, "TXN-00001", "desc", "Car repairs"); + }); + } + + public static ExampleFixture completeYamlConfigExample() { + return truncating( + "complete-yaml-demo", + context -> ExampleAssertions.assertCount(context.session(), context.dataSet("complete-yaml-demo"), 0)); + } + + public static ExampleFixture aelTestSpecRunner() { + return truncating( + "ael_test_spec", + context -> ExampleAssertions.assertCount(context.session(), context.dataSet("ael_test_spec"), 11)); + } + + public static ExampleFixture operationDifferences() { + return truncating( + "ael_diff_test", + context -> ExampleAssertions.assertCount(context.session(), context.dataSet("ael_diff_test"), 7)); + } + + public static ExampleFixture queryExamples() { + return truncating( + context -> List.of(context.dataSet("person"), context.dataSet("users")), + context -> { + Session session = context.session(); + DataSet person = context.dataSet("person"); + + ExampleAssertions.assertRecordExists(session, person, 999); + ExampleAssertions.assertRecordExists(session, person, 500); + }); + } + + public static ExampleFixture ecommerceExample() { + return truncating( + context -> List.of( + context.dataSet("customers"), + context.dataSet("products"), + context.dataSet("orders")), + context -> { + Session session = context.session(); + DataSet customers = context.dataSet("customers"); + DataSet products = context.dataSet("products"); + DataSet orders = context.dataSet("orders"); + + ExampleAssertions.assertCount(session, customers, 20); + ExampleAssertions.assertCount(session, products, 95); + ExampleAssertions.assertCount(session, orders, 55); + ExampleAssertions.assertBinEquals(session, customers, "C-100", "balance", 89999L); + ExampleAssertions.assertBinEquals(session, products, "SKU-LAP01", "stock", 24L); + ExampleAssertions.assertBinEquals(session, orders, "ORD-2001", "status", "CONFIRMED"); + }); + } + + private static void dropIndexIfExists(Session session, DataSet dataSet, String indexName) { + try { + session.dropIndex(dataSet, indexName).waitTillComplete(); + } + catch (AerospikeException ignored) { + // The cleanup path should be idempotent for local reruns and CI retries. + } + } +} diff --git a/examples/src/main/java/com/aerospike/examples/query/Address.java b/examples/src/main/java/com/aerospike/examples/query/Address.java new file mode 100644 index 00000000..2ee799d8 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/query/Address.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples.query; + +/** + * Simple immutable value object used by {@link com.aerospike.examples.QueryExamples} + * to demonstrate nested object mapping. + */ +public class Address { + private final String line1; + private final String city; + private final String state; + private final String country; + private final String zipCode; + + public Address(String line1, String city, String state, String country, String zipCode) { + super(); + this.line1 = line1; + this.city = city; + this.state = state; + this.country = country; + this.zipCode = zipCode; + } + + public String getLine1() { + return line1; + } + public String getCity() { + return city; + } + + public String getState() { + return state; + } + public String getCountry() { + return country; + } + public String getZipCode() { + return zipCode; + } + @Override + public String toString() { + return "Address [line1=" + line1 + ", city=" + city + ", state=" + state + ", country=" + country + ", zipCode=" + + zipCode + "]"; + } +} diff --git a/examples/src/main/java/com/aerospike/examples/query/AddressMapper.java b/examples/src/main/java/com/aerospike/examples/query/AddressMapper.java new file mode 100644 index 00000000..15faf9a9 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/query/AddressMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples.query; + +import java.util.Map; + +import com.aerospike.client.sdk.Key; +import com.aerospike.client.sdk.RecordMapper; +import com.aerospike.client.sdk.util.MapUtil; + +public class AddressMapper implements RecordMapper
    { + + @Override + public Address fromMap(Map map, Key recordKey, int generation) { + Address result = new Address( + MapUtil.asString(map, "line1"), + MapUtil.asString(map, "city"), + MapUtil.asString(map, "state"), + MapUtil.asString(map, "country"), + MapUtil.asString(map, "zip")); + return result; + } + + @Override + public Map toMap(Address addr) { + return MapUtil.buildMap() + .add("line1", addr.getLine1()) + .add("city", addr.getCity()) + .add("state", addr.getState()) + .add("country", addr.getCountry()) + .add("zip", addr.getZipCode()) + .done(); + } + + @Override + public Object id(Address element) { + return null; + } + +} diff --git a/examples/src/main/java/com/aerospike/examples/query/Customer.java b/examples/src/main/java/com/aerospike/examples/query/Customer.java new file mode 100644 index 00000000..fe13e862 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/query/Customer.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples.query; + +import java.util.Date; + +/** + * Mutable POJO used by {@link com.aerospike.examples.QueryExamples} to demonstrate + * object mapping, including a nested {@link Address}. + */ +public class Customer { + private long id; + private String name; + private int age; + private Date dob; + private Address address; + + public Customer() { + super(); + } + public Customer(long id, String name, int age, Date dob) { + this(id, name, age, dob, null); + } + public Customer(long id, String name, int age, Date dob, Address address) { + super(); + this.id = id; + this.name = name; + this.age = age; + this.dob = dob; + this.address = address; + } + + public long getId() { + return id; + } + public void setId(long id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public int getAge() { + return age; + } + public void setAge(int age) { + this.age = age; + } + public Date getDob() { + return dob; + } + public void setDob(Date dob) { + this.dob = dob; + } + + public Address getAddress() { + return address; + } + public void setAddress(Address address) { + this.address = address; + } + + @Override + public String toString() { + return "Customer [id=" + id + ", name=" + name + ", age=" + age + ", dob=" + dob + ", address=" + address + "]"; + } +} diff --git a/examples/src/main/java/com/aerospike/examples/query/CustomerMapper.java b/examples/src/main/java/com/aerospike/examples/query/CustomerMapper.java new file mode 100644 index 00000000..b320f225 --- /dev/null +++ b/examples/src/main/java/com/aerospike/examples/query/CustomerMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); 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. + */ +package com.aerospike.examples.query; + +import java.util.Map; + +import com.aerospike.client.sdk.Key; +import com.aerospike.client.sdk.RecordMapper; +import com.aerospike.client.sdk.util.MapUtil; + +public class CustomerMapper implements RecordMapper { + @Override + public Customer fromMap(Map map, Key recordKey, int generation) { + Customer result = new Customer(); + result.setId(recordKey.userKey.toLong()); + result.setAge(MapUtil.asInt(map, "age")); + result.setDob(MapUtil.asDateFromLong(map, "dob")); + result.setName(MapUtil.asString(map, "name")); + result.setAddress(MapUtil.asObjectFromMap(map, "address", new AddressMapper())); + return result; + } + + @Override + public Map toMap(Customer customer) { + return MapUtil.buildMap() + .add("id", customer.getId()) + .add("age", customer.getAge()) + .addAsLong("dob", customer.getDob()) + .add("name", customer.getName()) + .add("address", customer.getAddress(), new AddressMapper()) + .done(); + } + + @Override + public Object id(Customer customer) { + return customer.getId(); + } +} From 46dc2e681f2a37424433392011706a9228d8eb81 Mon Sep 17 00:00:00 2001 From: Shannon Klaus Date: Thu, 23 Jul 2026 09:12:59 -0600 Subject: [PATCH 2/4] Convert latest examples to new execution model --- .../examples/CdtPathExpressionExample.java | 7 +- .../aerospike/examples/ExampleRegistry.java | 24 +++ .../examples/StringOperationsExample.java | 6 +- .../examples/TypedMappingExamples.java | 148 +++++++++--------- .../examples/fixtures/ExampleFixtures.java | 43 +++++ 5 files changed, 145 insertions(+), 83 deletions(-) diff --git a/examples/src/main/java/com/aerospike/examples/CdtPathExpressionExample.java b/examples/src/main/java/com/aerospike/examples/CdtPathExpressionExample.java index 0c2b9faa..a0df3972 100644 --- a/examples/src/main/java/com/aerospike/examples/CdtPathExpressionExample.java +++ b/examples/src/main/java/com/aerospike/examples/CdtPathExpressionExample.java @@ -49,8 +49,6 @@ public void runExample() throws Exception { Session session = cluster.createSession(Behavior.DEFAULT); DataSet set = dataSet("cdt-path-demo"); - session.truncate(set); - boolean allOk = true; console.info("--- 1) Bin-root list: onEachChild + modifyBy (increment each element) ---"); @@ -153,7 +151,10 @@ public void runExample() throws Exception { "The Lord of the Rings"); allOk &= reportCheck(console, 5, stringListMatchesUnordered(rec.bins.get("catalog"), allTitles)); - console.info("Overall: " + (allOk ? "SUCCESS" : "*** FAILURE ***")); + if (!allOk) { + throw new AssertionError("One or more CDT path expression checks failed"); + } + console.info("Overall: SUCCESS"); } private static boolean reportCheck(Console console, int step, boolean ok) { diff --git a/examples/src/main/java/com/aerospike/examples/ExampleRegistry.java b/examples/src/main/java/com/aerospike/examples/ExampleRegistry.java index 5e40ddaa..52144af8 100644 --- a/examples/src/main/java/com/aerospike/examples/ExampleRegistry.java +++ b/examples/src/main/java/com/aerospike/examples/ExampleRegistry.java @@ -93,6 +93,30 @@ private static Map createExamples() { "diagnostic", "ael", "records"); + register( + examples, + "CdtPathExpressionExample", + CdtPathExpressionExample.class, + ExampleFixtures.cdtPathExpressionExample(), + "extended", + "records", + "server-specific"); + register( + examples, + "StringOperationsExample", + StringOperationsExample.class, + ExampleFixtures.stringOperationsExample(), + "extended", + "records", + "server-specific"); + register( + examples, + "TypedMappingExamples", + TypedMappingExamples.class, + ExampleFixtures.typedMappingExamples(), + "extended", + "mapping", + "records"); register(examples, "QueryExamples", QueryExamples.class, ExampleFixtures.queryExamples(), "extended", "records"); register(examples, "EcommerceExample", EcommerceExample.class, ExampleFixtures.ecommerceExample(), "extended", "records"); register(examples, "RosterExample", RosterExample.class, ExampleFixture.NONE, "server-specific"); diff --git a/examples/src/main/java/com/aerospike/examples/StringOperationsExample.java b/examples/src/main/java/com/aerospike/examples/StringOperationsExample.java index 104b3243..b585ed53 100644 --- a/examples/src/main/java/com/aerospike/examples/StringOperationsExample.java +++ b/examples/src/main/java/com/aerospike/examples/StringOperationsExample.java @@ -44,14 +44,12 @@ public void runExample() throws Exception { Cluster cluster = cluster(); Version v = cluster.getRandomNode().getVersion(); if (!v.isGreaterOrEqual(Version.SERVER_VERSION_8_1_3)) { - console.info("StringOperationsExample: skipping (server is " + v - + "; string ops require 8.1.3+)."); - return; + throw new ExampleSkipException( + "server is " + v + "; string operations require 8.1.3+"); } Session session = cluster.createSession(Behavior.DEFAULT); DataSet set = dataSet("string-ops-demo"); - session.truncate(set); Key key = set.id("row1"); console.info("--- 1) Fluent BinBuilder: strlen, substr [1,4), substr from 3, find, upper ---"); diff --git a/examples/src/main/java/com/aerospike/examples/TypedMappingExamples.java b/examples/src/main/java/com/aerospike/examples/TypedMappingExamples.java index 11676183..374f7ce1 100644 --- a/examples/src/main/java/com/aerospike/examples/TypedMappingExamples.java +++ b/examples/src/main/java/com/aerospike/examples/TypedMappingExamples.java @@ -29,7 +29,6 @@ import com.aerospike.client.sdk.RecordStream; import com.aerospike.client.sdk.Session; import com.aerospike.client.sdk.TypedDataSet; -import com.aerospike.client.sdk.exp.Exp; import com.aerospike.client.sdk.policy.Behavior; import com.aerospike.client.sdk.util.MapUtil; @@ -43,11 +42,9 @@ * to load a related {@link Gadget} through {@link RecordReadContext#getSession()} when the * {@code related_gadget_id} bin is set.

    * - *

    Connection flags match other standalone examples ({@link Args} via {@link Example#parseStandaloneArgs}): - * default {@code localhost:3000}; {@code -h} / {@code -p} to override; optional {@code -a} for services - * alternate. Requires namespace {@code test}.

    + *

    The example uses the runner-managed cluster and configured namespace.

    */ -public final class TypedMappingExamples { +public final class TypedMappingExamples extends Example { /** Simple inventory row stored under an integer user key. */ public static final class Widget { @@ -157,6 +154,14 @@ public String toString() { } public static final class WidgetMapper implements RecordMapper { + private final TypedDataSet gadgets; + private final Console console; + + public WidgetMapper(TypedDataSet gadgets, Console console) { + this.gadgets = gadgets; + this.console = console; + } + private static Widget mapBins(Map map, Key recordKey) { Widget w = new Widget(); w.setId(recordKey.userKey.toLong()); @@ -183,7 +188,6 @@ public Widget fromMap(Map map, Key recordKey, int generation, Re return w; } Session session = ctx.getSession(); - TypedDataSet gadgets = TypedDataSet.of("test", "typed_demo_gadgets", Gadget.class); Optional peer = session.query(gadgets.id(relatedId)) .readingOnlyBins("name", "enabled") .limit(1) @@ -192,10 +196,10 @@ public Widget fromMap(Map map, Key recordKey, int generation, Re .map(RecordResult::toObject); if (peer.isPresent()) { Gadget g = peer.get(); - System.out.println(" [WidgetMapper 4-arg fromMap] Session read of related gadget id=" + console.info(" [WidgetMapper 4-arg fromMap] Session read of related gadget id=" + relatedId + " -> name='" + g.getName() + "', enabled=" + g.isEnabled()); } else { - System.out.println(" [WidgetMapper 4-arg fromMap] No gadget found for rel_gadget_id=" + console.info(" [WidgetMapper 4-arg fromMap] No gadget found for rel_gadget_id=" + relatedId); } return w; @@ -242,74 +246,66 @@ public Object id(Gadget gadget) { } } - public static void main(String[] args) throws Exception { - Args arguments = Example.parseStandaloneArgs(args); - try (Cluster cluster = Example.clusterDefinition(arguments).connect()) { - - WidgetMapper widgetMapper = new WidgetMapper(); - GadgetMapper gadgetMapper = new GadgetMapper(); - cluster.setRecordMappingFactory(DefaultRecordMappingFactory.of( - Widget.class, widgetMapper, - Gadget.class, gadgetMapper)); - - Session session = cluster.createSession(Behavior.DEFAULT); - - TypedDataSet widgets = TypedDataSet.of("test", "typed_demo_widgets", Widget.class); - TypedDataSet gadgets = TypedDataSet.of("test", "typed_demo_gadgets", Gadget.class); - - session.truncate(widgets); - session.truncate(gadgets); - - // Writes: factory resolves mapper from object class (no .using()). - // Insert gadget first so widget mapper's 4-arg fromMap can resolve it via session.query. - session.insert(gadgets).object(new Gadget(1, "notifications", true)).execute(); - session.insert(widgets).object(new Widget(1, "alpha", 10, 1L)).execute(); - session.insert(widgets).object(new Widget(2, "beta", 20)).execute(); - - // Single-row read via typed query stream: factory only (no .using() / no mapper arg). - // Widget id=1 has related_gadget_id=1; WidgetMapper 4-arg fromMap loads that gadget via session. - System.out.println("Typed query getFirstObject — watch for [WidgetMapper 4-arg fromMap] line:"); - Widget one = session.query(widgets) - .where("$.qty == 10") - .limit(1) - .execute() - .getFirstObject() - .orElseThrow(); - System.out.println("Typed query getFirstObject (factory): " + one); - - // Typed dataset query → TypedRecordStream: mapper-less toObjectList() (4-arg fromMap per row). - List fromScan = session.query(widgets) - .where("$.qty == 20") - .execute() - .toObjectList(); - System.out.println("Typed query (factory, qty=20 only): " + fromScan); - - System.out.println("Typed query toObjectList(all widgets) — id=1 triggers session read again:"); - List allWidgets = session.query(widgets).limit(10).execute().toObjectList(); - System.out.println(" " + allWidgets); - - Optional firstTyped = session.query(widgets) - .where("$.label == 'alpha'") - .execute() - .getFirstObject(); - System.out.println("Typed query first (factory): " + firstTyped.orElseThrow()); - - // Heterogeneous batch: each leg carries Class; map per row with toObject() (embedded session). - RecordStream batch = session - .query(widgets.id(2)) - .readingOnlyBins("label", "qty") - .query(gadgets.id(1)) - .readingOnlyBins("name", "enabled") - .execute(); - try (batch) { - Widget w = batch.next().toObject(); - Gadget g = batch.next().toObject(); - System.out.println("Batch widget: " + w); - System.out.println("Batch gadget: " + g); - } + @Override + public void runExample() throws Exception { + Cluster cluster = cluster(); + TypedDataSet widgets = TypedDataSet.of(namespace(), "typed_demo_widgets", Widget.class); + TypedDataSet gadgets = TypedDataSet.of(namespace(), "typed_demo_gadgets", Gadget.class); + + WidgetMapper widgetMapper = new WidgetMapper(gadgets, console); + GadgetMapper gadgetMapper = new GadgetMapper(); + cluster.setRecordMappingFactory(DefaultRecordMappingFactory.of( + Widget.class, widgetMapper, + Gadget.class, gadgetMapper)); + + Session session = cluster.createSession(Behavior.DEFAULT); + + // Writes: factory resolves mapper from object class (no .using()). + // Insert gadget first so widget mapper's 4-arg fromMap can resolve it via session.query. + session.insert(gadgets).object(new Gadget(1, "notifications", true)).execute(); + session.insert(widgets).object(new Widget(1, "alpha", 10, 1L)).execute(); + session.insert(widgets).object(new Widget(2, "beta", 20)).execute(); + + // Single-row read via typed query stream: factory only (no .using() / no mapper arg). + // Widget id=1 has related_gadget_id=1; WidgetMapper 4-arg fromMap loads that gadget via session. + console.info("Typed query getFirstObject — watch for [WidgetMapper 4-arg fromMap] line:"); + Widget one = session.query(widgets) + .where("$.qty == 10") + .limit(1) + .execute() + .getFirstObject() + .orElseThrow(); + console.info("Typed query getFirstObject (factory): " + one); + + // Typed dataset query → TypedRecordStream: mapper-less toObjectList() (4-arg fromMap per row). + List fromScan = session.query(widgets) + .where("$.qty == 20") + .execute() + .toObjectList(); + console.info("Typed query (factory, qty=20 only): " + fromScan); + + console.info("Typed query toObjectList(all widgets) — id=1 triggers session read again:"); + List allWidgets = session.query(widgets).limit(10).execute().toObjectList(); + console.info(" " + allWidgets); + + Optional firstTyped = session.query(widgets) + .where("$.label == 'alpha'") + .execute() + .getFirstObject(); + console.info("Typed query first (factory): " + firstTyped.orElseThrow()); + + // Heterogeneous batch: each leg carries Class; map per row with toObject() (embedded session). + RecordStream batch = session + .query(widgets.id(2)) + .readingOnlyBins("label", "qty") + .query(gadgets.id(1)) + .readingOnlyBins("name", "enabled") + .execute(); + try (batch) { + Widget w = batch.next().toObject(); + Gadget g = batch.next().toObject(); + console.info("Batch widget: " + w); + console.info("Batch gadget: " + g); } } - - private TypedMappingExamples() { - } } diff --git a/examples/src/main/java/com/aerospike/examples/fixtures/ExampleFixtures.java b/examples/src/main/java/com/aerospike/examples/fixtures/ExampleFixtures.java index f5020ed7..ed4bb62f 100644 --- a/examples/src/main/java/com/aerospike/examples/fixtures/ExampleFixtures.java +++ b/examples/src/main/java/com/aerospike/examples/fixtures/ExampleFixtures.java @@ -252,6 +252,49 @@ public static ExampleFixture ecommerceExample() { }); } + public static ExampleFixture cdtPathExpressionExample() { + return truncating( + "cdt-path-demo", + context -> { + Session session = context.session(); + DataSet dataSet = context.dataSet("cdt-path-demo"); + + ExampleAssertions.assertCount(session, dataSet, 5); + ExampleAssertions.assertBinEquals(session, dataSet, 1, "nums", List.of(11L, 12L, 13L)); + ExampleAssertions.assertBinEquals(session, dataSet, 2, "nums", List.of(3L, 2L)); + }); + } + + public static ExampleFixture stringOperationsExample() { + return truncating( + "string-ops-demo", + context -> { + Session session = context.session(); + DataSet dataSet = context.dataSet("string-ops-demo"); + + ExampleAssertions.assertCount(session, dataSet, 1); + ExampleAssertions.assertBinEquals(session, dataSet, "row1", "message", "hello"); + }); + } + + public static ExampleFixture typedMappingExamples() { + return truncating( + context -> List.of( + context.dataSet("typed_demo_widgets"), + context.dataSet("typed_demo_gadgets")), + context -> { + Session session = context.session(); + DataSet widgets = context.dataSet("typed_demo_widgets"); + DataSet gadgets = context.dataSet("typed_demo_gadgets"); + + ExampleAssertions.assertCount(session, widgets, 2); + ExampleAssertions.assertCount(session, gadgets, 1); + ExampleAssertions.assertBinEquals(session, widgets, 1, "label", "alpha"); + ExampleAssertions.assertBinEquals(session, widgets, 2, "qty", 20L); + ExampleAssertions.assertBinEquals(session, gadgets, 1, "name", "notifications"); + }); + } + private static void dropIndexIfExists(Session session, DataSet dataSet, String indexName) { try { session.dropIndex(dataSet, indexName).waitTillComplete(); From c7b9baa97d559a74bce71dc4fc7230ead12a19e0 Mon Sep 17 00:00:00 2001 From: Shannon Klaus Date: Thu, 23 Jul 2026 09:23:20 -0600 Subject: [PATCH 3/4] Add gate for server version 8.1.1 to path expression example, run integration tests before examples --- .github/workflows/pr-open.yaml | 26 +++++++++---------- .../examples/CdtPathExpressionExample.java | 9 ++++++- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pr-open.yaml b/.github/workflows/pr-open.yaml index e9b54d01..8fd75af8 100644 --- a/.github/workflows/pr-open.yaml +++ b/.github/workflows/pr-open.yaml @@ -130,6 +130,19 @@ jobs: } } + - name: Build & run integration tests + run: | + mvn \ + --batch-mode \ + --no-transfer-progress \ + --update-snapshots \ + --fail-at-end \ + --errors \ + verify \ + -DskipTests=false + env: + MAVEN_OPTS: ${{ vars.MAVEN_OPTS }} + - name: Build & run examples run: | mvn \ @@ -154,19 +167,6 @@ jobs: env: MAVEN_OPTS: ${{ vars.MAVEN_OPTS }} - - name: Build & run integration tests - run: | - mvn \ - --batch-mode \ - --no-transfer-progress \ - --update-snapshots \ - --fail-at-end \ - --errors \ - verify \ - -DskipTests=false - env: - MAVEN_OPTS: ${{ vars.MAVEN_OPTS }} - - name: Upload surefire/failsafe reports (always) if: always() uses: actions/upload-artifact@v4 diff --git a/examples/src/main/java/com/aerospike/examples/CdtPathExpressionExample.java b/examples/src/main/java/com/aerospike/examples/CdtPathExpressionExample.java index a0df3972..e61f477a 100644 --- a/examples/src/main/java/com/aerospike/examples/CdtPathExpressionExample.java +++ b/examples/src/main/java/com/aerospike/examples/CdtPathExpressionExample.java @@ -33,19 +33,26 @@ import com.aerospike.client.sdk.exp.LoopVarPart; import com.aerospike.client.sdk.exp.MapExp; import com.aerospike.client.sdk.policy.Behavior; +import com.aerospike.client.sdk.util.Version; /** * Demonstrates fluent CDT path helpers: {@code onEachChild}, {@code collectValues}, * {@code modifyBy}, {@code removeMatches}, and {@code collectValuesAsExpressionRead}. * *

    These features require Aerospike Server 8.1.1 or later ({@code selectByPath} / - * {@code modifyByPath}). If the cluster is older, operations may fail with a server error.

    + * {@code modifyByPath}). If the cluster is older, this example is skipped.

    */ public class CdtPathExpressionExample extends Example { @Override public void runExample() throws Exception { Cluster cluster = cluster(); + Version version = cluster.getRandomNode().getVersion(); + if (!version.isGreaterOrEqual(8, 1, 1, 0)) { + throw new ExampleSkipException( + "server is " + version + "; CDT path expressions require 8.1.1+"); + } + Session session = cluster.createSession(Behavior.DEFAULT); DataSet set = dataSet("cdt-path-demo"); From 9d324a9755835febb58df52189ddae02c4a89ccc Mon Sep 17 00:00:00 2001 From: Shannon Klaus Date: Thu, 23 Jul 2026 09:32:36 -0600 Subject: [PATCH 4/4] Change server gate for error detail verbosity tests to 8.1.3 --- .../aerospike/client/sdk/ErrorDetailVerbosityTest.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/src/test/java/com/aerospike/client/sdk/ErrorDetailVerbosityTest.java b/client/src/test/java/com/aerospike/client/sdk/ErrorDetailVerbosityTest.java index 654df9c7..34b26cae 100644 --- a/client/src/test/java/com/aerospike/client/sdk/ErrorDetailVerbosityTest.java +++ b/client/src/test/java/com/aerospike/client/sdk/ErrorDetailVerbosityTest.java @@ -47,12 +47,8 @@ public class ErrorDetailVerbosityTest extends ClusterTest { @BeforeAll public static void setup() { - // Extended error-detail (subcode/message) plus the verbosity-3 expression - // build trace (SERVER-1137). The SERVER-1137 feature branch is cut from the - // 8.1.1 line and reports its base version as 8.1.1.0-start-*, so gate at - // 8.1.1 rather than the 8.1.3 release that first shipped the base tier. - Assumptions.assumeTrue(args.serverVersion.isGreaterOrEqual(8, 1, 1, 0), - "Extended error-detail requires server version 8.1.1 or later"); + Assumptions.assumeTrue(args.serverVersion.isGreaterOrEqual(8, 1, 3, 0), + "Extended error-detail requires server version 8.1.3 or later"); intKey = args.set.id("edv-int-key"); strKey = args.set.id("edv-str-key");