From 6e390ac1e40e84b1a08bdf8a90d65e81ef9e44e0 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 11:07:38 +0100 Subject: [PATCH 01/29] mongodb-crypt: fix verifyCryptLibs gpg failure on deep checkout paths --- mongodb-crypt/build.gradle.kts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 6a46f2dfc9a..8c11aabfda8 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -17,6 +17,7 @@ import ProjectExtensions.configureJarManifest import ProjectExtensions.configureMavenPublication import de.undercouch.gradle.tasks.download.Download import java.io.ByteArrayOutputStream +import java.io.File import javax.inject.Inject import org.gradle.api.GradleException import org.gradle.process.ExecOperations @@ -275,7 +276,16 @@ tasks.register("verifyCryptLibs") { publicKey.set(file("$jnaDownloadsDir/$libmongocryptPublicKeyFile")) skipVerify.set(skipCryptVerify) expectedFingerprint.set("F2F5BF4ABF517E039AFCADAA81F1404DEBACA586") - gnupgHome.set(layout.buildDirectory.dir("jnaLibs/gnupg")) + // Keep the scratch GPG keyring under the system temp dir, not the module build dir. gpg derives + // its agent socket path from the homedir, and macOS caps AF_UNIX socket paths (sun_path) at 104 + // bytes. A deeply-nested checkout pushes "/build/jnaLibs/gnupg/S.gpg-agent" past that + // limit, so on a fresh keyring gpg fails ("can't connect to the gpg-agent: File name too long") + // and exits non-zero even though the import/verify would otherwise succeed. A short, + // checkout-independent homedir avoids this; the hash suffix isolates builds of different + // checkouts. verify() recreates this directory on every run, so a stable name is safe. + val cryptGpgHomeName = + "crypt-gpg-" + layout.buildDirectory.get().asFile.absolutePath.hashCode().toUInt().toString(16) + gnupgHome.set(layout.dir(providers.provider { File(System.getProperty("java.io.tmpdir"), cryptGpgHomeName) })) verificationStamp.set(layout.buildDirectory.file("jnaLibs/verified.stamp")) /* Bypass entirely when the caller has supplied a local libmongocrypt directory. */ From c4794acdad6f853f01e6d6ec396a2ba7b99784fd Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 13:52:43 +0100 Subject: [PATCH 02/29] docs(JAVA-6168): design for QE prefix/suffix GA + Text->String rename --- ...e-prefix-suffix-ga-string-rename-design.md | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md diff --git a/docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md b/docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md new file mode 100644 index 00000000000..eaa96dc709b --- /dev/null +++ b/docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md @@ -0,0 +1,161 @@ +# QE Prefix/Suffix GA + rename Text→String API + +**Ticket:** JAVA-6168 (Java split of DRIVERS-3321, under epic DRIVERS-3408) +**Date:** 2026-06-29 +**Status:** Approved design + +## 1. Summary + +Promote Queryable Encryption (QE) prefix/suffix text queries from preview to GA in the +Java driver, and rename the public explicit-encryption API from "Text" to "String", matching +the spec changes in DRIVERS-3321 and the Node analog PR +[#4972](https://github.com/mongodb/node-mongodb-native/pull/4972). Substring stays as an +experimental preview carry-over. + +This rides on the libmongocrypt bump from 1.18.1 → **1.19.1** (already applied on the branch). + +## 2. Ticket hierarchy and scope + +- **DRIVERS-3408** is the epic ("QE Prefix/Suffix/Substring GA"). +- **DRIVERS-3321** ("Add QE Prefix+Suffix GA and rename API to string") is the implementation + child. Its Java split is **JAVA-6168** (this work). DRIVERS-3321 is **not** a parallel track — + it *is* the work for the epic in Java. Node PR #4972 (NODE-7537) is the exact analog. + +### In scope +- libmongocrypt 1.19.1 bump (done). +- Public API rename Text→String in `driver-core`, with `@Deprecated` aliases for the old names. +- Substring carried over as experimental preview (matches Node). +- Rewrite the QE text explicit-encryption prose test to the GA spec ("27. String Explicit + Encryption"); rely on the auto-discovered `QE-Text-*` unified spec tests. + +### Out of scope (separate, blocked, or future — not doable on 1.19.1) +- **DRIVERS-3540** — substring *GA* rename (`substringPreview` → `substring`). Blocked on a + server/libmongocrypt rename; libmongocrypt 1.19.1 only ships `substringPreview`. +- **DRIVERS-3548** — drop the preview query types. Scheduled near server 9.0, future. +- **DRIVERS-3294** — update the `$lookup` prose test 25 for MONGOCRYPT-793 (Node split NODE-7221). + Unrelated to text/string queries (it tests encrypted-collection joins). Its own JAVA ticket/PR. + +## 3. libmongocrypt 1.19.1 behavior (source-verified) + +Verified against `mongocrypt-ctx.c` at tag `1.19.1`. + +| Identifier | Kind | 1.19.0 | 1.19.1 | Notes | +|---|---|---|---|---| +| `prefixPreview` / `suffixPreview` | query type | removed | **restored** | Work on pre-9.0 servers; persisted in `encryptedFields` | +| `prefix` / `suffix` | query type | added (GA) | GA | Server 9.0.0+ | +| `substringPreview` | query type | preview | preview | Still experimental; no GA yet | +| `textPreview` | algorithm | renamed → `string`, **not** restored | still gone | Client-only selector | +| `string` | algorithm | added (GA) | GA | `MONGOCRYPT_ALGORITHM_STRING_STR` | + +`mongocrypt_ctx_setopt_algorithm()` matches (case-insensitive): Deterministic, Random, Indexed, +Unindexed, Range, RangePreview (→ explicit error), String. **There is no `textPreview` branch** — +passing it fails with `unsupported algorithm string "textPreview"`. + +### Valid algorithm / queryType pairings (verified) +The init validation in `mongocrypt-ctx-encrypt.c` requires the `string` index type for all the +text query types (`prefix query type requires string index type`, etc.), and treats +`prefixPreview`/`suffixPreview` as deprecated aliases of `prefix`/`suffix`: + +| algorithm | queryType | result | +|---|---|---| +| `"String"` | `"prefix"` (GA) | valid | +| `"String"` | `"prefixPreview"` (deprecated alias) | valid | +| `"String"` | `"substringPreview"` (experimental) | valid | +| `"TextPreview"` | *anything* | rejected — `unsupported algorithm string "textPreview"` | + +So the only way to use `prefixPreview`/`suffixPreview` after this work is with the `"String"` +algorithm — the old `"TextPreview"` + `"prefixPreview"` combination no longer exists. For +**explicit** encryption, `string`+`prefix` and `string`+`prefixPreview` produce identical payloads; +the distinction matters only for **auto-encryption**, where the query-type name persisted in +`encryptedFields` must match the target server (pre-9.0 → `prefixPreview`, 9.0+ → `prefix`). The +`EncryptOptions.queryType` javadoc should state this pairing constraint. + +### Why the asymmetry is intentional (not an oversight) +- **Query types** are persisted in a collection's `encryptedFields` schema and are + server-version-dependent. Pre-9.0 servers (8.2/8.3) *only* support the preview names; 9.0 + rejects them. For **auto-encryption** against a pre-9.0 server, libmongocrypt must tolerate + `prefixPreview`/`suffixPreview` in `encryptedFields` — hence the restore (MONGOCRYPT-937 #1191). +- **The algorithm name** is a client-only selector that picks the internal index type + (`MONGOCRYPT_INDEX_TYPE_STRING`) for explicit encryption. It is never written to + `encryptedFields` or sent to the server, so there is nothing deployed that references + `textPreview`. A one-time rename costs nothing and needs no restore (MONGOCRYPT-870 #1184). The + target `string` (over `text`) was chosen per DRIVERS-3531 to avoid confusion with MongoDB text + search. + +## 4. Public API changes — `driver-core` (`com.mongodb.client.model.vault`) + +- **New `StringOptions` class** (`@Alpha(Reason.SERVER)`): same shape as today's `TextOptions` + (`caseSensitive`, `diacriticSensitive`, `prefixOptions`, `suffixOptions`, `substringOptions`). + `substringOptions` documented as experimental/preview. +- **`TextOptions` retained as `@Deprecated`**, as a deprecated alias of `StringOptions`. Preferred + shape: `TextOptions extends StringOptions` with no new members, so existing code compiles and any + `TextOptions` instance is usable where `StringOptions` is expected. The exact binary-safe form + (subclass vs. standalone delegating class) is confirmed during planning. +- **`EncryptOptions`**: add `stringOptions(StringOptions)` + `getStringOptions()`; retain + `textOptions(TextOptions)` + `getTextOptions()` as `@Deprecated`, backed by the same field. + Resolution mirrors Node (`stringOptions` wins, falling back to `textOptions`). +- **Doc-only updates** to `EncryptOptions`: algorithm list `"TextPreview"` → `"String"`; queryType + docs `prefixPreview`/`suffixPreview` → `prefix`/`suffix` (note the preview names remain supported + for pre-9.0 servers; `substringPreview` experimental). + +### Algorithm caveat +The `algorithm` value is a free-form `String` supplied by the user and passed straight to +libmongocrypt, which removed `"textPreview"` (only `"string"` works). We therefore **cannot** +provide a driver-side code alias for the algorithm string — we document `"String"` as the GA name. +This is why the existing prose test (`new EncryptOptions("TextPreview")`) was disabled and must be +rewritten to `"String"`. + +### Binary compatibility +All changes are additive (new class, new methods) or retain deprecated members. **No breaking +change; no major-version bump required.** + +## 5. Internal mapping & JNA — `mongodb-crypt` (minimal) + +- `EncryptOptionsHelper`: read from `getStringOptions()` (falling back to deprecated + `getTextOptions()`); BSON keys (`prefix`/`suffix`/`substring`, `caseSensitive`, + `diacriticSensitive`) are **unchanged**. +- **No JNA binding change required.** The C function + `mongocrypt_ctx_setopt_algorithm_text(ctx, opts)` and its BSON option schema are identical in + 1.19.1. Optional: refresh doc-comment wording ("TextPreview" → "String"). `CAPI.java`, + `MongoCryptImpl`, `MongoExplicitEncryptOptions` need no functional change. + +## 6. Test changes + +- **Spec submodule:** already bumped; includes `QE-Text-prefix/suffix*` (GA) and `*Preview` + files plus the `-ci-di` / `-preview` `encryptedFields` data. +- **Unified runner** (`ClientSideEncryptionTest`, sync + reactive): auto-discovers and runs the new + `QE-Text-*.json` unified tests — largely free coverage. +- **Prose test rewrite** (`AbstractClientEncryptionTextExplicitEncryptionTest`): rewrite to spec + test "27. String Explicit Encryption": + - Switch to algorithm `"String"` + `stringOptions`/`StringOptions`. + - Parameterize each case across `(prefix, prefix-suffix)` on server 9.0+/libmongocrypt 1.19.0+ + and `(prefixPreview, prefix-suffix-preview)` on server pre-9.0/libmongocrypt 1.19.1+. + - Add the `-ci-di` (case-/diacritic-insensitive) and `-preview` collections; substring carried + over as preview. + - Restore proper `assumeTrue` gating (currently commented out). + - Bump `REQUIRED_LIB_MONGOCRYPT_VERSION` to match the spec's per-case requirements. + - Update the case-7 error substring (now the `"string"` algorithm). + +### Auto-encryption pre-9.0 preview path — coverage notes +The auto-encryption path against a pre-9.0 server with `prefixPreview`/`suffixPreview` +`encryptedFields` **is** covered by the unified tests `QE-Text-prefixPreview` / +`QE-Text-suffixPreview` (`minServerVersion 8.2.0`, `maxServerVersion 8.99.99`, `csfle` +auto-encryption, no `bypassQueryAnalysis`, full insert+query round-trip). No new prose test needed. +Two execution conditions must hold, to verify during implementation: +1. These unified tests are **not skipped** by the Java runner / `UnifiedTestModifications` + (they require libmongocrypt 1.19.1+, which we now have). +2. The **CI test matrix includes a pre-9.0 server (8.2/8.3)** — otherwise these `maxServerVersion + 8.99.99` tests silently no-op and the path is "covered" on paper but never executed. + +## 7. Language wrappers + +Kotlin and Scala reuse `driver-core`'s `EncryptOptions`/`TextOptions` directly (no wrapper copies +found), so they inherit `stringOptions`/`StringOptions` automatically. Verify during planning. + +## 8. Risks + +- Prose-test version gating (GA vs. preview keyed on the server-9.0 boundary and libmongocrypt + 1.19.0/1.19.1) is the trickiest part — must match the spec precisely. +- Users still passing `"TextPreview"` as the algorithm get a libmongocrypt error on 1.19.1+ — + inherent to the bump, documented, not preventable in driver code. +- Pre-9.0 server absent from CI would hide the auto-encryption preview-path coverage (see §6). From b38210c7a5c09b83d6d7ed434e909cdc2d7c73b9 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 15:42:13 +0100 Subject: [PATCH 03/29] docs(JAVA-6168): implementation plan for QE prefix/suffix GA + Text->String rename --- ...06-29-qe-prefix-suffix-ga-string-rename.md | 1212 +++++++++++++++++ 1 file changed, 1212 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-29-qe-prefix-suffix-ga-string-rename.md diff --git a/docs/superpowers/plans/2026-06-29-qe-prefix-suffix-ga-string-rename.md b/docs/superpowers/plans/2026-06-29-qe-prefix-suffix-ga-string-rename.md new file mode 100644 index 00000000000..202335411da --- /dev/null +++ b/docs/superpowers/plans/2026-06-29-qe-prefix-suffix-ga-string-rename.md @@ -0,0 +1,1212 @@ +# QE Prefix/Suffix GA + rename Text→String — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Promote QE prefix/suffix text queries to GA and rename the public explicit-encryption API from "Text" to "String" (with deprecated aliases), riding on the libmongocrypt 1.19.1 bump. + +**Architecture:** Add a new public `StringOptions` class mirroring `TextOptions`; deprecate `TextOptions`. On `EncryptOptions`, add `stringOptions(...)`/`getStringOptions()` and deprecate `textOptions(...)`/`getTextOptions()`, keeping two independent backing fields. `EncryptOptionsHelper` resolves `stringOptions` first, falling back to `textOptions` (mirrors Node's `stringOptions ?? textOptions`). The JNA layer is unchanged. The QE text explicit-encryption prose test is rewritten to spec test "27. String Explicit Encryption". + +**Tech Stack:** Java 8 (driver-core, driver-sync), JNA (mongodb-crypt), JUnit 5, libmongocrypt 1.19.1. + +## Global Constraints + +- Java 8 language level — no `var`, records, text blocks, switch expressions, `Stream.toList()`, etc. +- Copyright header `Copyright 2008-present MongoDB, Inc.` on every new file. +- No `System.out`/`System.err`/`printStackTrace` — use SLF4J if logging is needed. +- Nullability annotations: `com.mongodb.lang.Nullable` / `NonNull`. +- New `@Alpha(Reason.SERVER)` API for `StringOptions` (matches current `TextOptions`). +- Binary compatibility: all changes additive or deprecated-retained — **no breaking change, no major bump**. +- libmongocrypt floor for the rewritten prose test: **1.19.1** (already on the branch). +- Valid pairings (verified): the `"String"` algorithm is required for query types `prefix`, `suffix`, `prefixPreview`, `suffixPreview`, `substringPreview`. The `"TextPreview"` algorithm is rejected by libmongocrypt 1.19.x. +- Do not modify the spec submodule (`testing/resources/specifications/`) — already bumped. +- Run `./gradlew spotlessApply` before committing; do not reformat code outside your change. + +--- + +### Task 1: Add `StringOptions`, deprecate `TextOptions` + +**Files:** +- Create: `driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java` +- Modify: `driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java` +- Test: `driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java` + +**Interfaces:** +- Produces: `public class StringOptions` with fluent setters `caseSensitive(boolean)`, `diacriticSensitive(boolean)`, `prefixOptions(@Nullable BsonDocument)`, `suffixOptions(@Nullable BsonDocument)`, `substringOptions(@Nullable BsonDocument)` (each returns `StringOptions`); getters `boolean getCaseSensitive()`, `boolean getDiacriticSensitive()`, `@Nullable BsonDocument getPrefixOptions()`, `getSuffixOptions()`, `getSubstringOptions()`. +- Consumes: nothing. + +- [ ] **Step 1: Write the failing test** + +Create `driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java`: + +```java +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.client.model.vault; + +import org.bson.BsonDocument; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StringOptionsTest { + + @Test + void shouldRoundTripAllProperties() { + BsonDocument prefix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"); + BsonDocument suffix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"); + BsonDocument substring = BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}"); + + StringOptions options = new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(prefix) + .suffixOptions(suffix) + .substringOptions(substring); + + assertTrue(options.getCaseSensitive()); + assertTrue(options.getDiacriticSensitive()); + assertEquals(prefix, options.getPrefixOptions()); + assertEquals(suffix, options.getSuffixOptions()); + assertEquals(substring, options.getSubstringOptions()); + } + + @Test + void shouldDefaultOptionDocumentsToNull() { + StringOptions options = new StringOptions(); + assertNull(options.getPrefixOptions()); + assertNull(options.getSuffixOptions()); + assertNull(options.getSubstringOptions()); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./gradlew :driver-core:test --tests "com.mongodb.client.model.vault.StringOptionsTest"` +Expected: FAIL — compilation error, `StringOptions` does not exist. + +- [ ] **Step 3: Create `StringOptions`** + +Create `driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java` by copying the current `TextOptions` body verbatim, renaming the class to `StringOptions`, updating the class Javadoc to say "String options for a Queryable Encryption field that supports string queries" and noting `substringOptions` is experimental/preview. Keep `@Alpha(Reason.SERVER)`, `@since 5.6`, `@mongodb.server.release 8.2`. The full file: + +```java +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.client.model.vault; + +import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Reason; +import com.mongodb.lang.Nullable; +import org.bson.BsonDocument; + +/** + * String options for a Queryable Encryption field that supports string queries (prefix, suffix, and substring). + * + *

Note: StringOptions is in Alpha and subject to backwards breaking changes. The {@code substring} query support + * is experimental (preview) and may change in a future non-major release. + * + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ +@Alpha(Reason.SERVER) +public class StringOptions { + private Boolean caseSensitive; + private Boolean diacriticSensitive; + @Nullable + private BsonDocument prefixOptions; + @Nullable + private BsonDocument suffixOptions; + @Nullable + private BsonDocument substringOptions; + + /** + * Construct a new instance + */ + public StringOptions() { + } + + /** + * @return true if string indexes for this field are case sensitive. + */ + public boolean getCaseSensitive() { + return caseSensitive; + } + + /** + * Set case sensitivity + * + * @param caseSensitive true if string indexes are case sensitive + * @return this + */ + public StringOptions caseSensitive(final boolean caseSensitive) { + this.caseSensitive = caseSensitive; + return this; + } + + /** + * @return true if string indexes are diacritic sensitive + */ + public boolean getDiacriticSensitive() { + return diacriticSensitive; + } + + /** + * Set diacritic sensitivity + * + * @param diacriticSensitive true if string indexes are diacritic sensitive + * @return this + */ + public StringOptions diacriticSensitive(final boolean diacriticSensitive) { + this.diacriticSensitive = diacriticSensitive; + return this; + } + + /** + * Set the prefix options. + * + *

Expected to be a {@link BsonDocument} in the format of:

+ * + *
+     * {@code
+     *   {
+     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
+     *    strMinQueryLength: BsonInt32,
+     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
+     *    strMaxQueryLength: BsonInt32
+     *   }
+     * }
+     * 
+ * + * @param prefixOptions the prefix options or null + * @return this + */ + public StringOptions prefixOptions(@Nullable final BsonDocument prefixOptions) { + this.prefixOptions = prefixOptions; + return this; + } + + /** + * @see #prefixOptions(BsonDocument) + * @return the prefix options document or null + */ + @Nullable + public BsonDocument getPrefixOptions() { + return prefixOptions; + } + + /** + * Set the suffix options. + * + *

Expected to be a {@link BsonDocument} in the format of:

+ * + *
+     * {@code
+     *   {
+     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
+     *    strMinQueryLength: BsonInt32,
+     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
+     *    strMaxQueryLength: BsonInt32
+     *   }
+     * }
+     * 
+ * + * @param suffixOptions the suffix options or null + * @return this + */ + public StringOptions suffixOptions(@Nullable final BsonDocument suffixOptions) { + this.suffixOptions = suffixOptions; + return this; + } + + /** + * @see #suffixOptions(BsonDocument) + * @return the suffix options document or null + */ + @Nullable + public BsonDocument getSuffixOptions() { + return suffixOptions; + } + + /** + * Set the substring options. + * + *

Note: substring string queries are experimental (preview) and may change in a future non-major release.

+ * + *

Expected to be a {@link BsonDocument} in the format of:

+ * + *
+     * {@code
+     *   {
+     *    // strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
+     *    strMaxLength: BsonInt32,
+     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
+     *    strMinQueryLength: BsonInt32,
+     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
+     *    strMaxQueryLength: BsonInt32
+     *   }
+     * }
+     * 
+ * + * @param substringOptions the substring options or null + * @return this + */ + public StringOptions substringOptions(@Nullable final BsonDocument substringOptions) { + this.substringOptions = substringOptions; + return this; + } + + /** + * @see #substringOptions(BsonDocument) + * @return the substring options document or null + */ + @Nullable + public BsonDocument getSubstringOptions() { + return substringOptions; + } +} +``` + +- [ ] **Step 4: Deprecate `TextOptions`** + +In `driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java`, leave the body unchanged but add a deprecation. Update the class Javadoc and annotation block (lines 24–34). Replace: + +```java +/** + * Text options for a Queryable Encryption field that supports text queries. + * + *

Note: TextOptions is in Alpha and subject to backwards breaking changes. + * + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ +@Alpha(Reason.SERVER) +public class TextOptions { +``` + +with: + +```java +/** + * Text options for a Queryable Encryption field that supports text queries. + * + *

Note: TextOptions is in Alpha and subject to backwards breaking changes. + * + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + * @deprecated Use {@link StringOptions} instead. + */ +@Deprecated +@Alpha(Reason.SERVER) +public class TextOptions { +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `./gradlew :driver-core:test --tests "com.mongodb.client.model.vault.StringOptionsTest"` +Expected: PASS (2 tests). + +- [ ] **Step 6: Commit** + +```bash +./gradlew :driver-core:spotlessApply +git add driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java \ + driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java \ + driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java +git commit -m "feat(JAVA-6168): add StringOptions and deprecate TextOptions" +``` + +--- + +### Task 2: `EncryptOptions` — add `stringOptions`, deprecate `textOptions`, update docs + +**Files:** +- Modify: `driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java` +- Test: `driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java` + +**Interfaces:** +- Consumes: `StringOptions`, `TextOptions` (Task 1). +- Produces: `EncryptOptions stringOptions(@Nullable StringOptions)`, `@Nullable StringOptions getStringOptions()`; deprecated `EncryptOptions textOptions(@Nullable TextOptions)`, `@Nullable TextOptions getTextOptions()`. Two independent backing fields `stringOptions` and `textOptions`. + +- [ ] **Step 1: Write the failing test** + +Create `driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java`: + +```java +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.client.model.vault; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +class EncryptOptionsTest { + + @Test + void shouldStoreStringOptions() { + StringOptions stringOptions = new StringOptions().caseSensitive(true); + EncryptOptions options = new EncryptOptions("String").stringOptions(stringOptions); + assertSame(stringOptions, options.getStringOptions()); + assertNull(options.getTextOptions()); + } + + @Test + @SuppressWarnings("deprecation") + void shouldStoreDeprecatedTextOptionsIndependently() { + TextOptions textOptions = new TextOptions().caseSensitive(true); + EncryptOptions options = new EncryptOptions("String").textOptions(textOptions); + assertSame(textOptions, options.getTextOptions()); + assertNull(options.getStringOptions()); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./gradlew :driver-core:test --tests "com.mongodb.client.model.vault.EncryptOptionsTest"` +Expected: FAIL — `stringOptions`/`getStringOptions` do not exist. + +- [ ] **Step 3: Add the field** + +In `EncryptOptions.java`, after line 36 (`private TextOptions textOptions;`) add: + +```java + private StringOptions stringOptions; +``` + +- [ ] **Step 4: Add `stringOptions(...)` and `getStringOptions()`** + +In `EncryptOptions.java`, immediately after the existing `getTextOptions()` method (ends at line 233, before the `toString()` at line 235), insert: + +```java + /** + * The StringOptions + * + *

It is an error to set StringOptions when the algorithm is not "String". + * @param stringOptions the string options + * @return this + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ + @Alpha(Reason.SERVER) + public EncryptOptions stringOptions(@Nullable final StringOptions stringOptions) { + this.stringOptions = stringOptions; + return this; + } + + /** + * Gets the StringOptions + * @see #stringOptions(StringOptions) + * @return the string options or null if not set + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ + @Alpha(Reason.SERVER) + @Nullable + public StringOptions getStringOptions() { + return stringOptions; + } +``` + +- [ ] **Step 5: Deprecate `textOptions(...)` and `getTextOptions()`** + +In `EncryptOptions.java`, update the existing `textOptions(...)` (lines ~205–219) and `getTextOptions()` (lines ~221–233) Javadocs to add `@deprecated Use {@link #stringOptions(StringOptions)} instead.` / `@deprecated Use {@link #getStringOptions()} instead.` and annotate both methods `@Deprecated`. The method bodies are unchanged. Example for the setter: + +```java + /** + * The TextOptions + * + *

It is an error to set TextOptions when the algorithm is not "TextPreview". + * @param textOptions the text options + * @return this + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + * @deprecated Use {@link #stringOptions(StringOptions)} instead. + */ + @Deprecated + @Alpha(Reason.SERVER) + public EncryptOptions textOptions(@Nullable final TextOptions textOptions) { + this.textOptions = textOptions; + return this; + } +``` + +Apply the analogous `@Deprecated` + `@deprecated Use {@link #getStringOptions()} instead.` change to `getTextOptions()`. + +- [ ] **Step 6: Update the algorithm and queryType Javadocs** + +In `getAlgorithm()` (lines ~49–65), change the bullet `

  • TextPreview
  • ` to `
  • String
  • ` and replace the "TextPreview" preview note paragraph with: + +```java + *

    The "String" algorithm supports Queryable Encryption prefix and suffix string queries, and (in preview) + * substring queries. Use the "String" algorithm with query types "prefix"/"suffix" (server 9.0+) or the + * deprecated aliases "prefixPreview"/"suffixPreview" (server 8.2 to pre-9.0), and "substringPreview" + * (experimental).

    +``` + +In `queryType(...)` (lines ~149–162), replace the line: + +```java + *

    Currently, we support only "equality", "range", "prefixPreview", "suffixPreview" or "substringPreview" queryType.

    + *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "TextPreview".

    +``` + +with: + +```java + *

    Currently, we support only "equality", "range", "prefix", "suffix", "prefixPreview", "suffixPreview" or + * "substringPreview" queryType.

    + *

    The "prefix", "suffix", "prefixPreview", "suffixPreview" and "substringPreview" query types are only valid + * with the "String" algorithm. "prefixPreview"/"suffixPreview" are deprecated aliases supported for servers 8.2 + * to pre-9.0; use "prefix"/"suffix" on server 9.0+.

    + *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "String".

    +``` + +- [ ] **Step 7: Run tests to verify they pass** + +Run: `./gradlew :driver-core:test --tests "com.mongodb.client.model.vault.EncryptOptionsTest"` +Expected: PASS (2 tests). + +- [ ] **Step 8: Commit** + +```bash +./gradlew :driver-core:spotlessApply +git add driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java \ + driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java +git commit -m "feat(JAVA-6168): add EncryptOptions.stringOptions and deprecate textOptions" +``` + +--- + +### Task 3: `EncryptOptionsHelper` — resolve stringOptions, fall back to textOptions + +**Files:** +- Modify: `driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java` +- Test: `driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java` + +**Interfaces:** +- Consumes: `EncryptOptions.getStringOptions()`, `EncryptOptions.getTextOptions()` (Task 2); `MongoExplicitEncryptOptions.getTextOptions()` (unchanged internal type). +- Produces: unchanged public method `asMongoExplicitEncryptOptions(EncryptOptions)`. + +- [ ] **Step 1: Write the failing test** + +Create `driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java`: + +```java +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.internal.client.vault; + +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.StringOptions; +import com.mongodb.client.model.vault.TextOptions; +import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions; +import org.bson.BsonDocument; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class EncryptOptionsHelperTest { + + @Test + void shouldMapStringOptionsToTextOptionsDocument() { + EncryptOptions options = new EncryptOptions("String").stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(false) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); + + assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: false, " + + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), + result.getTextOptions()); + } + + @Test + @SuppressWarnings("deprecation") + void shouldFallBackToDeprecatedTextOptions() { + EncryptOptions options = new EncryptOptions("String").textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); + + assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, " + + "suffix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), + result.getTextOptions()); + } + + @Test + void shouldLeaveTextOptionsNullWhenNeitherSet() { + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions( + new EncryptOptions("Indexed")); + assertNull(result.getTextOptions()); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./gradlew :driver-core:test --tests "com.mongodb.internal.client.vault.EncryptOptionsHelperTest"` +Expected: FAIL — helper still reads only `getTextOptions()`, so `shouldMapStringOptionsToTextOptionsDocument` fails (text options null). + +- [ ] **Step 3: Update the helper** + +In `EncryptOptionsHelper.java`, add the import `import com.mongodb.client.model.vault.StringOptions;` (after the `RangeOptions` import). Then replace the `TextOptions` block (lines 76–97): + +```java + TextOptions textOptions = options.getTextOptions(); + if (textOptions != null) { + BsonDocument textOptionsDocument = new BsonDocument(); + textOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(textOptions.getCaseSensitive())); + textOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(textOptions.getDiacriticSensitive())); + + BsonDocument substringOptions = textOptions.getSubstringOptions(); + if (substringOptions != null) { + textOptionsDocument.put("substring", substringOptions); + } + + BsonDocument prefixOptions = textOptions.getPrefixOptions(); + if (prefixOptions != null) { + textOptionsDocument.put("prefix", prefixOptions); + } + + BsonDocument suffixOptions = textOptions.getSuffixOptions(); + if (suffixOptions != null) { + textOptionsDocument.put("suffix", suffixOptions); + } + encryptOptionsBuilder.textOptions(textOptionsDocument); + } +``` + +with (note: `getTextOptions()` is deprecated; suppress on the method or read via a local — use a `@SuppressWarnings("deprecation")` on `asMongoExplicitEncryptOptions`): + +```java + StringOptions stringOptions = resolveStringOptions(options); + if (stringOptions != null) { + BsonDocument stringOptionsDocument = new BsonDocument(); + stringOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(stringOptions.getCaseSensitive())); + stringOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(stringOptions.getDiacriticSensitive())); + + BsonDocument substringOptions = stringOptions.getSubstringOptions(); + if (substringOptions != null) { + stringOptionsDocument.put("substring", substringOptions); + } + + BsonDocument prefixOptions = stringOptions.getPrefixOptions(); + if (prefixOptions != null) { + stringOptionsDocument.put("prefix", prefixOptions); + } + + BsonDocument suffixOptions = stringOptions.getSuffixOptions(); + if (suffixOptions != null) { + stringOptionsDocument.put("suffix", suffixOptions); + } + encryptOptionsBuilder.textOptions(stringOptionsDocument); + } +``` + +Then add the import `import com.mongodb.client.model.vault.TextOptions;` is already present — keep it. Add this private helper method above the private constructor (before `private EncryptOptionsHelper()`): + +```java + @SuppressWarnings("deprecation") + @Nullable + private static StringOptions resolveStringOptions(final EncryptOptions options) { + StringOptions stringOptions = options.getStringOptions(); + if (stringOptions != null) { + return stringOptions; + } + TextOptions textOptions = options.getTextOptions(); + if (textOptions == null) { + return null; + } + return new StringOptions() + .caseSensitive(textOptions.getCaseSensitive()) + .diacriticSensitive(textOptions.getDiacriticSensitive()) + .prefixOptions(textOptions.getPrefixOptions()) + .suffixOptions(textOptions.getSuffixOptions()) + .substringOptions(textOptions.getSubstringOptions()); + } +``` + +Add the import `import com.mongodb.lang.Nullable;` if not present. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `./gradlew :driver-core:test --tests "com.mongodb.internal.client.vault.EncryptOptionsHelperTest"` +Expected: PASS (3 tests). + +- [ ] **Step 5: Commit** + +```bash +./gradlew :driver-core:spotlessApply +git add driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java \ + driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java +git commit -m "feat(JAVA-6168): resolve stringOptions with textOptions fallback in EncryptOptionsHelper" +``` + +--- + +### Task 4: Refresh mongodb-crypt doc comments (TextPreview → String) + +**Files:** +- Modify: `mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java` + +**Interfaces:** +- Consumes/Produces: none — doc-comment-only change. No signature or behavior change. + +- [ ] **Step 1: Update doc comments** + +In `MongoExplicitEncryptOptions.java`, update three Javadoc mentions of `"TextPreview"` to reflect the GA `"String"` algorithm. Replace `

    Only applies when algorithm is "Indexed", "Range", or "TextPreview".

    ` (two occurrences, on `contentionFactor(...)` and `queryType(...)`) with `

    Only applies when algorithm is "Indexed", "Range", or "String".

    `, and replace `

    Only applies when algorithm is "TextPreview".

    ` (on `textOptions(...)`) with `

    Only applies when algorithm is "String".

    `. + +- [ ] **Step 2: Verify it compiles** + +Run: `./gradlew :mongodb-crypt:compileJava` +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 3: Commit** + +```bash +./gradlew :mongodb-crypt:spotlessApply +git add mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java +git commit -m "docs(JAVA-6168): refer to String algorithm in MongoExplicitEncryptOptions" +``` + +--- + +### Task 5: Rewrite the QE string explicit-encryption prose test to GA (spec test 27) + +**Files:** +- Modify: `driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java` + +**Interfaces:** +- Consumes: `EncryptOptions` with `"String"` algorithm + `stringOptions(StringOptions)` (Tasks 1–2); test data files already present in the spec submodule (`encryptedFields-prefix-suffix.json`, `encryptedFields-prefix-suffix-ci-di.json`, `encryptedFields-prefix-suffix-preview.json`, `encryptedFields-substring.json`, `encryptedFields-substring-ci-di.json`, `keys/key1-document.json`). +- Produces: nothing (test only). The two concrete subclasses (`driver-sync/.../ClientEncryptionTextExplicitEncryptionTest.java` and `driver-reactive-streams/.../ClientEncryptionTextExplicitEncryptionTest.java`) need no change — they only implement `createMongoClient`/`createClientEncryption`. + +Implements spec "27. String Explicit Encryption": algorithm `"String"` + `stringOptions`/`StringOptions`; cases 1–4 parameterized as `(prefix, prefix-suffix)` on server 9.0+ and `(prefixPreview, prefix-suffix-preview)` on server pre-9.0; cases 7–9 require server 9.0+; cases 5,6,10 run on 8.2+. An `autoEncryptedClient` (auto-encryption, no `bypassQueryAnalysis`) is added for cases 8–10. + +- [ ] **Step 1: Run the existing test to confirm the starting state** + +Run: `./gradlew :driver-sync:test --tests "com.mongodb.client.ClientEncryptionTextExplicitEncryptionTest"` +Expected: with a configured 8.2+ server and libmongocrypt 1.19.1, the *current* (commented-out-guards) test FAILS or errors, because it passes `new EncryptOptions("TextPreview")` which libmongocrypt 1.19.1 rejects (`unsupported algorithm string "textPreview"`). This confirms the rewrite is needed. (If no server is configured, the test is skipped — note this and proceed; the rewrite is verified in Step 3.) + +- [ ] **Step 2: Replace the abstract test file** + +Overwrite `driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java` with: + +```java +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.client; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoException; +import com.mongodb.MongoNamespace; +import com.mongodb.WriteConcern; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.DropCollectionOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.StringOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.connection.ServerVersion; +import com.mongodb.fixture.EncryptionFixture; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static com.mongodb.ClusterFixture.getDefaultDatabaseName; +import static com.mongodb.ClusterFixture.getMongoCryptVersion; +import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled; +import static com.mongodb.ClusterFixture.isStandalone; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.Fixture.getDefaultDatabase; +import static com.mongodb.client.Fixture.getMongoClient; +import static com.mongodb.client.Fixture.getMongoClientSettings; +import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; +import static com.mongodb.fixture.EncryptionFixture.getKmsProviders; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static util.JsonPoweredTestHelper.getTestDocument; + +public abstract class AbstractClientEncryptionTextExplicitEncryptionTest { + + private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); + private boolean gaSupported; + private MongoClient explicitEncryptedClient; + private MongoClient autoEncryptedClient; + private MongoDatabase explicitEncryptedDatabase; + private MongoDatabase autoEncryptedDatabase; + private ClientEncryption clientEncryption; + private BsonBinary key1Id; + + protected abstract MongoClient createMongoClient(MongoClientSettings settings); + protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings); + + @BeforeEach + public void setUp() { + assumeTrue("String explicit encryption tests disabled", hasEncryptionTestsEnabled()); + assumeTrue("Requires newer MongoCrypt version", getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0); + assumeTrue(serverVersionAtLeast(8, 2)); + assumeFalse(isStandalone()); + + gaSupported = serverVersionAtLeast(9, 0); + + MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys"); + BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json"); + + Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL); + + if (gaSupported) { + createEncryptedCollection("prefix-suffix", "encryptedFields-prefix-suffix.json"); + createEncryptedCollection("prefix-suffix-ci-di", "encryptedFields-prefix-suffix-ci-di.json"); + } else { + createEncryptedCollection("prefix-suffix-preview", "encryptedFields-prefix-suffix-preview.json"); + } + createEncryptedCollection("substring", "encryptedFields-substring.json"); + createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); + + MongoCollection dataKeysCollection = getMongoClient() + .getDatabase(dataKeysNamespace.getDatabaseName()) + .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class) + .withWriteConcern(WriteConcern.MAJORITY); + dataKeysCollection.drop(); + dataKeysCollection.insertOne(key1Document); + key1Id = key1Document.getBinary("_id"); + + clientEncryption = createClientEncryption(ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(getMongoClientSettings()) + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build()); + + explicitEncryptedClient = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .bypassQueryAnalysis(true) + .build()) + .build()); + explicitEncryptedDatabase = explicitEncryptedClient.getDatabase(getDefaultDatabaseName()) + .withWriteConcern(WriteConcern.MAJORITY); + + autoEncryptedClient = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build()) + .build()); + autoEncryptedDatabase = autoEncryptedClient.getDatabase(getDefaultDatabaseName()) + .withWriteConcern(WriteConcern.MAJORITY); + + // Seed the prefix-suffix collection(s) with an encrypted "foobarbaz" document. + BsonBinary prefixSuffixSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), + new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + if (gaSupported) { + explicitEncryptedDatabase.getCollection("prefix-suffix") + .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); + } else { + explicitEncryptedDatabase.getCollection("prefix-suffix-preview") + .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); + } + + // Seed the substring collection with an encrypted "foobarbaz" document. + BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), + new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .substringOptions(BsonDocument.parse( + "{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")))); + explicitEncryptedDatabase.getCollection("substring") + .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); + } + + @Test + @DisplayName("Case 1: can find a document by prefix") + public void test1CanFindADocumentByPrefix() { + String queryType = gaSupported ? "prefix" : "prefixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForPrefix("foo", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrStartsWith(encrypted)).first(); + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 2: can find a document by suffix") + public void test2CanFindADocumentBySuffix() { + String queryType = gaSupported ? "suffix" : "suffixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForSuffix("baz", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrEndsWith(encrypted)).first(); + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 3: assert no document found by prefix") + public void test3AssertNoDocumentFoundByPrefix() { + String queryType = gaSupported ? "prefix" : "prefixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForPrefix("baz", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrStartsWith(encrypted)).first(); + assertNull(result); + } + + @Test + @DisplayName("Case 4: assert no document found by suffix") + public void test4AssertNoDocumentFoundBySuffix() { + String queryType = gaSupported ? "suffix" : "suffixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForSuffix("foo", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrEndsWith(encrypted)).first(); + assertNull(result); + } + + @Test + @DisplayName("Case 5: can find a document by substring") + public void test5CanFindADocumentBySubstring() { + BsonBinary encrypted = encryptForSubstring("bar", true, true); + Document result = explicitEncryptedDatabase.getCollection("substring") + .find(encStrContains(encrypted)).first(); + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 6: assert no document found by substring") + public void test6AssertNoDocumentFoundBySubstring() { + BsonBinary encrypted = encryptForSubstring("qux", true, true); + Document result = explicitEncryptedDatabase.getCollection("substring") + .find(encStrContains(encrypted)).first(); + assertNull(result); + } + + @Test + @DisplayName("Case 7: assert `contentionFactor` is required") + public void test7AssertContentionFactorIsRequired() { + assumeTrue(gaSupported); + EncryptOptions encryptOptions = new EncryptOptions("String") + .keyId(key1Id) + .queryType("prefix") + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + MongoException exception = assertThrows(MongoException.class, + () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions)); + assertTrue(exception.getMessage().contains("contention factor is required for string algorithm")); + } + + @Test + @DisplayName("Case 8: can find an auto-encrypted case-insensitively indexed document by prefix and suffix") + public void test8AutoEncryptedCaseInsensitivePrefixAndSuffix() { + assumeTrue(gaSupported); + autoEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .insertOne(new Document("encryptedText", "BingQiLin")); + + BsonBinary prefix = encryptForPrefix("bing", "prefix", false, false); + Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrStartsWith(prefix)).first(); + assertEncryptedTextEquals("BingQiLin", byPrefix); + + BsonBinary suffix = encryptForSuffix("lin", "suffix", false, false); + Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrEndsWith(suffix)).first(); + assertEncryptedTextEquals("BingQiLin", bySuffix); + } + + @Test + @DisplayName("Case 9: can find an auto-encrypted diacritic-insensitively indexed document by prefix and suffix") + public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { + assumeTrue(gaSupported); + autoEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .insertOne(new Document("encryptedText", "cafébarbäz")); + + BsonBinary prefix = encryptForPrefix("cafe", "prefix", false, false); + Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrStartsWith(prefix)).first(); + assertEncryptedTextEquals("cafébarbäz", byPrefix); + + BsonBinary suffix = encryptForSuffix("baz", "suffix", false, false); + Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrEndsWith(suffix)).first(); + assertEncryptedTextEquals("cafébarbäz", bySuffix); + } + + @Test + @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring") + public void test10AutoEncryptedCaseInsensitiveSubstring() { + autoEncryptedDatabase.getCollection("substring-ci-di") + .insertOne(new Document("encryptedText", "FooBarBaz")); + + BsonBinary substring = encryptForSubstring("bar", false, false); + Document result = explicitEncryptedDatabase.getCollection("substring-ci-di") + .find(encStrContains(substring)).first(); + assertEncryptedTextEquals("FooBarBaz", result); + } + + @AfterEach + @SuppressWarnings("try") + public void cleanUp() { + getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY).drop(); + try (ClientEncryption ignored = this.clientEncryption; + MongoClient ignored1 = this.explicitEncryptedClient; + MongoClient ignored2 = this.autoEncryptedClient + ) { + // just using try-with-resources to ensure they all get closed, even in the case of exceptions + } + } + + private void createEncryptedCollection(final String name, final String encryptedFieldsFile) { + BsonDocument encryptedFields = bsonDocumentFromPath(encryptedFieldsFile); + MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY); + database.getCollection(name).drop(new DropCollectionOptions().encryptedFields(encryptedFields)); + database.createCollection(name, new CreateCollectionOptions().encryptedFields(encryptedFields)); + } + + private BsonBinary encryptForPrefix(final String value, final String queryType, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType(queryType) + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + } + + private BsonBinary encryptForSuffix(final String value, final String queryType, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType(queryType) + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + } + + private BsonBinary encryptForSubstring(final String value, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType("substringPreview") + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .substringOptions(BsonDocument.parse( + "{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")))); + } + + private static Document encStrStartsWith(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrStartsWith", + new Document("input", "$encryptedText").append("prefix", encrypted))); + } + + private static Document encStrEndsWith(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrEndsWith", + new Document("input", "$encryptedText").append("suffix", encrypted))); + } + + private static Document encStrContains(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrContains", + new Document("input", "$encryptedText").append("substring", encrypted))); + } + + private static void assertDocumentEquals(final Document expectedDocument, final Document actualDocument) { + actualDocument.remove("__safeContent__"); + assertEquals(expectedDocument, actualDocument); + } + + private static void assertEncryptedTextEquals(final String expectedText, final Document actualDocument) { + assertEquals(expectedText, actualDocument.getString("encryptedText")); + } + + private static BsonDocument bsonDocumentFromPath(final String path) { + return getTestDocument("client-side-encryption/etc/data/" + path); + } +} +``` + +- [ ] **Step 3: Run the prose test against a server** + +Run (requires a configured non-standalone server 8.2+; set the URI): +`./gradlew :driver-sync:test --tests "com.mongodb.client.ClientEncryptionTextExplicitEncryptionTest" -Dorg.mongodb.test.uri="mongodb://localhost:27017"` +Expected: PASS. On a 9.0+ server, cases 1–10 run (GA query types). On an 8.2/8.3 server, cases 1–6 and 10 run with the preview path and cases 7–9 are skipped (`assumeTrue(gaSupported)`). + +- [ ] **Step 4: Run the reactive variant** + +Run: `./gradlew :driver-reactive-streams:test --tests "com.mongodb.reactivestreams.client.ClientEncryptionTextExplicitEncryptionTest" -Dorg.mongodb.test.uri="mongodb://localhost:27017"` +Expected: PASS (same gating). + +- [ ] **Step 5: Commit** + +```bash +./gradlew :driver-sync:spotlessApply +git add driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java +git commit -m "test(JAVA-6168): rewrite QE string explicit-encryption prose test to GA" +``` + +--- + +### Task 6: Verify unified QE-Text spec tests run (not skipped) + flag CI matrix + +**Files:** +- Inspect: `driver-sync/src/test/functional/com/mongodb/client/unified/ClientSideEncryptionTest.java` +- Inspect: `driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java` + +**Interfaces:** +- Consumes/Produces: none — verification + optional skip-list edit only. + +- [ ] **Step 1: Confirm the QE-Text unified tests are discovered** + +Run: `./gradlew :driver-sync:test --tests "com.mongodb.client.unified.ClientSideEncryptionTest" -Dorg.mongodb.test.uri="mongodb://localhost:27017" --info` and confirm test names containing `QE-Text-prefix`, `QE-Text-suffix`, `QE-Text-prefixPreview`, `QE-Text-suffixPreview`, `QE-Text-substringPreview` appear (executed or skipped by `runOnRequirements`, not absent). +Expected: those display names are present in the run. + +- [ ] **Step 2: Check for unintended skips** + +Search the modification/skip registry for any entry that would disable these: + +Run: `grep -ni "QE-Text\|text" driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java` +Expected: no skip that excludes the `QE-Text-*` files on supported server/libmongocrypt versions. If a stale skip exists (e.g. one added while the feature was preview/disabled), remove it and re-run Step 1. + +- [ ] **Step 3: Record the CI matrix requirement** + +The `QE-Text-prefixPreview`/`QE-Text-suffixPreview` unified tests declare `minServerVersion 8.2.0` / `maxServerVersion 8.99.99`. They only execute the auto-encryption preview path on a **pre-9.0** server. Confirm the encryption test matrix in `.evergreen/` runs against at least one 8.2/8.3 server in addition to 9.0+. **Do not modify `.evergreen/` config without human approval** — if the matrix lacks a pre-9.0 server, note it in the PR description and flag it to a human reviewer rather than editing CI config. + +- [ ] **Step 4: Commit (only if a stale skip was removed in Step 2)** + +```bash +./gradlew :driver-sync:spotlessApply +git add driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java +git commit -m "test(JAVA-6168): run QE-Text unified spec tests" +``` + +If no change was needed, skip the commit. + +--- + +## Self-Review + +**Spec coverage** (against `docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md`): +- §4 new `StringOptions` + deprecated `TextOptions` → Task 1. ✅ +- §4 `EncryptOptions.stringOptions`/deprecated `textOptions` + algorithm/queryType doc updates → Task 2. ✅ +- §5 `EncryptOptionsHelper` resolution (stringOptions → textOptions fallback) → Task 3. ✅ +- §5 mongodb-crypt doc-only refresh; no JNA change → Task 4. ✅ +- §6 prose test 27 rewrite (cases 1–10, GA/preview gating, autoEncryptedClient) → Task 5. ✅ +- §6 unified-test execution notes + CI pre-9.0 matrix → Task 6. ✅ +- §7 language wrappers reuse driver-core classes (no wrapper change) → covered by Task 5 "Produces" note (concrete subclasses unchanged); Kotlin/Scala inherit automatically. +- Out-of-scope items (substring GA / preview removal / lookup prose-25) correctly excluded. + +**Placeholder scan:** No TBD/TODO; all steps contain full code or exact commands. + +**Type consistency:** `StringOptions` fluent setters/getters used identically in Tasks 1–3 and Task 5; `EncryptOptions.stringOptions(StringOptions)`/`getStringOptions()` and deprecated `textOptions(TextOptions)`/`getTextOptions()` consistent across Tasks 2, 3, 5; `MongoExplicitEncryptOptions.getTextOptions()` (internal type, unchanged) used in Task 3 test. From 967af8a143a30aa00edf99c822db06c58ddbad7f Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 16:56:23 +0100 Subject: [PATCH 04/29] feat(JAVA-6168): add StringOptions and deprecate TextOptions --- .../client/model/vault/StringOptions.java | 189 ++++++++++++++++++ .../client/model/vault/TextOptions.java | 2 + .../client/model/vault/StringOptionsTest.java | 55 +++++ 3 files changed, 246 insertions(+) create mode 100644 driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java create mode 100644 driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java new file mode 100644 index 00000000000..a80188b1870 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java @@ -0,0 +1,189 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.client.model.vault; + +import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Reason; +import com.mongodb.lang.Nullable; +import org.bson.BsonDocument; + +/** + * String options for a Queryable Encryption field that supports string queries (prefix, suffix, and substring). + * + *

    Note: StringOptions is in Alpha and subject to backwards breaking changes. The {@code substring} query support + * is experimental (preview) and may change in a future non-major release. + * + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ +@Alpha(Reason.SERVER) +public class StringOptions { + private Boolean caseSensitive; + private Boolean diacriticSensitive; + @Nullable + private BsonDocument prefixOptions; + @Nullable + private BsonDocument suffixOptions; + @Nullable + private BsonDocument substringOptions; + + /** + * Construct a new instance + */ + public StringOptions() { + } + + /** + * @return true if string indexes for this field are case sensitive. + */ + public boolean getCaseSensitive() { + return caseSensitive; + } + + /** + * Set case sensitivity + * + * @param caseSensitive true if string indexes are case sensitive + * @return this + */ + public StringOptions caseSensitive(final boolean caseSensitive) { + this.caseSensitive = caseSensitive; + return this; + } + + /** + * @return true if string indexes are diacritic sensitive + */ + public boolean getDiacriticSensitive() { + return diacriticSensitive; + } + + /** + * Set diacritic sensitivity + * + * @param diacriticSensitive true if string indexes are diacritic sensitive + * @return this + */ + public StringOptions diacriticSensitive(final boolean diacriticSensitive) { + this.diacriticSensitive = diacriticSensitive; + return this; + } + + /** + * Set the prefix options. + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param prefixOptions the prefix options or null + * @return this + */ + public StringOptions prefixOptions(@Nullable final BsonDocument prefixOptions) { + this.prefixOptions = prefixOptions; + return this; + } + + /** + * @see #prefixOptions(BsonDocument) + * @return the prefix options document or null + */ + @Nullable + public BsonDocument getPrefixOptions() { + return prefixOptions; + } + + /** + * Set the suffix options. + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param suffixOptions the suffix options or null + * @return this + */ + public StringOptions suffixOptions(@Nullable final BsonDocument suffixOptions) { + this.suffixOptions = suffixOptions; + return this; + } + + /** + * @see #suffixOptions(BsonDocument) + * @return the suffix options document or null + */ + @Nullable + public BsonDocument getSuffixOptions() { + return suffixOptions; + } + + /** + * Set the substring options. + * + *

    Note: substring string queries are experimental (preview) and may change in a future non-major release.

    + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
    +     *    strMaxLength: BsonInt32,
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param substringOptions the substring options or null + * @return this + */ + public StringOptions substringOptions(@Nullable final BsonDocument substringOptions) { + this.substringOptions = substringOptions; + return this; + } + + /** + * @see #substringOptions(BsonDocument) + * @return the substring options document or null + */ + @Nullable + public BsonDocument getSubstringOptions() { + return substringOptions; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java index 34dcd0d806d..588ce161e77 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java @@ -29,7 +29,9 @@ * @since 5.6 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + * @deprecated Use {@link StringOptions} instead. */ +@Deprecated @Alpha(Reason.SERVER) public class TextOptions { private Boolean caseSensitive; diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java new file mode 100644 index 00000000000..3652b3c5392 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.client.model.vault; + +import org.bson.BsonDocument; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StringOptionsTest { + + @Test + void shouldRoundTripAllProperties() { + BsonDocument prefix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"); + BsonDocument suffix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"); + BsonDocument substring = BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}"); + + StringOptions options = new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(prefix) + .suffixOptions(suffix) + .substringOptions(substring); + + assertTrue(options.getCaseSensitive()); + assertTrue(options.getDiacriticSensitive()); + assertEquals(prefix, options.getPrefixOptions()); + assertEquals(suffix, options.getSuffixOptions()); + assertEquals(substring, options.getSubstringOptions()); + } + + @Test + void shouldDefaultOptionDocumentsToNull() { + StringOptions options = new StringOptions(); + assertNull(options.getPrefixOptions()); + assertNull(options.getSuffixOptions()); + assertNull(options.getSubstringOptions()); + } +} From 6d36cc5ea37902eba453780fa31aeeec03254e03 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 17:00:21 +0100 Subject: [PATCH 05/29] fix(JAVA-6168): default StringOptions/TextOptions sensitivity flags to false --- .../com/mongodb/client/model/vault/StringOptions.java | 4 ++-- .../main/com/mongodb/client/model/vault/TextOptions.java | 4 ++-- .../com/mongodb/client/model/vault/StringOptionsTest.java | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java index a80188b1870..5077fa61e64 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java @@ -33,8 +33,8 @@ */ @Alpha(Reason.SERVER) public class StringOptions { - private Boolean caseSensitive; - private Boolean diacriticSensitive; + private Boolean caseSensitive = false; + private Boolean diacriticSensitive = false; @Nullable private BsonDocument prefixOptions; @Nullable diff --git a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java index 588ce161e77..bcd5a7ff2da 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java @@ -34,8 +34,8 @@ @Deprecated @Alpha(Reason.SERVER) public class TextOptions { - private Boolean caseSensitive; - private Boolean diacriticSensitive; + private Boolean caseSensitive = false; + private Boolean diacriticSensitive = false; @Nullable private BsonDocument prefixOptions; @Nullable diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java index 3652b3c5392..04f20239423 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java @@ -19,6 +19,7 @@ import org.bson.BsonDocument; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -52,4 +53,11 @@ void shouldDefaultOptionDocumentsToNull() { assertNull(options.getSuffixOptions()); assertNull(options.getSubstringOptions()); } + + @Test + void shouldDefaultBooleanFlagsToFalse() { + StringOptions options = new StringOptions(); + assertFalse(options.getCaseSensitive()); + assertFalse(options.getDiacriticSensitive()); + } } From 92481747bb4ec7fa3891c8889e4619cc72809af0 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 17:12:45 +0100 Subject: [PATCH 06/29] feat(JAVA-6168): add EncryptOptions.stringOptions and deprecate textOptions --- .../client/model/vault/EncryptOptions.java | 52 ++++++++++++++++--- .../model/vault/EncryptOptionsTest.java | 42 +++++++++++++++ 2 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java diff --git a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java index f509f8b3ea3..d1574e798be 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java @@ -34,6 +34,7 @@ public class EncryptOptions { private String queryType; private RangeOptions rangeOptions; private TextOptions textOptions; + private StringOptions stringOptions; /** * Construct an instance with the given algorithm. @@ -54,12 +55,13 @@ public EncryptOptions(final String algorithm) { *
  • Indexed
  • *
  • Unindexed
  • *
  • Range
  • - *
  • TextPreview
  • + *
  • String
  • * * - *

    The "TextPreview" algorithm is in preview and should be used for experimental workloads only. - * These features are unstable and their security is not guaranteed until released as Generally Available (GA). - * The GA version of these features may not be backwards compatible with the preview version.

    + *

    The "String" algorithm supports Queryable Encryption prefix and suffix string queries, and (in preview) + * substring queries. Use the "String" algorithm with query types "prefix"/"suffix" (server 9.0+) or the + * deprecated aliases "prefixPreview"/"suffixPreview" (server 8.2 to pre-9.0), and "substringPreview" + * (experimental).

    * * @return the encryption algorithm */ @@ -149,8 +151,12 @@ public Long getContentionFactor() { /** * The QueryType. * - *

    Currently, we support only "equality", "range", "prefixPreview", "suffixPreview" or "substringPreview" queryType.

    - *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "TextPreview".

    + *

    Currently, we support only "equality", "range", "prefix", "suffix", "prefixPreview", "suffixPreview" or + * "substringPreview" queryType.

    + *

    The "prefix", "suffix", "prefixPreview", "suffixPreview" and "substringPreview" query types are only valid + * with the "String" algorithm. "prefixPreview"/"suffixPreview" are deprecated aliases supported for servers 8.2 + * to pre-9.0; use "prefix"/"suffix" on server 9.0+.

    + *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "String".

    * @param queryType the query type * @return this * @since 4.7 @@ -211,7 +217,9 @@ public RangeOptions getRangeOptions() { * @since 5.6 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + * @deprecated Use {@link #stringOptions(StringOptions)} instead. */ + @Deprecated @Alpha(Reason.SERVER) public EncryptOptions textOptions(@Nullable final TextOptions textOptions) { this.textOptions = textOptions; @@ -225,13 +233,45 @@ public EncryptOptions textOptions(@Nullable final TextOptions textOptions) { * @since 5.6 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + * @deprecated Use {@link #getStringOptions()} instead. */ + @Deprecated @Alpha(Reason.SERVER) @Nullable public TextOptions getTextOptions() { return textOptions; } + /** + * The StringOptions + * + *

    It is an error to set StringOptions when the algorithm is not "String". + * @param stringOptions the string options + * @return this + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ + @Alpha(Reason.SERVER) + public EncryptOptions stringOptions(@Nullable final StringOptions stringOptions) { + this.stringOptions = stringOptions; + return this; + } + + /** + * Gets the StringOptions + * @see #stringOptions(StringOptions) + * @return the string options or null if not set + * @since 5.6 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ + @Alpha(Reason.SERVER) + @Nullable + public StringOptions getStringOptions() { + return stringOptions; + } + @Override public String toString() { return "EncryptOptions{" diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java new file mode 100644 index 00000000000..cf4b1216da5 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.client.model.vault; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +class EncryptOptionsTest { + + @Test + void shouldStoreStringOptions() { + StringOptions stringOptions = new StringOptions().caseSensitive(true); + EncryptOptions options = new EncryptOptions("String").stringOptions(stringOptions); + assertSame(stringOptions, options.getStringOptions()); + assertNull(options.getTextOptions()); + } + + @Test + @SuppressWarnings("deprecation") + void shouldStoreDeprecatedTextOptionsIndependently() { + TextOptions textOptions = new TextOptions().caseSensitive(true); + EncryptOptions options = new EncryptOptions("String").textOptions(textOptions); + assertSame(textOptions, options.getTextOptions()); + assertNull(options.getStringOptions()); + } +} From 5c2b1574f47df25a504fbe2ef43cad07230cae44 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 17:31:40 +0100 Subject: [PATCH 07/29] fix(JAVA-6168): include options in EncryptOptions.toString and correct @since to 5.9 --- .../com/mongodb/client/model/vault/EncryptOptions.java | 8 +++++--- .../com/mongodb/client/model/vault/StringOptions.java | 2 +- .../mongodb/client/model/vault/EncryptOptionsTest.java | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java index d1574e798be..6235e64b988 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java @@ -211,7 +211,7 @@ public RangeOptions getRangeOptions() { /** * The TextOptions * - *

    It is an error to set TextOptions when the algorithm is not "TextPreview". + *

    It is an error to set TextOptions when the algorithm is not "String". * @param textOptions the text options * @return this * @since 5.6 @@ -248,7 +248,7 @@ public TextOptions getTextOptions() { *

    It is an error to set StringOptions when the algorithm is not "String". * @param stringOptions the string options * @return this - * @since 5.6 + * @since 5.9 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption */ @@ -262,7 +262,7 @@ public EncryptOptions stringOptions(@Nullable final StringOptions stringOptions) * Gets the StringOptions * @see #stringOptions(StringOptions) * @return the string options or null if not set - * @since 5.6 + * @since 5.9 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption */ @@ -281,6 +281,8 @@ public String toString() { + ", contentionFactor=" + contentionFactor + ", queryType='" + queryType + '\'' + ", rangeOptions=" + rangeOptions + + ", textOptions=" + textOptions + + ", stringOptions=" + stringOptions + '}'; } } diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java index 5077fa61e64..2b59cfdb999 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java @@ -27,7 +27,7 @@ *

    Note: StringOptions is in Alpha and subject to backwards breaking changes. The {@code substring} query support * is experimental (preview) and may change in a future non-major release. * - * @since 5.6 + * @since 5.9 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption */ diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java index cf4b1216da5..2859d728e99 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; class EncryptOptionsTest { @@ -39,4 +40,10 @@ void shouldStoreDeprecatedTextOptionsIndependently() { assertSame(textOptions, options.getTextOptions()); assertNull(options.getStringOptions()); } + + @Test + void toStringShouldIncludeStringOptions() { + EncryptOptions options = new EncryptOptions("String").stringOptions(new StringOptions()); + assertTrue(options.toString().contains("stringOptions=")); + } } From 3d9dcd2ce5ad4f14a65c7a2d5b28d7a749f7896d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 17:33:53 +0100 Subject: [PATCH 08/29] feat(JAVA-6168): resolve stringOptions with textOptions fallback in EncryptOptionsHelper --- .../client/vault/EncryptOptionsHelper.java | 47 +++++++++---- .../vault/EncryptOptionsHelperTest.java | 66 +++++++++++++++++++ 2 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java diff --git a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java index 2b472668d98..5161cfc6c16 100644 --- a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java +++ b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java @@ -17,8 +17,10 @@ import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.model.vault.RangeOptions; +import com.mongodb.client.model.vault.StringOptions; import com.mongodb.client.model.vault.TextOptions; import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions; +import com.mongodb.lang.Nullable; import org.bson.BsonBoolean; import org.bson.BsonDocument; import org.bson.BsonInt32; @@ -73,31 +75,52 @@ public static MongoExplicitEncryptOptions asMongoExplicitEncryptOptions(final En encryptOptionsBuilder.rangeOptions(rangeOptionsBsonDocument); } - TextOptions textOptions = options.getTextOptions(); - if (textOptions != null) { - BsonDocument textOptionsDocument = new BsonDocument(); - textOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(textOptions.getCaseSensitive())); - textOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(textOptions.getDiacriticSensitive())); + StringOptions stringOptions = resolveStringOptions(options); + if (stringOptions != null) { + BsonDocument stringOptionsDocument = new BsonDocument(); + stringOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(stringOptions.getCaseSensitive())); + stringOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(stringOptions.getDiacriticSensitive())); - BsonDocument substringOptions = textOptions.getSubstringOptions(); + BsonDocument substringOptions = stringOptions.getSubstringOptions(); if (substringOptions != null) { - textOptionsDocument.put("substring", substringOptions); + stringOptionsDocument.put("substring", substringOptions); } - BsonDocument prefixOptions = textOptions.getPrefixOptions(); + BsonDocument prefixOptions = stringOptions.getPrefixOptions(); if (prefixOptions != null) { - textOptionsDocument.put("prefix", prefixOptions); + stringOptionsDocument.put("prefix", prefixOptions); } - BsonDocument suffixOptions = textOptions.getSuffixOptions(); + BsonDocument suffixOptions = stringOptions.getSuffixOptions(); if (suffixOptions != null) { - textOptionsDocument.put("suffix", suffixOptions); + stringOptionsDocument.put("suffix", suffixOptions); } - encryptOptionsBuilder.textOptions(textOptionsDocument); + encryptOptionsBuilder.textOptions(stringOptionsDocument); } return encryptOptionsBuilder.build(); } + + @SuppressWarnings("deprecation") + @Nullable + private static StringOptions resolveStringOptions(final EncryptOptions options) { + StringOptions stringOptions = options.getStringOptions(); + if (stringOptions != null) { + return stringOptions; + } + TextOptions textOptions = options.getTextOptions(); + if (textOptions == null) { + return null; + } + return new StringOptions() + .caseSensitive(textOptions.getCaseSensitive()) + .diacriticSensitive(textOptions.getDiacriticSensitive()) + .prefixOptions(textOptions.getPrefixOptions()) + .suffixOptions(textOptions.getSuffixOptions()) + .substringOptions(textOptions.getSubstringOptions()); + } + private EncryptOptionsHelper() { } } + diff --git a/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java new file mode 100644 index 00000000000..ea015185c55 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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.mongodb.internal.client.vault; + +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.StringOptions; +import com.mongodb.client.model.vault.TextOptions; +import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions; +import org.bson.BsonDocument; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class EncryptOptionsHelperTest { + + @Test + void shouldMapStringOptionsToTextOptionsDocument() { + EncryptOptions options = new EncryptOptions("String").stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(false) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); + + assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: false, " + + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), + result.getTextOptions()); + } + + @Test + @SuppressWarnings("deprecation") + void shouldFallBackToDeprecatedTextOptions() { + EncryptOptions options = new EncryptOptions("String").textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); + + assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, " + + "suffix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), + result.getTextOptions()); + } + + @Test + void shouldLeaveTextOptionsNullWhenNeitherSet() { + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions( + new EncryptOptions("Indexed")); + assertNull(result.getTextOptions()); + } +} From 283d2781ea65f8b05cf2755bc0d184627b8c1736 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 17:36:22 +0100 Subject: [PATCH 09/29] docs(JAVA-6168): refer to String algorithm in MongoExplicitEncryptOptions --- .../internal/crypt/capi/MongoExplicitEncryptOptions.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java index c08608ca595..992b5bf479d 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java @@ -87,7 +87,7 @@ public Builder algorithm(final String algorithm) { /** * The contention factor. * - *

    Only applies when algorithm is "Indexed", "Range", or "TextPreview".

    + *

    Only applies when algorithm is "Indexed", "Range", or "String".

    * @param contentionFactor the contention factor * @return this * @since 1.5 @@ -100,7 +100,7 @@ public Builder contentionFactor(final Long contentionFactor) { /** * The QueryType. * - *

    Only applies when algorithm is "Indexed", "Range", or "TextPreview".

    + *

    Only applies when algorithm is "Indexed", "Range", or "String".

    * * @param queryType the query type * @return this @@ -128,7 +128,7 @@ public Builder rangeOptions(final BsonDocument rangeOptions) { /** * The Text Options. * - *

    Only applies when algorithm is "TextPreview".

    + *

    Only applies when algorithm is "String".

    * * @param textOptions the text options * @return this From 818222ac98deed90c7a237d3d1a52ff145654262 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 17:41:29 +0100 Subject: [PATCH 10/29] test(JAVA-6168): rewrite QE string explicit-encryption prose test to GA --- ...tEncryptionTextExplicitEncryptionTest.java | 379 ++++++++++-------- 1 file changed, 213 insertions(+), 166 deletions(-) diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java index 1677e49a66f..5093a2e8343 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java @@ -25,7 +25,7 @@ import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.DropCollectionOptions; import com.mongodb.client.model.vault.EncryptOptions; -import com.mongodb.client.model.vault.TextOptions; +import com.mongodb.client.model.vault.StringOptions; import com.mongodb.client.vault.ClientEncryption; import com.mongodb.connection.ServerVersion; import com.mongodb.fixture.EncryptionFixture; @@ -51,275 +51,322 @@ import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; import static com.mongodb.fixture.EncryptionFixture.getKmsProviders; import static java.util.Arrays.asList; -import static org.junit.Assume.assumeTrue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static util.JsonPoweredTestHelper.getTestDocument; public abstract class AbstractClientEncryptionTextExplicitEncryptionTest { - private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 15, 1)); - private MongoClient encryptedClient; - private MongoDatabase encryptedDatabase; + private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); + private boolean gaSupported; + private MongoClient explicitEncryptedClient; + private MongoClient autoEncryptedClient; + private MongoDatabase explicitEncryptedDatabase; + private MongoDatabase autoEncryptedDatabase; private ClientEncryption clientEncryption; private BsonBinary key1Id; protected abstract MongoClient createMongoClient(MongoClientSettings settings); protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings); - @BeforeEach public void setUp() { - assumeTrue("Text explicit encryption tests disabled", hasEncryptionTestsEnabled()); - assumeTrue("Requires newer MongoCrypt version", getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0); + assumeTrue(hasEncryptionTestsEnabled(), "String explicit encryption tests disabled"); + assumeTrue(getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0, "Requires newer MongoCrypt version"); assumeTrue(serverVersionAtLeast(8, 2)); - // TODO-JAVA-6168 update prose tests for post 9.0 - assumeTrue(!serverVersionAtLeast(9, 0)); assumeFalse(isStandalone()); + gaSupported = serverVersionAtLeast(9, 0); + MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys"); - BsonDocument encryptedFieldsPrefixSuffix = bsonDocumentFromPath("encryptedFields-prefix-suffix.json"); - BsonDocument encryptedFieldsSubstring = bsonDocumentFromPath("encryptedFields-substring.json"); BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json"); - MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY); - database.getCollection("prefix-suffix") - .drop(new DropCollectionOptions().encryptedFields(encryptedFieldsPrefixSuffix)); - database.createCollection("prefix-suffix", - new CreateCollectionOptions().encryptedFields(encryptedFieldsPrefixSuffix)); + Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL); - database.getCollection("substring") - .drop(new DropCollectionOptions().encryptedFields(encryptedFieldsSubstring)); - database.createCollection("substring", - new CreateCollectionOptions().encryptedFields(encryptedFieldsSubstring)); + if (gaSupported) { + createEncryptedCollection("prefix-suffix", "encryptedFields-prefix-suffix.json"); + createEncryptedCollection("prefix-suffix-ci-di", "encryptedFields-prefix-suffix-ci-di.json"); + } else { + createEncryptedCollection("prefix-suffix-preview", "encryptedFields-prefix-suffix-preview.json"); + } + createEncryptedCollection("substring", "encryptedFields-substring.json"); + createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); MongoCollection dataKeysCollection = getMongoClient() .getDatabase(dataKeysNamespace.getDatabaseName()) .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class) .withWriteConcern(WriteConcern.MAJORITY); - dataKeysCollection.drop(); dataKeysCollection.insertOne(key1Document); key1Id = key1Document.getBinary("_id"); - Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL); - clientEncryption = createClientEncryption(ClientEncryptionSettings.builder() .keyVaultMongoClientSettings(getMongoClientSettings()) .keyVaultNamespace(dataKeysNamespace.getFullName()) .kmsProviders(kmsProviders) .build()); - encryptedClient = createMongoClient(getMongoClientSettingsBuilder() - .autoEncryptionSettings( - AutoEncryptionSettings.builder() - .keyVaultNamespace(dataKeysNamespace.getFullName()) - .kmsProviders(kmsProviders) - .bypassQueryAnalysis(true) - .build()) + explicitEncryptedClient = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .bypassQueryAnalysis(true) + .build()) .build()); + explicitEncryptedDatabase = explicitEncryptedClient.getDatabase(getDefaultDatabaseName()) + .withWriteConcern(WriteConcern.MAJORITY); - encryptedDatabase = encryptedClient.getDatabase(getDefaultDatabaseName()).withWriteConcern(WriteConcern.MAJORITY); - - EncryptOptions prefixSuffixEncryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary foobarbaz = clientEncryption.encrypt(new BsonString("foobarbaz"), prefixSuffixEncryptOptions); - - encryptedDatabase - .getCollection("prefix-suffix") - .insertOne(new Document("_id", 0).append("encryptedText", foobarbaz)); + autoEncryptedClient = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build()) + .build()); + autoEncryptedDatabase = autoEncryptedClient.getDatabase(getDefaultDatabaseName()) + .withWriteConcern(WriteConcern.MAJORITY); - EncryptOptions substringEncryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - foobarbaz = clientEncryption.encrypt(new BsonString("foobarbaz"), substringEncryptOptions); + // Seed the prefix-suffix collection(s) with an encrypted "foobarbaz" document. + BsonBinary prefixSuffixSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), + new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + if (gaSupported) { + explicitEncryptedDatabase.getCollection("prefix-suffix") + .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); + } else { + explicitEncryptedDatabase.getCollection("prefix-suffix-preview") + .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); + } - encryptedDatabase - .getCollection("substring") - .insertOne(new Document("_id", 0).append("encryptedText", foobarbaz)); + // Seed the substring collection with an encrypted "foobarbaz" document. + BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), + new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .substringOptions(BsonDocument.parse( + "{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")))); + explicitEncryptedDatabase.getCollection("substring") + .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); } @Test @DisplayName("Case 1: can find a document by prefix") public void test1CanFindADocumentByPrefix() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("prefixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("foo"), encryptOptions); - Document result = encryptedDatabase.getCollection("prefix-suffix") - .find(new Document("$expr", - new Document("$encStrStartsWith", - new Document("input", "$encryptedText").append("prefix", encrypted)))).first(); - + String queryType = gaSupported ? "prefix" : "prefixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForPrefix("foo", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrStartsWith(encrypted)).first(); assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); } @Test @DisplayName("Case 2: can find a document by suffix") public void test2CanFindADocumentBySuffix() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("suffixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("baz"), encryptOptions); - Document result = encryptedDatabase.getCollection("prefix-suffix") - .find(new Document("$expr", - new Document("$encStrEndsWith", - new Document("input", "$encryptedText").append("suffix", encrypted)))).first(); - + String queryType = gaSupported ? "suffix" : "suffixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForSuffix("baz", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrEndsWith(encrypted)).first(); assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); } @Test @DisplayName("Case 3: assert no document found by prefix") public void test3AssertNoDocumentFoundByPrefix() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("prefixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("baz"), encryptOptions); - Document result = encryptedDatabase.getCollection("prefix-suffix") - .find(new Document("$expr", - new Document("$encStrStartsWith", - new Document("input", "$encryptedText").append("prefix", encrypted)))).first(); - + String queryType = gaSupported ? "prefix" : "prefixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForPrefix("baz", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrStartsWith(encrypted)).first(); assertNull(result); } @Test @DisplayName("Case 4: assert no document found by suffix") - public void test4AssertNoDocumentFoundByPrefix() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("suffixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("foo"), encryptOptions); - Document result = encryptedDatabase.getCollection("prefix-suffix") - .find(new Document("$expr", - new Document("$encStrEndsWith", - new Document("input", "$encryptedText").append("suffix", encrypted)))).first(); - + public void test4AssertNoDocumentFoundBySuffix() { + String queryType = gaSupported ? "suffix" : "suffixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForSuffix("foo", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrEndsWith(encrypted)).first(); assertNull(result); } @Test @DisplayName("Case 5: can find a document by substring") public void test5CanFindADocumentBySubstring() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("substringPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("bar"), encryptOptions); - Document result = encryptedDatabase.getCollection("substring") - .find(new Document("$expr", - new Document("$encStrContains", - new Document("input", "$encryptedText").append("substring", encrypted)))).first(); - + BsonBinary encrypted = encryptForSubstring("bar", true, true); + Document result = explicitEncryptedDatabase.getCollection("substring") + .find(encStrContains(encrypted)).first(); assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); } @Test @DisplayName("Case 6: assert no document found by substring") public void test6AssertNoDocumentFoundBySubstring() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("substringPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("qux"), encryptOptions); - Document result = encryptedDatabase.getCollection("substring") - .find(new Document("$expr", - new Document("$encStrContains", - new Document("input", "$encryptedText").append("substring", encrypted)))).first(); - + BsonBinary encrypted = encryptForSubstring("qux", true, true); + Document result = explicitEncryptedDatabase.getCollection("substring") + .find(encStrContains(encrypted)).first(); assertNull(result); } @Test @DisplayName("Case 7: assert `contentionFactor` is required") public void test7AssertContentionFactorIsRequired() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") + assumeTrue(gaSupported); + EncryptOptions encryptOptions = new EncryptOptions("String") .keyId(key1Id) - .queryType("prefixPreview") - .textOptions(new TextOptions() + .queryType("prefix") + .stringOptions(new StringOptions() .caseSensitive(true) .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - MongoException exception = assertThrows(MongoException.class, () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions)); - assertTrue(exception.getMessage().contains("contention factor is required for textPreview algorithm")); + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + MongoException exception = assertThrows(MongoException.class, + () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions)); + assertTrue(exception.getMessage().contains("contention factor is required for string algorithm")); + } + + @Test + @DisplayName("Case 8: can find an auto-encrypted case-insensitively indexed document by prefix and suffix") + public void test8AutoEncryptedCaseInsensitivePrefixAndSuffix() { + assumeTrue(gaSupported); + autoEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .insertOne(new Document("encryptedText", "BingQiLin")); + + BsonBinary prefix = encryptForPrefix("bing", "prefix", false, false); + Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrStartsWith(prefix)).first(); + assertEncryptedTextEquals("BingQiLin", byPrefix); + + BsonBinary suffix = encryptForSuffix("lin", "suffix", false, false); + Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrEndsWith(suffix)).first(); + assertEncryptedTextEquals("BingQiLin", bySuffix); } + @Test + @DisplayName("Case 9: can find an auto-encrypted diacritic-insensitively indexed document by prefix and suffix") + public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { + assumeTrue(gaSupported); + autoEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .insertOne(new Document("encryptedText", "cafébarbäz")); + + BsonBinary prefix = encryptForPrefix("cafe", "prefix", false, false); + Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrStartsWith(prefix)).first(); + assertEncryptedTextEquals("cafébarbäz", byPrefix); + + BsonBinary suffix = encryptForSuffix("baz", "suffix", false, false); + Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrEndsWith(suffix)).first(); + assertEncryptedTextEquals("cafébarbäz", bySuffix); + } + + @Test + @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring") + public void test10AutoEncryptedCaseInsensitiveSubstring() { + autoEncryptedDatabase.getCollection("substring-ci-di") + .insertOne(new Document("encryptedText", "FooBarBaz")); + + BsonBinary substring = encryptForSubstring("bar", false, false); + Document result = explicitEncryptedDatabase.getCollection("substring-ci-di") + .find(encStrContains(substring)).first(); + assertEncryptedTextEquals("FooBarBaz", result); + } @AfterEach @SuppressWarnings("try") public void cleanUp() { - //noinspection EmptyTryBlock getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY).drop(); try (ClientEncryption ignored = this.clientEncryption; - MongoClient ignored1 = this.encryptedClient + MongoClient ignored1 = this.explicitEncryptedClient; + MongoClient ignored2 = this.autoEncryptedClient ) { // just using try-with-resources to ensure they all get closed, even in the case of exceptions } } + private void createEncryptedCollection(final String name, final String encryptedFieldsFile) { + BsonDocument encryptedFields = bsonDocumentFromPath(encryptedFieldsFile); + MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY); + database.getCollection(name).drop(new DropCollectionOptions().encryptedFields(encryptedFields)); + database.createCollection(name, new CreateCollectionOptions().encryptedFields(encryptedFields)); + } + + private BsonBinary encryptForPrefix(final String value, final String queryType, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType(queryType) + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + } + + private BsonBinary encryptForSuffix(final String value, final String queryType, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType(queryType) + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + } + + private BsonBinary encryptForSubstring(final String value, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType("substringPreview") + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .substringOptions(BsonDocument.parse( + "{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")))); + } + + private static Document encStrStartsWith(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrStartsWith", + new Document("input", "$encryptedText").append("prefix", encrypted))); + } + + private static Document encStrEndsWith(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrEndsWith", + new Document("input", "$encryptedText").append("suffix", encrypted))); + } + + private static Document encStrContains(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrContains", + new Document("input", "$encryptedText").append("substring", encrypted))); + } + private static void assertDocumentEquals(final Document expectedDocument, final Document actualDocument) { actualDocument.remove("__safeContent__"); assertEquals(expectedDocument, actualDocument); } + private static void assertEncryptedTextEquals(final String expectedText, final Document actualDocument) { + assertEquals(expectedText, actualDocument.getString("encryptedText")); + } + private static BsonDocument bsonDocumentFromPath(final String path) { return getTestDocument("client-side-encryption/etc/data/" + path); } From 7b57f7e56439e158acd831036a30d0ba683645c3 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 17:46:28 +0100 Subject: [PATCH 11/29] test(JAVA-6168): add prose test case 11 and null-guard assertion --- ...ClientEncryptionTextExplicitEncryptionTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java index 5093a2e8343..fa7497606de 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java @@ -52,6 +52,7 @@ import static com.mongodb.fixture.EncryptionFixture.getKmsProviders; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -287,6 +288,18 @@ public void test10AutoEncryptedCaseInsensitiveSubstring() { assertEncryptedTextEquals("FooBarBaz", result); } + @Test + @DisplayName("Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring") + public void test11AutoEncryptedDiacriticInsensitiveSubstring() { + autoEncryptedDatabase.getCollection("substring-ci-di") + .insertOne(new Document("encryptedText", "foocafébaz")); + + BsonBinary substring = encryptForSubstring("cafe", false, false); + Document result = explicitEncryptedDatabase.getCollection("substring-ci-di") + .find(encStrContains(substring)).first(); + assertEncryptedTextEquals("foocafébaz", result); + } + @AfterEach @SuppressWarnings("try") public void cleanUp() { @@ -364,6 +377,7 @@ private static void assertDocumentEquals(final Document expectedDocument, final } private static void assertEncryptedTextEquals(final String expectedText, final Document actualDocument) { + assertNotNull(actualDocument); assertEquals(expectedText, actualDocument.getString("encryptedText")); } From 796a0ca2f70cf7a8174bfc007eafd9baa80dfbe4 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 17:51:58 +0100 Subject: [PATCH 12/29] style(JAVA-6168): remove trailing blank line in EncryptOptionsHelper --- .../com/mongodb/internal/client/vault/EncryptOptionsHelper.java | 1 - 1 file changed, 1 deletion(-) diff --git a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java index 5161cfc6c16..7a8dab950f4 100644 --- a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java +++ b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java @@ -123,4 +123,3 @@ private static StringOptions resolveStringOptions(final EncryptOptions options) private EncryptOptionsHelper() { } } - From 1ed7978d0fce7817f2ae12ee1e94f1ebd05356f1 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 18:01:59 +0100 Subject: [PATCH 13/29] build(JAVA-6168): upgrade libmongocrypt to 1.19.1 Provides GA support for QE 'prefix'/'suffix' query types and the 'string' algorithm (formerly 'textPreview'), and restores 'prefixPreview'/'suffixPreview' for pre-9.0 servers. Required by the Text->String API rename. --- mongodb-crypt/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 8c11aabfda8..81a6d20ad3c 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -67,7 +67,7 @@ val jnaResources: String = System.getProperty("jna.library.path", jnaLibsPath) // Download the libmongocrypt per-platform tarballs (and their signatures) to jnaDownloadsDir. // To upgrade: change downloadRevision, run `./gradlew clean downloadJnaLibs`, and verify the build. -val downloadRevision = "1.18.1" +val downloadRevision = "1.19.1" val downloadUrlBase = "https://github.com/mongodb/libmongocrypt/releases/download/$downloadRevision" /** From 89b55e6e383e9ca38bfdb89736cd5378f478eb60 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 29 Jun 2026 18:01:59 +0100 Subject: [PATCH 14/29] test(JAVA-6168): bump spec submodule to QE prefix/suffix GA tests Updates testing/resources/specifications 4484038..8990543, bringing the GA QE-Text unified tests and String Explicit Encryption prose test (DRIVERS-3321, DRIVERS-3470). --- testing/resources/specifications | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/resources/specifications b/testing/resources/specifications index 44840386103..8990543a680 160000 --- a/testing/resources/specifications +++ b/testing/resources/specifications @@ -1 +1 @@ -Subproject commit 44840386103b93e5ebb7d7d595ca605c44eb6e08 +Subproject commit 8990543a6808568f2246a82a209b75627cb71cbf From 7e5ae583f08bfbad21c654a15db81aab8bb4b750 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 15:37:21 +0100 Subject: [PATCH 15/29] build(JAVA-6244): upgrade libmongocrypt to 1.20.0 for QE GA substring libmongocrypt 1.20.0 adds stable support for substring queries (the GA 'substring' query type), unblocking DRIVERS-3540/JAVA-6244. --- mongodb-crypt/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 81a6d20ad3c..0d233d410dd 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -67,7 +67,7 @@ val jnaResources: String = System.getProperty("jna.library.path", jnaLibsPath) // Download the libmongocrypt per-platform tarballs (and their signatures) to jnaDownloadsDir. // To upgrade: change downloadRevision, run `./gradlew clean downloadJnaLibs`, and verify the build. -val downloadRevision = "1.19.1" +val downloadRevision = "1.20.0" val downloadUrlBase = "https://github.com/mongodb/libmongocrypt/releases/download/$downloadRevision" /** From 71981f816dfdd3ede874a4ebd91c80d7e8ee3879 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 15:41:19 +0100 Subject: [PATCH 16/29] test(JAVA-6244): implement QE GA substring (un-skip unified test, prose GA/preview split) - Un-skip the QE-Text-substring GA unified spec test (now supported via libmongocrypt 1.20.0). - Prose test 27: split substring cases 5/6 into GA (substring, server 9.0+/libmongocrypt 1.20+) and preview (substringPreview, server pre-9.0) paths using the new substring-preview collection; update seed strMaxQueryLength to 6; gate cases 10/11 to pre-9.0 per the updated spec. --- ...tEncryptionTextExplicitEncryptionTest.java | 38 ++++++++++++------- .../unified/UnifiedTestModifications.java | 3 -- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java index fa7497606de..e8a5beae441 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java @@ -91,11 +91,12 @@ public void setUp() { if (gaSupported) { createEncryptedCollection("prefix-suffix", "encryptedFields-prefix-suffix.json"); createEncryptedCollection("prefix-suffix-ci-di", "encryptedFields-prefix-suffix-ci-di.json"); + createEncryptedCollection("substring", "encryptedFields-substring.json"); + createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); } else { createEncryptedCollection("prefix-suffix-preview", "encryptedFields-prefix-suffix-preview.json"); + createEncryptedCollection("substring-preview", "encryptedFields-substring-preview.json"); } - createEncryptedCollection("substring", "encryptedFields-substring.json"); - createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); MongoCollection dataKeysCollection = getMongoClient() .getDatabase(dataKeysNamespace.getDatabaseName()) @@ -148,7 +149,7 @@ public void setUp() { .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); } - // Seed the substring collection with an encrypted "foobarbaz" document. + // Seed the substring collection (GA on 9.0+, preview on pre-9.0) with an encrypted "foobarbaz" document. BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), new EncryptOptions("String") .keyId(key1Id) @@ -157,8 +158,8 @@ public void setUp() { .caseSensitive(true) .diacriticSensitive(true) .substringOptions(BsonDocument.parse( - "{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")))); - explicitEncryptedDatabase.getCollection("substring") + "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}")))); + explicitEncryptedDatabase.getCollection(gaSupported ? "substring" : "substring-preview") .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); } @@ -209,8 +210,10 @@ public void test4AssertNoDocumentFoundBySuffix() { @Test @DisplayName("Case 5: can find a document by substring") public void test5CanFindADocumentBySubstring() { - BsonBinary encrypted = encryptForSubstring("bar", true, true); - Document result = explicitEncryptedDatabase.getCollection("substring") + String queryType = gaSupported ? "substring" : "substringPreview"; + String collection = gaSupported ? "substring" : "substring-preview"; + BsonBinary encrypted = encryptForSubstring("bar", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) .find(encStrContains(encrypted)).first(); assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); } @@ -218,8 +221,10 @@ public void test5CanFindADocumentBySubstring() { @Test @DisplayName("Case 6: assert no document found by substring") public void test6AssertNoDocumentFoundBySubstring() { - BsonBinary encrypted = encryptForSubstring("qux", true, true); - Document result = explicitEncryptedDatabase.getCollection("substring") + String queryType = gaSupported ? "substring" : "substringPreview"; + String collection = gaSupported ? "substring" : "substring-preview"; + BsonBinary encrypted = encryptForSubstring("qux", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) .find(encStrContains(encrypted)).first(); assertNull(result); } @@ -279,10 +284,13 @@ public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { @Test @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring") public void test10AutoEncryptedCaseInsensitiveSubstring() { + // Spec: skip on server 9.0.0+. The substring-ci-di collection is only created on 9.0+ (GA "substring" + // encryptedFields), so these regression cases effectively exercise the pre-9.0 substring behaviour only. + assumeFalse(gaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") .insertOne(new Document("encryptedText", "FooBarBaz")); - BsonBinary substring = encryptForSubstring("bar", false, false); + BsonBinary substring = encryptForSubstring("bar", "substring", false, false); Document result = explicitEncryptedDatabase.getCollection("substring-ci-di") .find(encStrContains(substring)).first(); assertEncryptedTextEquals("FooBarBaz", result); @@ -291,10 +299,12 @@ public void test10AutoEncryptedCaseInsensitiveSubstring() { @Test @DisplayName("Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring") public void test11AutoEncryptedDiacriticInsensitiveSubstring() { + // Spec: skip on server 9.0.0+ (see Case 10). + assumeFalse(gaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") .insertOne(new Document("encryptedText", "foocafébaz")); - BsonBinary substring = encryptForSubstring("cafe", false, false); + BsonBinary substring = encryptForSubstring("cafe", "substring", false, false); Document result = explicitEncryptedDatabase.getCollection("substring-ci-di") .find(encStrContains(substring)).first(); assertEncryptedTextEquals("foocafébaz", result); @@ -343,17 +353,17 @@ private BsonBinary encryptForSuffix(final String value, final String queryType, .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); } - private BsonBinary encryptForSubstring(final String value, + private BsonBinary encryptForSubstring(final String value, final String queryType, final boolean caseSensitive, final boolean diacriticSensitive) { return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") .keyId(key1Id) .contentionFactor(0L) - .queryType("substringPreview") + .queryType(queryType) .stringOptions(new StringOptions() .caseSensitive(caseSensitive) .diacriticSensitive(diacriticSensitive) .substringOptions(BsonDocument.parse( - "{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")))); + "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}")))); } private static Document encStrStartsWith(final BsonBinary encrypted) { diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java index 7b432acd050..15f62c58ee1 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java @@ -63,9 +63,6 @@ public static void applyCustomizations(final TestDef def) { // Client side encryption (QE) def.skipJira("https://jira.mongodb.org/browse/JAVA-5675 Support QE with Client.bulkWrite") .file("client-side-encryption/tests/unified", "client bulkWrite with queryable encryption"); - def.skipJira("https://jira.mongodb.org/browse/JAVA-6244 QE GA \"substring\" query type is not yet " - + "implemented (DRIVERS-3540)") - .file("client-side-encryption/tests/unified", "QE-Text-substring"); // client-side-operation-timeout (CSOT) // The expected change stream timeout-refresh behaviour is unspecified on server 9.0+ (DRIVERS-3006), so the From 6b10fe924d28a7356be1dc5090233608e61c153d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 17:26:11 +0100 Subject: [PATCH 17/29] chore: stop tracking internal design/plan docs (kept locally, gitignored) --- .gitignore | 1 + ...06-29-qe-prefix-suffix-ga-string-rename.md | 1212 ----------------- ...e-prefix-suffix-ga-string-rename-design.md | 161 --- 3 files changed, 1 insertion(+), 1373 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-29-qe-prefix-suffix-ga-string-rename.md delete mode 100644 docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md diff --git a/.gitignore b/.gitignore index 02e9bdf0e21..5581f51dc17 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ driver-benchmarks/.factorypath # bin build directories **/bin +docs/superpowers/ diff --git a/docs/superpowers/plans/2026-06-29-qe-prefix-suffix-ga-string-rename.md b/docs/superpowers/plans/2026-06-29-qe-prefix-suffix-ga-string-rename.md deleted file mode 100644 index 202335411da..00000000000 --- a/docs/superpowers/plans/2026-06-29-qe-prefix-suffix-ga-string-rename.md +++ /dev/null @@ -1,1212 +0,0 @@ -# QE Prefix/Suffix GA + rename Text→String — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Promote QE prefix/suffix text queries to GA and rename the public explicit-encryption API from "Text" to "String" (with deprecated aliases), riding on the libmongocrypt 1.19.1 bump. - -**Architecture:** Add a new public `StringOptions` class mirroring `TextOptions`; deprecate `TextOptions`. On `EncryptOptions`, add `stringOptions(...)`/`getStringOptions()` and deprecate `textOptions(...)`/`getTextOptions()`, keeping two independent backing fields. `EncryptOptionsHelper` resolves `stringOptions` first, falling back to `textOptions` (mirrors Node's `stringOptions ?? textOptions`). The JNA layer is unchanged. The QE text explicit-encryption prose test is rewritten to spec test "27. String Explicit Encryption". - -**Tech Stack:** Java 8 (driver-core, driver-sync), JNA (mongodb-crypt), JUnit 5, libmongocrypt 1.19.1. - -## Global Constraints - -- Java 8 language level — no `var`, records, text blocks, switch expressions, `Stream.toList()`, etc. -- Copyright header `Copyright 2008-present MongoDB, Inc.` on every new file. -- No `System.out`/`System.err`/`printStackTrace` — use SLF4J if logging is needed. -- Nullability annotations: `com.mongodb.lang.Nullable` / `NonNull`. -- New `@Alpha(Reason.SERVER)` API for `StringOptions` (matches current `TextOptions`). -- Binary compatibility: all changes additive or deprecated-retained — **no breaking change, no major bump**. -- libmongocrypt floor for the rewritten prose test: **1.19.1** (already on the branch). -- Valid pairings (verified): the `"String"` algorithm is required for query types `prefix`, `suffix`, `prefixPreview`, `suffixPreview`, `substringPreview`. The `"TextPreview"` algorithm is rejected by libmongocrypt 1.19.x. -- Do not modify the spec submodule (`testing/resources/specifications/`) — already bumped. -- Run `./gradlew spotlessApply` before committing; do not reformat code outside your change. - ---- - -### Task 1: Add `StringOptions`, deprecate `TextOptions` - -**Files:** -- Create: `driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java` -- Modify: `driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java` -- Test: `driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java` - -**Interfaces:** -- Produces: `public class StringOptions` with fluent setters `caseSensitive(boolean)`, `diacriticSensitive(boolean)`, `prefixOptions(@Nullable BsonDocument)`, `suffixOptions(@Nullable BsonDocument)`, `substringOptions(@Nullable BsonDocument)` (each returns `StringOptions`); getters `boolean getCaseSensitive()`, `boolean getDiacriticSensitive()`, `@Nullable BsonDocument getPrefixOptions()`, `getSuffixOptions()`, `getSubstringOptions()`. -- Consumes: nothing. - -- [ ] **Step 1: Write the failing test** - -Create `driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java`: - -```java -/* - * Copyright 2008-present MongoDB, Inc. - * - * 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.mongodb.client.model.vault; - -import org.bson.BsonDocument; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class StringOptionsTest { - - @Test - void shouldRoundTripAllProperties() { - BsonDocument prefix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"); - BsonDocument suffix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"); - BsonDocument substring = BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}"); - - StringOptions options = new StringOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(prefix) - .suffixOptions(suffix) - .substringOptions(substring); - - assertTrue(options.getCaseSensitive()); - assertTrue(options.getDiacriticSensitive()); - assertEquals(prefix, options.getPrefixOptions()); - assertEquals(suffix, options.getSuffixOptions()); - assertEquals(substring, options.getSubstringOptions()); - } - - @Test - void shouldDefaultOptionDocumentsToNull() { - StringOptions options = new StringOptions(); - assertNull(options.getPrefixOptions()); - assertNull(options.getSuffixOptions()); - assertNull(options.getSubstringOptions()); - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `./gradlew :driver-core:test --tests "com.mongodb.client.model.vault.StringOptionsTest"` -Expected: FAIL — compilation error, `StringOptions` does not exist. - -- [ ] **Step 3: Create `StringOptions`** - -Create `driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java` by copying the current `TextOptions` body verbatim, renaming the class to `StringOptions`, updating the class Javadoc to say "String options for a Queryable Encryption field that supports string queries" and noting `substringOptions` is experimental/preview. Keep `@Alpha(Reason.SERVER)`, `@since 5.6`, `@mongodb.server.release 8.2`. The full file: - -```java -/* - * Copyright 2008-present MongoDB, Inc. - * - * 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.mongodb.client.model.vault; - -import com.mongodb.annotations.Alpha; -import com.mongodb.annotations.Reason; -import com.mongodb.lang.Nullable; -import org.bson.BsonDocument; - -/** - * String options for a Queryable Encryption field that supports string queries (prefix, suffix, and substring). - * - *

    Note: StringOptions is in Alpha and subject to backwards breaking changes. The {@code substring} query support - * is experimental (preview) and may change in a future non-major release. - * - * @since 5.6 - * @mongodb.server.release 8.2 - * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption - */ -@Alpha(Reason.SERVER) -public class StringOptions { - private Boolean caseSensitive; - private Boolean diacriticSensitive; - @Nullable - private BsonDocument prefixOptions; - @Nullable - private BsonDocument suffixOptions; - @Nullable - private BsonDocument substringOptions; - - /** - * Construct a new instance - */ - public StringOptions() { - } - - /** - * @return true if string indexes for this field are case sensitive. - */ - public boolean getCaseSensitive() { - return caseSensitive; - } - - /** - * Set case sensitivity - * - * @param caseSensitive true if string indexes are case sensitive - * @return this - */ - public StringOptions caseSensitive(final boolean caseSensitive) { - this.caseSensitive = caseSensitive; - return this; - } - - /** - * @return true if string indexes are diacritic sensitive - */ - public boolean getDiacriticSensitive() { - return diacriticSensitive; - } - - /** - * Set diacritic sensitivity - * - * @param diacriticSensitive true if string indexes are diacritic sensitive - * @return this - */ - public StringOptions diacriticSensitive(final boolean diacriticSensitive) { - this.diacriticSensitive = diacriticSensitive; - return this; - } - - /** - * Set the prefix options. - * - *

    Expected to be a {@link BsonDocument} in the format of:

    - * - *
    -     * {@code
    -     *   {
    -     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    -     *    strMinQueryLength: BsonInt32,
    -     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    -     *    strMaxQueryLength: BsonInt32
    -     *   }
    -     * }
    -     * 
    - * - * @param prefixOptions the prefix options or null - * @return this - */ - public StringOptions prefixOptions(@Nullable final BsonDocument prefixOptions) { - this.prefixOptions = prefixOptions; - return this; - } - - /** - * @see #prefixOptions(BsonDocument) - * @return the prefix options document or null - */ - @Nullable - public BsonDocument getPrefixOptions() { - return prefixOptions; - } - - /** - * Set the suffix options. - * - *

    Expected to be a {@link BsonDocument} in the format of:

    - * - *
    -     * {@code
    -     *   {
    -     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    -     *    strMinQueryLength: BsonInt32,
    -     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    -     *    strMaxQueryLength: BsonInt32
    -     *   }
    -     * }
    -     * 
    - * - * @param suffixOptions the suffix options or null - * @return this - */ - public StringOptions suffixOptions(@Nullable final BsonDocument suffixOptions) { - this.suffixOptions = suffixOptions; - return this; - } - - /** - * @see #suffixOptions(BsonDocument) - * @return the suffix options document or null - */ - @Nullable - public BsonDocument getSuffixOptions() { - return suffixOptions; - } - - /** - * Set the substring options. - * - *

    Note: substring string queries are experimental (preview) and may change in a future non-major release.

    - * - *

    Expected to be a {@link BsonDocument} in the format of:

    - * - *
    -     * {@code
    -     *   {
    -     *    // strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
    -     *    strMaxLength: BsonInt32,
    -     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    -     *    strMinQueryLength: BsonInt32,
    -     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    -     *    strMaxQueryLength: BsonInt32
    -     *   }
    -     * }
    -     * 
    - * - * @param substringOptions the substring options or null - * @return this - */ - public StringOptions substringOptions(@Nullable final BsonDocument substringOptions) { - this.substringOptions = substringOptions; - return this; - } - - /** - * @see #substringOptions(BsonDocument) - * @return the substring options document or null - */ - @Nullable - public BsonDocument getSubstringOptions() { - return substringOptions; - } -} -``` - -- [ ] **Step 4: Deprecate `TextOptions`** - -In `driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java`, leave the body unchanged but add a deprecation. Update the class Javadoc and annotation block (lines 24–34). Replace: - -```java -/** - * Text options for a Queryable Encryption field that supports text queries. - * - *

    Note: TextOptions is in Alpha and subject to backwards breaking changes. - * - * @since 5.6 - * @mongodb.server.release 8.2 - * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption - */ -@Alpha(Reason.SERVER) -public class TextOptions { -``` - -with: - -```java -/** - * Text options for a Queryable Encryption field that supports text queries. - * - *

    Note: TextOptions is in Alpha and subject to backwards breaking changes. - * - * @since 5.6 - * @mongodb.server.release 8.2 - * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption - * @deprecated Use {@link StringOptions} instead. - */ -@Deprecated -@Alpha(Reason.SERVER) -public class TextOptions { -``` - -- [ ] **Step 5: Run tests to verify they pass** - -Run: `./gradlew :driver-core:test --tests "com.mongodb.client.model.vault.StringOptionsTest"` -Expected: PASS (2 tests). - -- [ ] **Step 6: Commit** - -```bash -./gradlew :driver-core:spotlessApply -git add driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java \ - driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java \ - driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java -git commit -m "feat(JAVA-6168): add StringOptions and deprecate TextOptions" -``` - ---- - -### Task 2: `EncryptOptions` — add `stringOptions`, deprecate `textOptions`, update docs - -**Files:** -- Modify: `driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java` -- Test: `driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java` - -**Interfaces:** -- Consumes: `StringOptions`, `TextOptions` (Task 1). -- Produces: `EncryptOptions stringOptions(@Nullable StringOptions)`, `@Nullable StringOptions getStringOptions()`; deprecated `EncryptOptions textOptions(@Nullable TextOptions)`, `@Nullable TextOptions getTextOptions()`. Two independent backing fields `stringOptions` and `textOptions`. - -- [ ] **Step 1: Write the failing test** - -Create `driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java`: - -```java -/* - * Copyright 2008-present MongoDB, Inc. - * - * 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.mongodb.client.model.vault; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; - -class EncryptOptionsTest { - - @Test - void shouldStoreStringOptions() { - StringOptions stringOptions = new StringOptions().caseSensitive(true); - EncryptOptions options = new EncryptOptions("String").stringOptions(stringOptions); - assertSame(stringOptions, options.getStringOptions()); - assertNull(options.getTextOptions()); - } - - @Test - @SuppressWarnings("deprecation") - void shouldStoreDeprecatedTextOptionsIndependently() { - TextOptions textOptions = new TextOptions().caseSensitive(true); - EncryptOptions options = new EncryptOptions("String").textOptions(textOptions); - assertSame(textOptions, options.getTextOptions()); - assertNull(options.getStringOptions()); - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `./gradlew :driver-core:test --tests "com.mongodb.client.model.vault.EncryptOptionsTest"` -Expected: FAIL — `stringOptions`/`getStringOptions` do not exist. - -- [ ] **Step 3: Add the field** - -In `EncryptOptions.java`, after line 36 (`private TextOptions textOptions;`) add: - -```java - private StringOptions stringOptions; -``` - -- [ ] **Step 4: Add `stringOptions(...)` and `getStringOptions()`** - -In `EncryptOptions.java`, immediately after the existing `getTextOptions()` method (ends at line 233, before the `toString()` at line 235), insert: - -```java - /** - * The StringOptions - * - *

    It is an error to set StringOptions when the algorithm is not "String". - * @param stringOptions the string options - * @return this - * @since 5.6 - * @mongodb.server.release 8.2 - * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption - */ - @Alpha(Reason.SERVER) - public EncryptOptions stringOptions(@Nullable final StringOptions stringOptions) { - this.stringOptions = stringOptions; - return this; - } - - /** - * Gets the StringOptions - * @see #stringOptions(StringOptions) - * @return the string options or null if not set - * @since 5.6 - * @mongodb.server.release 8.2 - * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption - */ - @Alpha(Reason.SERVER) - @Nullable - public StringOptions getStringOptions() { - return stringOptions; - } -``` - -- [ ] **Step 5: Deprecate `textOptions(...)` and `getTextOptions()`** - -In `EncryptOptions.java`, update the existing `textOptions(...)` (lines ~205–219) and `getTextOptions()` (lines ~221–233) Javadocs to add `@deprecated Use {@link #stringOptions(StringOptions)} instead.` / `@deprecated Use {@link #getStringOptions()} instead.` and annotate both methods `@Deprecated`. The method bodies are unchanged. Example for the setter: - -```java - /** - * The TextOptions - * - *

    It is an error to set TextOptions when the algorithm is not "TextPreview". - * @param textOptions the text options - * @return this - * @since 5.6 - * @mongodb.server.release 8.2 - * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption - * @deprecated Use {@link #stringOptions(StringOptions)} instead. - */ - @Deprecated - @Alpha(Reason.SERVER) - public EncryptOptions textOptions(@Nullable final TextOptions textOptions) { - this.textOptions = textOptions; - return this; - } -``` - -Apply the analogous `@Deprecated` + `@deprecated Use {@link #getStringOptions()} instead.` change to `getTextOptions()`. - -- [ ] **Step 6: Update the algorithm and queryType Javadocs** - -In `getAlgorithm()` (lines ~49–65), change the bullet `

  • TextPreview
  • ` to `
  • String
  • ` and replace the "TextPreview" preview note paragraph with: - -```java - *

    The "String" algorithm supports Queryable Encryption prefix and suffix string queries, and (in preview) - * substring queries. Use the "String" algorithm with query types "prefix"/"suffix" (server 9.0+) or the - * deprecated aliases "prefixPreview"/"suffixPreview" (server 8.2 to pre-9.0), and "substringPreview" - * (experimental).

    -``` - -In `queryType(...)` (lines ~149–162), replace the line: - -```java - *

    Currently, we support only "equality", "range", "prefixPreview", "suffixPreview" or "substringPreview" queryType.

    - *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "TextPreview".

    -``` - -with: - -```java - *

    Currently, we support only "equality", "range", "prefix", "suffix", "prefixPreview", "suffixPreview" or - * "substringPreview" queryType.

    - *

    The "prefix", "suffix", "prefixPreview", "suffixPreview" and "substringPreview" query types are only valid - * with the "String" algorithm. "prefixPreview"/"suffixPreview" are deprecated aliases supported for servers 8.2 - * to pre-9.0; use "prefix"/"suffix" on server 9.0+.

    - *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "String".

    -``` - -- [ ] **Step 7: Run tests to verify they pass** - -Run: `./gradlew :driver-core:test --tests "com.mongodb.client.model.vault.EncryptOptionsTest"` -Expected: PASS (2 tests). - -- [ ] **Step 8: Commit** - -```bash -./gradlew :driver-core:spotlessApply -git add driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java \ - driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java -git commit -m "feat(JAVA-6168): add EncryptOptions.stringOptions and deprecate textOptions" -``` - ---- - -### Task 3: `EncryptOptionsHelper` — resolve stringOptions, fall back to textOptions - -**Files:** -- Modify: `driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java` -- Test: `driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java` - -**Interfaces:** -- Consumes: `EncryptOptions.getStringOptions()`, `EncryptOptions.getTextOptions()` (Task 2); `MongoExplicitEncryptOptions.getTextOptions()` (unchanged internal type). -- Produces: unchanged public method `asMongoExplicitEncryptOptions(EncryptOptions)`. - -- [ ] **Step 1: Write the failing test** - -Create `driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java`: - -```java -/* - * Copyright 2008-present MongoDB, Inc. - * - * 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.mongodb.internal.client.vault; - -import com.mongodb.client.model.vault.EncryptOptions; -import com.mongodb.client.model.vault.StringOptions; -import com.mongodb.client.model.vault.TextOptions; -import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions; -import org.bson.BsonDocument; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -class EncryptOptionsHelperTest { - - @Test - void shouldMapStringOptionsToTextOptionsDocument() { - EncryptOptions options = new EncryptOptions("String").stringOptions(new StringOptions() - .caseSensitive(true) - .diacriticSensitive(false) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); - - MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); - - assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: false, " - + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), - result.getTextOptions()); - } - - @Test - @SuppressWarnings("deprecation") - void shouldFallBackToDeprecatedTextOptions() { - EncryptOptions options = new EncryptOptions("String").textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); - - MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); - - assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, " - + "suffix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), - result.getTextOptions()); - } - - @Test - void shouldLeaveTextOptionsNullWhenNeitherSet() { - MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions( - new EncryptOptions("Indexed")); - assertNull(result.getTextOptions()); - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `./gradlew :driver-core:test --tests "com.mongodb.internal.client.vault.EncryptOptionsHelperTest"` -Expected: FAIL — helper still reads only `getTextOptions()`, so `shouldMapStringOptionsToTextOptionsDocument` fails (text options null). - -- [ ] **Step 3: Update the helper** - -In `EncryptOptionsHelper.java`, add the import `import com.mongodb.client.model.vault.StringOptions;` (after the `RangeOptions` import). Then replace the `TextOptions` block (lines 76–97): - -```java - TextOptions textOptions = options.getTextOptions(); - if (textOptions != null) { - BsonDocument textOptionsDocument = new BsonDocument(); - textOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(textOptions.getCaseSensitive())); - textOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(textOptions.getDiacriticSensitive())); - - BsonDocument substringOptions = textOptions.getSubstringOptions(); - if (substringOptions != null) { - textOptionsDocument.put("substring", substringOptions); - } - - BsonDocument prefixOptions = textOptions.getPrefixOptions(); - if (prefixOptions != null) { - textOptionsDocument.put("prefix", prefixOptions); - } - - BsonDocument suffixOptions = textOptions.getSuffixOptions(); - if (suffixOptions != null) { - textOptionsDocument.put("suffix", suffixOptions); - } - encryptOptionsBuilder.textOptions(textOptionsDocument); - } -``` - -with (note: `getTextOptions()` is deprecated; suppress on the method or read via a local — use a `@SuppressWarnings("deprecation")` on `asMongoExplicitEncryptOptions`): - -```java - StringOptions stringOptions = resolveStringOptions(options); - if (stringOptions != null) { - BsonDocument stringOptionsDocument = new BsonDocument(); - stringOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(stringOptions.getCaseSensitive())); - stringOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(stringOptions.getDiacriticSensitive())); - - BsonDocument substringOptions = stringOptions.getSubstringOptions(); - if (substringOptions != null) { - stringOptionsDocument.put("substring", substringOptions); - } - - BsonDocument prefixOptions = stringOptions.getPrefixOptions(); - if (prefixOptions != null) { - stringOptionsDocument.put("prefix", prefixOptions); - } - - BsonDocument suffixOptions = stringOptions.getSuffixOptions(); - if (suffixOptions != null) { - stringOptionsDocument.put("suffix", suffixOptions); - } - encryptOptionsBuilder.textOptions(stringOptionsDocument); - } -``` - -Then add the import `import com.mongodb.client.model.vault.TextOptions;` is already present — keep it. Add this private helper method above the private constructor (before `private EncryptOptionsHelper()`): - -```java - @SuppressWarnings("deprecation") - @Nullable - private static StringOptions resolveStringOptions(final EncryptOptions options) { - StringOptions stringOptions = options.getStringOptions(); - if (stringOptions != null) { - return stringOptions; - } - TextOptions textOptions = options.getTextOptions(); - if (textOptions == null) { - return null; - } - return new StringOptions() - .caseSensitive(textOptions.getCaseSensitive()) - .diacriticSensitive(textOptions.getDiacriticSensitive()) - .prefixOptions(textOptions.getPrefixOptions()) - .suffixOptions(textOptions.getSuffixOptions()) - .substringOptions(textOptions.getSubstringOptions()); - } -``` - -Add the import `import com.mongodb.lang.Nullable;` if not present. - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `./gradlew :driver-core:test --tests "com.mongodb.internal.client.vault.EncryptOptionsHelperTest"` -Expected: PASS (3 tests). - -- [ ] **Step 5: Commit** - -```bash -./gradlew :driver-core:spotlessApply -git add driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java \ - driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java -git commit -m "feat(JAVA-6168): resolve stringOptions with textOptions fallback in EncryptOptionsHelper" -``` - ---- - -### Task 4: Refresh mongodb-crypt doc comments (TextPreview → String) - -**Files:** -- Modify: `mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java` - -**Interfaces:** -- Consumes/Produces: none — doc-comment-only change. No signature or behavior change. - -- [ ] **Step 1: Update doc comments** - -In `MongoExplicitEncryptOptions.java`, update three Javadoc mentions of `"TextPreview"` to reflect the GA `"String"` algorithm. Replace `

    Only applies when algorithm is "Indexed", "Range", or "TextPreview".

    ` (two occurrences, on `contentionFactor(...)` and `queryType(...)`) with `

    Only applies when algorithm is "Indexed", "Range", or "String".

    `, and replace `

    Only applies when algorithm is "TextPreview".

    ` (on `textOptions(...)`) with `

    Only applies when algorithm is "String".

    `. - -- [ ] **Step 2: Verify it compiles** - -Run: `./gradlew :mongodb-crypt:compileJava` -Expected: BUILD SUCCESSFUL. - -- [ ] **Step 3: Commit** - -```bash -./gradlew :mongodb-crypt:spotlessApply -git add mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java -git commit -m "docs(JAVA-6168): refer to String algorithm in MongoExplicitEncryptOptions" -``` - ---- - -### Task 5: Rewrite the QE string explicit-encryption prose test to GA (spec test 27) - -**Files:** -- Modify: `driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java` - -**Interfaces:** -- Consumes: `EncryptOptions` with `"String"` algorithm + `stringOptions(StringOptions)` (Tasks 1–2); test data files already present in the spec submodule (`encryptedFields-prefix-suffix.json`, `encryptedFields-prefix-suffix-ci-di.json`, `encryptedFields-prefix-suffix-preview.json`, `encryptedFields-substring.json`, `encryptedFields-substring-ci-di.json`, `keys/key1-document.json`). -- Produces: nothing (test only). The two concrete subclasses (`driver-sync/.../ClientEncryptionTextExplicitEncryptionTest.java` and `driver-reactive-streams/.../ClientEncryptionTextExplicitEncryptionTest.java`) need no change — they only implement `createMongoClient`/`createClientEncryption`. - -Implements spec "27. String Explicit Encryption": algorithm `"String"` + `stringOptions`/`StringOptions`; cases 1–4 parameterized as `(prefix, prefix-suffix)` on server 9.0+ and `(prefixPreview, prefix-suffix-preview)` on server pre-9.0; cases 7–9 require server 9.0+; cases 5,6,10 run on 8.2+. An `autoEncryptedClient` (auto-encryption, no `bypassQueryAnalysis`) is added for cases 8–10. - -- [ ] **Step 1: Run the existing test to confirm the starting state** - -Run: `./gradlew :driver-sync:test --tests "com.mongodb.client.ClientEncryptionTextExplicitEncryptionTest"` -Expected: with a configured 8.2+ server and libmongocrypt 1.19.1, the *current* (commented-out-guards) test FAILS or errors, because it passes `new EncryptOptions("TextPreview")` which libmongocrypt 1.19.1 rejects (`unsupported algorithm string "textPreview"`). This confirms the rewrite is needed. (If no server is configured, the test is skipped — note this and proceed; the rewrite is verified in Step 3.) - -- [ ] **Step 2: Replace the abstract test file** - -Overwrite `driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java` with: - -```java -/* - * Copyright 2008-present MongoDB, Inc. - * - * 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.mongodb.client; - -import com.mongodb.AutoEncryptionSettings; -import com.mongodb.ClientEncryptionSettings; -import com.mongodb.MongoClientSettings; -import com.mongodb.MongoException; -import com.mongodb.MongoNamespace; -import com.mongodb.WriteConcern; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.DropCollectionOptions; -import com.mongodb.client.model.vault.EncryptOptions; -import com.mongodb.client.model.vault.StringOptions; -import com.mongodb.client.vault.ClientEncryption; -import com.mongodb.connection.ServerVersion; -import com.mongodb.fixture.EncryptionFixture; -import org.bson.BsonBinary; -import org.bson.BsonDocument; -import org.bson.BsonString; -import org.bson.Document; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static com.mongodb.ClusterFixture.getDefaultDatabaseName; -import static com.mongodb.ClusterFixture.getMongoCryptVersion; -import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled; -import static com.mongodb.ClusterFixture.isStandalone; -import static com.mongodb.ClusterFixture.serverVersionAtLeast; -import static com.mongodb.client.Fixture.getDefaultDatabase; -import static com.mongodb.client.Fixture.getMongoClient; -import static com.mongodb.client.Fixture.getMongoClientSettings; -import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; -import static com.mongodb.fixture.EncryptionFixture.getKmsProviders; -import static java.util.Arrays.asList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static util.JsonPoweredTestHelper.getTestDocument; - -public abstract class AbstractClientEncryptionTextExplicitEncryptionTest { - - private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); - private boolean gaSupported; - private MongoClient explicitEncryptedClient; - private MongoClient autoEncryptedClient; - private MongoDatabase explicitEncryptedDatabase; - private MongoDatabase autoEncryptedDatabase; - private ClientEncryption clientEncryption; - private BsonBinary key1Id; - - protected abstract MongoClient createMongoClient(MongoClientSettings settings); - protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings); - - @BeforeEach - public void setUp() { - assumeTrue("String explicit encryption tests disabled", hasEncryptionTestsEnabled()); - assumeTrue("Requires newer MongoCrypt version", getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0); - assumeTrue(serverVersionAtLeast(8, 2)); - assumeFalse(isStandalone()); - - gaSupported = serverVersionAtLeast(9, 0); - - MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys"); - BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json"); - - Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL); - - if (gaSupported) { - createEncryptedCollection("prefix-suffix", "encryptedFields-prefix-suffix.json"); - createEncryptedCollection("prefix-suffix-ci-di", "encryptedFields-prefix-suffix-ci-di.json"); - } else { - createEncryptedCollection("prefix-suffix-preview", "encryptedFields-prefix-suffix-preview.json"); - } - createEncryptedCollection("substring", "encryptedFields-substring.json"); - createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); - - MongoCollection dataKeysCollection = getMongoClient() - .getDatabase(dataKeysNamespace.getDatabaseName()) - .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class) - .withWriteConcern(WriteConcern.MAJORITY); - dataKeysCollection.drop(); - dataKeysCollection.insertOne(key1Document); - key1Id = key1Document.getBinary("_id"); - - clientEncryption = createClientEncryption(ClientEncryptionSettings.builder() - .keyVaultMongoClientSettings(getMongoClientSettings()) - .keyVaultNamespace(dataKeysNamespace.getFullName()) - .kmsProviders(kmsProviders) - .build()); - - explicitEncryptedClient = createMongoClient(getMongoClientSettingsBuilder() - .autoEncryptionSettings(AutoEncryptionSettings.builder() - .keyVaultNamespace(dataKeysNamespace.getFullName()) - .kmsProviders(kmsProviders) - .bypassQueryAnalysis(true) - .build()) - .build()); - explicitEncryptedDatabase = explicitEncryptedClient.getDatabase(getDefaultDatabaseName()) - .withWriteConcern(WriteConcern.MAJORITY); - - autoEncryptedClient = createMongoClient(getMongoClientSettingsBuilder() - .autoEncryptionSettings(AutoEncryptionSettings.builder() - .keyVaultNamespace(dataKeysNamespace.getFullName()) - .kmsProviders(kmsProviders) - .build()) - .build()); - autoEncryptedDatabase = autoEncryptedClient.getDatabase(getDefaultDatabaseName()) - .withWriteConcern(WriteConcern.MAJORITY); - - // Seed the prefix-suffix collection(s) with an encrypted "foobarbaz" document. - BsonBinary prefixSuffixSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), - new EncryptOptions("String") - .keyId(key1Id) - .contentionFactor(0L) - .stringOptions(new StringOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); - if (gaSupported) { - explicitEncryptedDatabase.getCollection("prefix-suffix") - .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); - } else { - explicitEncryptedDatabase.getCollection("prefix-suffix-preview") - .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); - } - - // Seed the substring collection with an encrypted "foobarbaz" document. - BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), - new EncryptOptions("String") - .keyId(key1Id) - .contentionFactor(0L) - .stringOptions(new StringOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse( - "{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")))); - explicitEncryptedDatabase.getCollection("substring") - .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); - } - - @Test - @DisplayName("Case 1: can find a document by prefix") - public void test1CanFindADocumentByPrefix() { - String queryType = gaSupported ? "prefix" : "prefixPreview"; - String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; - BsonBinary encrypted = encryptForPrefix("foo", queryType, true, true); - Document result = explicitEncryptedDatabase.getCollection(collection) - .find(encStrStartsWith(encrypted)).first(); - assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); - } - - @Test - @DisplayName("Case 2: can find a document by suffix") - public void test2CanFindADocumentBySuffix() { - String queryType = gaSupported ? "suffix" : "suffixPreview"; - String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; - BsonBinary encrypted = encryptForSuffix("baz", queryType, true, true); - Document result = explicitEncryptedDatabase.getCollection(collection) - .find(encStrEndsWith(encrypted)).first(); - assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); - } - - @Test - @DisplayName("Case 3: assert no document found by prefix") - public void test3AssertNoDocumentFoundByPrefix() { - String queryType = gaSupported ? "prefix" : "prefixPreview"; - String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; - BsonBinary encrypted = encryptForPrefix("baz", queryType, true, true); - Document result = explicitEncryptedDatabase.getCollection(collection) - .find(encStrStartsWith(encrypted)).first(); - assertNull(result); - } - - @Test - @DisplayName("Case 4: assert no document found by suffix") - public void test4AssertNoDocumentFoundBySuffix() { - String queryType = gaSupported ? "suffix" : "suffixPreview"; - String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; - BsonBinary encrypted = encryptForSuffix("foo", queryType, true, true); - Document result = explicitEncryptedDatabase.getCollection(collection) - .find(encStrEndsWith(encrypted)).first(); - assertNull(result); - } - - @Test - @DisplayName("Case 5: can find a document by substring") - public void test5CanFindADocumentBySubstring() { - BsonBinary encrypted = encryptForSubstring("bar", true, true); - Document result = explicitEncryptedDatabase.getCollection("substring") - .find(encStrContains(encrypted)).first(); - assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); - } - - @Test - @DisplayName("Case 6: assert no document found by substring") - public void test6AssertNoDocumentFoundBySubstring() { - BsonBinary encrypted = encryptForSubstring("qux", true, true); - Document result = explicitEncryptedDatabase.getCollection("substring") - .find(encStrContains(encrypted)).first(); - assertNull(result); - } - - @Test - @DisplayName("Case 7: assert `contentionFactor` is required") - public void test7AssertContentionFactorIsRequired() { - assumeTrue(gaSupported); - EncryptOptions encryptOptions = new EncryptOptions("String") - .keyId(key1Id) - .queryType("prefix") - .stringOptions(new StringOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); - MongoException exception = assertThrows(MongoException.class, - () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions)); - assertTrue(exception.getMessage().contains("contention factor is required for string algorithm")); - } - - @Test - @DisplayName("Case 8: can find an auto-encrypted case-insensitively indexed document by prefix and suffix") - public void test8AutoEncryptedCaseInsensitivePrefixAndSuffix() { - assumeTrue(gaSupported); - autoEncryptedDatabase.getCollection("prefix-suffix-ci-di") - .insertOne(new Document("encryptedText", "BingQiLin")); - - BsonBinary prefix = encryptForPrefix("bing", "prefix", false, false); - Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") - .find(encStrStartsWith(prefix)).first(); - assertEncryptedTextEquals("BingQiLin", byPrefix); - - BsonBinary suffix = encryptForSuffix("lin", "suffix", false, false); - Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") - .find(encStrEndsWith(suffix)).first(); - assertEncryptedTextEquals("BingQiLin", bySuffix); - } - - @Test - @DisplayName("Case 9: can find an auto-encrypted diacritic-insensitively indexed document by prefix and suffix") - public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { - assumeTrue(gaSupported); - autoEncryptedDatabase.getCollection("prefix-suffix-ci-di") - .insertOne(new Document("encryptedText", "cafébarbäz")); - - BsonBinary prefix = encryptForPrefix("cafe", "prefix", false, false); - Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") - .find(encStrStartsWith(prefix)).first(); - assertEncryptedTextEquals("cafébarbäz", byPrefix); - - BsonBinary suffix = encryptForSuffix("baz", "suffix", false, false); - Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") - .find(encStrEndsWith(suffix)).first(); - assertEncryptedTextEquals("cafébarbäz", bySuffix); - } - - @Test - @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring") - public void test10AutoEncryptedCaseInsensitiveSubstring() { - autoEncryptedDatabase.getCollection("substring-ci-di") - .insertOne(new Document("encryptedText", "FooBarBaz")); - - BsonBinary substring = encryptForSubstring("bar", false, false); - Document result = explicitEncryptedDatabase.getCollection("substring-ci-di") - .find(encStrContains(substring)).first(); - assertEncryptedTextEquals("FooBarBaz", result); - } - - @AfterEach - @SuppressWarnings("try") - public void cleanUp() { - getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY).drop(); - try (ClientEncryption ignored = this.clientEncryption; - MongoClient ignored1 = this.explicitEncryptedClient; - MongoClient ignored2 = this.autoEncryptedClient - ) { - // just using try-with-resources to ensure they all get closed, even in the case of exceptions - } - } - - private void createEncryptedCollection(final String name, final String encryptedFieldsFile) { - BsonDocument encryptedFields = bsonDocumentFromPath(encryptedFieldsFile); - MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY); - database.getCollection(name).drop(new DropCollectionOptions().encryptedFields(encryptedFields)); - database.createCollection(name, new CreateCollectionOptions().encryptedFields(encryptedFields)); - } - - private BsonBinary encryptForPrefix(final String value, final String queryType, - final boolean caseSensitive, final boolean diacriticSensitive) { - return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") - .keyId(key1Id) - .contentionFactor(0L) - .queryType(queryType) - .stringOptions(new StringOptions() - .caseSensitive(caseSensitive) - .diacriticSensitive(diacriticSensitive) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); - } - - private BsonBinary encryptForSuffix(final String value, final String queryType, - final boolean caseSensitive, final boolean diacriticSensitive) { - return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") - .keyId(key1Id) - .contentionFactor(0L) - .queryType(queryType) - .stringOptions(new StringOptions() - .caseSensitive(caseSensitive) - .diacriticSensitive(diacriticSensitive) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); - } - - private BsonBinary encryptForSubstring(final String value, - final boolean caseSensitive, final boolean diacriticSensitive) { - return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("substringPreview") - .stringOptions(new StringOptions() - .caseSensitive(caseSensitive) - .diacriticSensitive(diacriticSensitive) - .substringOptions(BsonDocument.parse( - "{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")))); - } - - private static Document encStrStartsWith(final BsonBinary encrypted) { - return new Document("$expr", new Document("$encStrStartsWith", - new Document("input", "$encryptedText").append("prefix", encrypted))); - } - - private static Document encStrEndsWith(final BsonBinary encrypted) { - return new Document("$expr", new Document("$encStrEndsWith", - new Document("input", "$encryptedText").append("suffix", encrypted))); - } - - private static Document encStrContains(final BsonBinary encrypted) { - return new Document("$expr", new Document("$encStrContains", - new Document("input", "$encryptedText").append("substring", encrypted))); - } - - private static void assertDocumentEquals(final Document expectedDocument, final Document actualDocument) { - actualDocument.remove("__safeContent__"); - assertEquals(expectedDocument, actualDocument); - } - - private static void assertEncryptedTextEquals(final String expectedText, final Document actualDocument) { - assertEquals(expectedText, actualDocument.getString("encryptedText")); - } - - private static BsonDocument bsonDocumentFromPath(final String path) { - return getTestDocument("client-side-encryption/etc/data/" + path); - } -} -``` - -- [ ] **Step 3: Run the prose test against a server** - -Run (requires a configured non-standalone server 8.2+; set the URI): -`./gradlew :driver-sync:test --tests "com.mongodb.client.ClientEncryptionTextExplicitEncryptionTest" -Dorg.mongodb.test.uri="mongodb://localhost:27017"` -Expected: PASS. On a 9.0+ server, cases 1–10 run (GA query types). On an 8.2/8.3 server, cases 1–6 and 10 run with the preview path and cases 7–9 are skipped (`assumeTrue(gaSupported)`). - -- [ ] **Step 4: Run the reactive variant** - -Run: `./gradlew :driver-reactive-streams:test --tests "com.mongodb.reactivestreams.client.ClientEncryptionTextExplicitEncryptionTest" -Dorg.mongodb.test.uri="mongodb://localhost:27017"` -Expected: PASS (same gating). - -- [ ] **Step 5: Commit** - -```bash -./gradlew :driver-sync:spotlessApply -git add driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java -git commit -m "test(JAVA-6168): rewrite QE string explicit-encryption prose test to GA" -``` - ---- - -### Task 6: Verify unified QE-Text spec tests run (not skipped) + flag CI matrix - -**Files:** -- Inspect: `driver-sync/src/test/functional/com/mongodb/client/unified/ClientSideEncryptionTest.java` -- Inspect: `driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java` - -**Interfaces:** -- Consumes/Produces: none — verification + optional skip-list edit only. - -- [ ] **Step 1: Confirm the QE-Text unified tests are discovered** - -Run: `./gradlew :driver-sync:test --tests "com.mongodb.client.unified.ClientSideEncryptionTest" -Dorg.mongodb.test.uri="mongodb://localhost:27017" --info` and confirm test names containing `QE-Text-prefix`, `QE-Text-suffix`, `QE-Text-prefixPreview`, `QE-Text-suffixPreview`, `QE-Text-substringPreview` appear (executed or skipped by `runOnRequirements`, not absent). -Expected: those display names are present in the run. - -- [ ] **Step 2: Check for unintended skips** - -Search the modification/skip registry for any entry that would disable these: - -Run: `grep -ni "QE-Text\|text" driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java` -Expected: no skip that excludes the `QE-Text-*` files on supported server/libmongocrypt versions. If a stale skip exists (e.g. one added while the feature was preview/disabled), remove it and re-run Step 1. - -- [ ] **Step 3: Record the CI matrix requirement** - -The `QE-Text-prefixPreview`/`QE-Text-suffixPreview` unified tests declare `minServerVersion 8.2.0` / `maxServerVersion 8.99.99`. They only execute the auto-encryption preview path on a **pre-9.0** server. Confirm the encryption test matrix in `.evergreen/` runs against at least one 8.2/8.3 server in addition to 9.0+. **Do not modify `.evergreen/` config without human approval** — if the matrix lacks a pre-9.0 server, note it in the PR description and flag it to a human reviewer rather than editing CI config. - -- [ ] **Step 4: Commit (only if a stale skip was removed in Step 2)** - -```bash -./gradlew :driver-sync:spotlessApply -git add driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java -git commit -m "test(JAVA-6168): run QE-Text unified spec tests" -``` - -If no change was needed, skip the commit. - ---- - -## Self-Review - -**Spec coverage** (against `docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md`): -- §4 new `StringOptions` + deprecated `TextOptions` → Task 1. ✅ -- §4 `EncryptOptions.stringOptions`/deprecated `textOptions` + algorithm/queryType doc updates → Task 2. ✅ -- §5 `EncryptOptionsHelper` resolution (stringOptions → textOptions fallback) → Task 3. ✅ -- §5 mongodb-crypt doc-only refresh; no JNA change → Task 4. ✅ -- §6 prose test 27 rewrite (cases 1–10, GA/preview gating, autoEncryptedClient) → Task 5. ✅ -- §6 unified-test execution notes + CI pre-9.0 matrix → Task 6. ✅ -- §7 language wrappers reuse driver-core classes (no wrapper change) → covered by Task 5 "Produces" note (concrete subclasses unchanged); Kotlin/Scala inherit automatically. -- Out-of-scope items (substring GA / preview removal / lookup prose-25) correctly excluded. - -**Placeholder scan:** No TBD/TODO; all steps contain full code or exact commands. - -**Type consistency:** `StringOptions` fluent setters/getters used identically in Tasks 1–3 and Task 5; `EncryptOptions.stringOptions(StringOptions)`/`getStringOptions()` and deprecated `textOptions(TextOptions)`/`getTextOptions()` consistent across Tasks 2, 3, 5; `MongoExplicitEncryptOptions.getTextOptions()` (internal type, unchanged) used in Task 3 test. diff --git a/docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md b/docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md deleted file mode 100644 index eaa96dc709b..00000000000 --- a/docs/superpowers/specs/2026-06-29-qe-prefix-suffix-ga-string-rename-design.md +++ /dev/null @@ -1,161 +0,0 @@ -# QE Prefix/Suffix GA + rename Text→String API - -**Ticket:** JAVA-6168 (Java split of DRIVERS-3321, under epic DRIVERS-3408) -**Date:** 2026-06-29 -**Status:** Approved design - -## 1. Summary - -Promote Queryable Encryption (QE) prefix/suffix text queries from preview to GA in the -Java driver, and rename the public explicit-encryption API from "Text" to "String", matching -the spec changes in DRIVERS-3321 and the Node analog PR -[#4972](https://github.com/mongodb/node-mongodb-native/pull/4972). Substring stays as an -experimental preview carry-over. - -This rides on the libmongocrypt bump from 1.18.1 → **1.19.1** (already applied on the branch). - -## 2. Ticket hierarchy and scope - -- **DRIVERS-3408** is the epic ("QE Prefix/Suffix/Substring GA"). -- **DRIVERS-3321** ("Add QE Prefix+Suffix GA and rename API to string") is the implementation - child. Its Java split is **JAVA-6168** (this work). DRIVERS-3321 is **not** a parallel track — - it *is* the work for the epic in Java. Node PR #4972 (NODE-7537) is the exact analog. - -### In scope -- libmongocrypt 1.19.1 bump (done). -- Public API rename Text→String in `driver-core`, with `@Deprecated` aliases for the old names. -- Substring carried over as experimental preview (matches Node). -- Rewrite the QE text explicit-encryption prose test to the GA spec ("27. String Explicit - Encryption"); rely on the auto-discovered `QE-Text-*` unified spec tests. - -### Out of scope (separate, blocked, or future — not doable on 1.19.1) -- **DRIVERS-3540** — substring *GA* rename (`substringPreview` → `substring`). Blocked on a - server/libmongocrypt rename; libmongocrypt 1.19.1 only ships `substringPreview`. -- **DRIVERS-3548** — drop the preview query types. Scheduled near server 9.0, future. -- **DRIVERS-3294** — update the `$lookup` prose test 25 for MONGOCRYPT-793 (Node split NODE-7221). - Unrelated to text/string queries (it tests encrypted-collection joins). Its own JAVA ticket/PR. - -## 3. libmongocrypt 1.19.1 behavior (source-verified) - -Verified against `mongocrypt-ctx.c` at tag `1.19.1`. - -| Identifier | Kind | 1.19.0 | 1.19.1 | Notes | -|---|---|---|---|---| -| `prefixPreview` / `suffixPreview` | query type | removed | **restored** | Work on pre-9.0 servers; persisted in `encryptedFields` | -| `prefix` / `suffix` | query type | added (GA) | GA | Server 9.0.0+ | -| `substringPreview` | query type | preview | preview | Still experimental; no GA yet | -| `textPreview` | algorithm | renamed → `string`, **not** restored | still gone | Client-only selector | -| `string` | algorithm | added (GA) | GA | `MONGOCRYPT_ALGORITHM_STRING_STR` | - -`mongocrypt_ctx_setopt_algorithm()` matches (case-insensitive): Deterministic, Random, Indexed, -Unindexed, Range, RangePreview (→ explicit error), String. **There is no `textPreview` branch** — -passing it fails with `unsupported algorithm string "textPreview"`. - -### Valid algorithm / queryType pairings (verified) -The init validation in `mongocrypt-ctx-encrypt.c` requires the `string` index type for all the -text query types (`prefix query type requires string index type`, etc.), and treats -`prefixPreview`/`suffixPreview` as deprecated aliases of `prefix`/`suffix`: - -| algorithm | queryType | result | -|---|---|---| -| `"String"` | `"prefix"` (GA) | valid | -| `"String"` | `"prefixPreview"` (deprecated alias) | valid | -| `"String"` | `"substringPreview"` (experimental) | valid | -| `"TextPreview"` | *anything* | rejected — `unsupported algorithm string "textPreview"` | - -So the only way to use `prefixPreview`/`suffixPreview` after this work is with the `"String"` -algorithm — the old `"TextPreview"` + `"prefixPreview"` combination no longer exists. For -**explicit** encryption, `string`+`prefix` and `string`+`prefixPreview` produce identical payloads; -the distinction matters only for **auto-encryption**, where the query-type name persisted in -`encryptedFields` must match the target server (pre-9.0 → `prefixPreview`, 9.0+ → `prefix`). The -`EncryptOptions.queryType` javadoc should state this pairing constraint. - -### Why the asymmetry is intentional (not an oversight) -- **Query types** are persisted in a collection's `encryptedFields` schema and are - server-version-dependent. Pre-9.0 servers (8.2/8.3) *only* support the preview names; 9.0 - rejects them. For **auto-encryption** against a pre-9.0 server, libmongocrypt must tolerate - `prefixPreview`/`suffixPreview` in `encryptedFields` — hence the restore (MONGOCRYPT-937 #1191). -- **The algorithm name** is a client-only selector that picks the internal index type - (`MONGOCRYPT_INDEX_TYPE_STRING`) for explicit encryption. It is never written to - `encryptedFields` or sent to the server, so there is nothing deployed that references - `textPreview`. A one-time rename costs nothing and needs no restore (MONGOCRYPT-870 #1184). The - target `string` (over `text`) was chosen per DRIVERS-3531 to avoid confusion with MongoDB text - search. - -## 4. Public API changes — `driver-core` (`com.mongodb.client.model.vault`) - -- **New `StringOptions` class** (`@Alpha(Reason.SERVER)`): same shape as today's `TextOptions` - (`caseSensitive`, `diacriticSensitive`, `prefixOptions`, `suffixOptions`, `substringOptions`). - `substringOptions` documented as experimental/preview. -- **`TextOptions` retained as `@Deprecated`**, as a deprecated alias of `StringOptions`. Preferred - shape: `TextOptions extends StringOptions` with no new members, so existing code compiles and any - `TextOptions` instance is usable where `StringOptions` is expected. The exact binary-safe form - (subclass vs. standalone delegating class) is confirmed during planning. -- **`EncryptOptions`**: add `stringOptions(StringOptions)` + `getStringOptions()`; retain - `textOptions(TextOptions)` + `getTextOptions()` as `@Deprecated`, backed by the same field. - Resolution mirrors Node (`stringOptions` wins, falling back to `textOptions`). -- **Doc-only updates** to `EncryptOptions`: algorithm list `"TextPreview"` → `"String"`; queryType - docs `prefixPreview`/`suffixPreview` → `prefix`/`suffix` (note the preview names remain supported - for pre-9.0 servers; `substringPreview` experimental). - -### Algorithm caveat -The `algorithm` value is a free-form `String` supplied by the user and passed straight to -libmongocrypt, which removed `"textPreview"` (only `"string"` works). We therefore **cannot** -provide a driver-side code alias for the algorithm string — we document `"String"` as the GA name. -This is why the existing prose test (`new EncryptOptions("TextPreview")`) was disabled and must be -rewritten to `"String"`. - -### Binary compatibility -All changes are additive (new class, new methods) or retain deprecated members. **No breaking -change; no major-version bump required.** - -## 5. Internal mapping & JNA — `mongodb-crypt` (minimal) - -- `EncryptOptionsHelper`: read from `getStringOptions()` (falling back to deprecated - `getTextOptions()`); BSON keys (`prefix`/`suffix`/`substring`, `caseSensitive`, - `diacriticSensitive`) are **unchanged**. -- **No JNA binding change required.** The C function - `mongocrypt_ctx_setopt_algorithm_text(ctx, opts)` and its BSON option schema are identical in - 1.19.1. Optional: refresh doc-comment wording ("TextPreview" → "String"). `CAPI.java`, - `MongoCryptImpl`, `MongoExplicitEncryptOptions` need no functional change. - -## 6. Test changes - -- **Spec submodule:** already bumped; includes `QE-Text-prefix/suffix*` (GA) and `*Preview` - files plus the `-ci-di` / `-preview` `encryptedFields` data. -- **Unified runner** (`ClientSideEncryptionTest`, sync + reactive): auto-discovers and runs the new - `QE-Text-*.json` unified tests — largely free coverage. -- **Prose test rewrite** (`AbstractClientEncryptionTextExplicitEncryptionTest`): rewrite to spec - test "27. String Explicit Encryption": - - Switch to algorithm `"String"` + `stringOptions`/`StringOptions`. - - Parameterize each case across `(prefix, prefix-suffix)` on server 9.0+/libmongocrypt 1.19.0+ - and `(prefixPreview, prefix-suffix-preview)` on server pre-9.0/libmongocrypt 1.19.1+. - - Add the `-ci-di` (case-/diacritic-insensitive) and `-preview` collections; substring carried - over as preview. - - Restore proper `assumeTrue` gating (currently commented out). - - Bump `REQUIRED_LIB_MONGOCRYPT_VERSION` to match the spec's per-case requirements. - - Update the case-7 error substring (now the `"string"` algorithm). - -### Auto-encryption pre-9.0 preview path — coverage notes -The auto-encryption path against a pre-9.0 server with `prefixPreview`/`suffixPreview` -`encryptedFields` **is** covered by the unified tests `QE-Text-prefixPreview` / -`QE-Text-suffixPreview` (`minServerVersion 8.2.0`, `maxServerVersion 8.99.99`, `csfle` -auto-encryption, no `bypassQueryAnalysis`, full insert+query round-trip). No new prose test needed. -Two execution conditions must hold, to verify during implementation: -1. These unified tests are **not skipped** by the Java runner / `UnifiedTestModifications` - (they require libmongocrypt 1.19.1+, which we now have). -2. The **CI test matrix includes a pre-9.0 server (8.2/8.3)** — otherwise these `maxServerVersion - 8.99.99` tests silently no-op and the path is "covered" on paper but never executed. - -## 7. Language wrappers - -Kotlin and Scala reuse `driver-core`'s `EncryptOptions`/`TextOptions` directly (no wrapper copies -found), so they inherit `stringOptions`/`StringOptions` automatically. Verify during planning. - -## 8. Risks - -- Prose-test version gating (GA vs. preview keyed on the server-9.0 boundary and libmongocrypt - 1.19.0/1.19.1) is the trickiest part — must match the spec precisely. -- Users still passing `"TextPreview"` as the algorithm get a libmongocrypt error on 1.19.1+ — - inherent to the bump, documented, not preventable in driver code. -- Pre-9.0 server absent from CI would hide the auto-encryption preview-path coverage (see §6). From 5b91f0def13ccc0677a35c2f128ff616bee6f427 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 18:58:50 +0100 Subject: [PATCH 18/29] test(JAVA-6244): address Copilot review - substring GA docs, prose cases 10/11, precedence test - Update EncryptOptions/StringOptions Javadocs: substring is GA (queryType 'substring', server 9.0+), 'substringPreview' is the deprecated pre-9.0 alias (was still documented as experimental/preview). - Prose test cases 10/11 (auto-encrypted substring ci-di): run on server 9.0+ (assumeTrue(gaSupported)), matching the substring-ci-di 9.0+ setup requirement and the prefix/suffix ci-di cases 8/9. The spec's 'skip on 9.0.0+' contradicts its own setup; TODO-JAVA-6244 pending upstream clarification. - Add EncryptOptionsHelper precedence test: stringOptions wins when both stringOptions and textOptions set. --- .../client/model/vault/EncryptOptions.java | 17 +++++++------- .../client/model/vault/StringOptions.java | 5 +---- .../vault/EncryptOptionsHelperTest.java | 22 +++++++++++++++++++ ...tEncryptionTextExplicitEncryptionTest.java | 12 +++++----- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java index 6235e64b988..4f85bd2571e 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java @@ -58,10 +58,9 @@ public EncryptOptions(final String algorithm) { *
  • String
  • * * - *

    The "String" algorithm supports Queryable Encryption prefix and suffix string queries, and (in preview) - * substring queries. Use the "String" algorithm with query types "prefix"/"suffix" (server 9.0+) or the - * deprecated aliases "prefixPreview"/"suffixPreview" (server 8.2 to pre-9.0), and "substringPreview" - * (experimental).

    + *

    The "String" algorithm supports Queryable Encryption prefix, suffix, and substring string queries. + * Use the "String" algorithm with query types "prefix"/"suffix"/"substring" (server 9.0+) or the deprecated + * aliases "prefixPreview"/"suffixPreview"/"substringPreview" (server 8.2 to pre-9.0).

    * * @return the encryption algorithm */ @@ -151,11 +150,11 @@ public Long getContentionFactor() { /** * The QueryType. * - *

    Currently, we support only "equality", "range", "prefix", "suffix", "prefixPreview", "suffixPreview" or - * "substringPreview" queryType.

    - *

    The "prefix", "suffix", "prefixPreview", "suffixPreview" and "substringPreview" query types are only valid - * with the "String" algorithm. "prefixPreview"/"suffixPreview" are deprecated aliases supported for servers 8.2 - * to pre-9.0; use "prefix"/"suffix" on server 9.0+.

    + *

    Currently, we support only "equality", "range", "prefix", "suffix", "substring", "prefixPreview", + * "suffixPreview" or "substringPreview" queryType.

    + *

    The "prefix", "suffix", "substring", "prefixPreview", "suffixPreview" and "substringPreview" query types are + * only valid with the "String" algorithm. "prefixPreview"/"suffixPreview"/"substringPreview" are deprecated + * aliases supported for servers 8.2 to pre-9.0; use "prefix"/"suffix"/"substring" on server 9.0+.

    *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "String".

    * @param queryType the query type * @return this diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java index 2b59cfdb999..50b6721c456 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java @@ -24,8 +24,7 @@ /** * String options for a Queryable Encryption field that supports string queries (prefix, suffix, and substring). * - *

    Note: StringOptions is in Alpha and subject to backwards breaking changes. The {@code substring} query support - * is experimental (preview) and may change in a future non-major release. + *

    Note: StringOptions is in Alpha and subject to backwards breaking changes. * * @since 5.9 * @mongodb.server.release 8.2 @@ -153,8 +152,6 @@ public BsonDocument getSuffixOptions() { /** * Set the substring options. * - *

    Note: substring string queries are experimental (preview) and may change in a future non-major release.

    - * *

    Expected to be a {@link BsonDocument} in the format of:

    * *
    diff --git a/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java
    index ea015185c55..65815e668a7 100644
    --- a/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java
    +++ b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java
    @@ -63,4 +63,26 @@ void shouldLeaveTextOptionsNullWhenNeitherSet() {
                     new EncryptOptions("Indexed"));
             assertNull(result.getTextOptions());
         }
    +
    +    @Test
    +    @SuppressWarnings("deprecation")
    +    void shouldPreferStringOptionsOverDeprecatedTextOptionsWhenBothSet() {
    +        EncryptOptions options = new EncryptOptions("String")
    +                .stringOptions(new StringOptions()
    +                        .caseSensitive(true)
    +                        .diacriticSensitive(true)
    +                        .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))
    +                .textOptions(new TextOptions()
    +                        .caseSensitive(false)
    +                        .diacriticSensitive(false)
    +                        .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 5, strMinQueryLength: 1}")));
    +
    +        MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options);
    +
    +        // stringOptions wins: prefix present (from stringOptions), no suffix (from the ignored textOptions),
    +        // and caseSensitive/diacriticSensitive reflect stringOptions (true), not textOptions (false).
    +        assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, "
    +                        + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"),
    +                result.getTextOptions());
    +    }
     }
    diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java
    index e8a5beae441..f02de4bf15f 100644
    --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java
    +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java
    @@ -284,9 +284,11 @@ public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() {
         @Test
         @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring")
         public void test10AutoEncryptedCaseInsensitiveSubstring() {
    -        // Spec: skip on server 9.0.0+. The substring-ci-di collection is only created on 9.0+ (GA "substring"
    -        // encryptedFields), so these regression cases effectively exercise the pre-9.0 substring behaviour only.
    -        assumeFalse(gaSupported);
    +        // GA substring auto-encryption requires server 9.0+ and libmongocrypt 1.20+, like the prefix/suffix ci-di
    +        // cases 8/9. The substring-ci-di collection is created only on 9.0+, so this case runs there.
    +        // TODO-JAVA-6244: the spec's "skip on 9.0.0+" for this case contradicts its own substring-ci-di 9.0+ setup
    +        // requirement; running on 9.0+ pending upstream spec clarification (DRIVERS-3540).
    +        assumeTrue(gaSupported);
             autoEncryptedDatabase.getCollection("substring-ci-di")
                     .insertOne(new Document("encryptedText", "FooBarBaz"));
     
    @@ -299,8 +301,8 @@ public void test10AutoEncryptedCaseInsensitiveSubstring() {
         @Test
         @DisplayName("Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring")
         public void test11AutoEncryptedDiacriticInsensitiveSubstring() {
    -        // Spec: skip on server 9.0.0+ (see Case 10).
    -        assumeFalse(gaSupported);
    +        // Runs on server 9.0+ with GA substring, like Case 10 (see its note).
    +        assumeTrue(gaSupported);
             autoEncryptedDatabase.getCollection("substring-ci-di")
                     .insertOne(new Document("encryptedText", "foocafébaz"));
     
    
    From 317ef2d7e1dcb0cb23564fd1950b6f8c5ca7393d Mon Sep 17 00:00:00 2001
    From: Nabil Hachicha 
    Date: Sat, 4 Jul 2026 19:31:07 +0100
    Subject: [PATCH 19/29] docs(JAVA-6244): describe
     MongoExplicitEncryptOptions.textOptions as String options
    
    The algorithm is now 'String'; the builder Javadoc header said 'Text Options' while the body
    said 'String'. Describe it as String options (method name kept to mirror the unchanged
    libmongocrypt 'textOptions' BSON field).
    ---
     .../internal/crypt/capi/MongoExplicitEncryptOptions.java   | 7 ++++---
     1 file changed, 4 insertions(+), 3 deletions(-)
    
    diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java
    index 992b5bf479d..f94929448c0 100644
    --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java
    +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java
    @@ -126,11 +126,12 @@ public Builder rangeOptions(final BsonDocument rangeOptions) {
             }
     
             /**
    -         * The Text Options.
    +         * The String Options.
              *
    -         * 

    Only applies when algorithm is "String".

    + *

    Only applies when algorithm is "String". The method name mirrors the libmongocrypt {@code textOptions} + * BSON field, which is unchanged.

    * - * @param textOptions the text options + * @param textOptions the string options * @return this * @since 5.6 */ From 676995ea7115bd36ca482268f8dd7f3f215dcf9d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 19:49:10 +0100 Subject: [PATCH 20/29] test(JAVA-6244): gate GA substring cases by libmongocrypt 1.20+ GA 'substring' needs server 9.0+ AND libmongocrypt 1.20+. Introduce substringGaSupported (gaSupported && mongocrypt>=1.20) and gate the substring collection creation/seed and cases 5/6/10/11 on it, so a 9.0+ run with a locally-supplied libmongocrypt <1.20 skips the GA substring cases instead of failing at runtime. Prefix/suffix (1.19) and preview (1.18) paths keep the class-level 1.19.1 minimum. --- ...tEncryptionTextExplicitEncryptionTest.java | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java index f02de4bf15f..b6d4de83d5a 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java @@ -63,7 +63,10 @@ public abstract class AbstractClientEncryptionTextExplicitEncryptionTest { private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); + // GA "substring" (server 9.0+) needs libmongocrypt 1.20+; prefix/suffix GA and the preview paths need only 1.19/1.18. + private static final ServerVersion SUBSTRING_GA_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 20, 0)); private boolean gaSupported; + private boolean substringGaSupported; private MongoClient explicitEncryptedClient; private MongoClient autoEncryptedClient; private MongoDatabase explicitEncryptedDatabase; @@ -82,6 +85,7 @@ public void setUp() { assumeFalse(isStandalone()); gaSupported = serverVersionAtLeast(9, 0); + substringGaSupported = gaSupported && getMongoCryptVersion().compareTo(SUBSTRING_GA_LIB_MONGOCRYPT_VERSION) >= 0; MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys"); BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json"); @@ -91,12 +95,14 @@ public void setUp() { if (gaSupported) { createEncryptedCollection("prefix-suffix", "encryptedFields-prefix-suffix.json"); createEncryptedCollection("prefix-suffix-ci-di", "encryptedFields-prefix-suffix-ci-di.json"); - createEncryptedCollection("substring", "encryptedFields-substring.json"); - createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); } else { createEncryptedCollection("prefix-suffix-preview", "encryptedFields-prefix-suffix-preview.json"); createEncryptedCollection("substring-preview", "encryptedFields-substring-preview.json"); } + if (substringGaSupported) { + createEncryptedCollection("substring", "encryptedFields-substring.json"); + createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); + } MongoCollection dataKeysCollection = getMongoClient() .getDatabase(dataKeysNamespace.getDatabaseName()) @@ -149,18 +155,21 @@ public void setUp() { .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); } - // Seed the substring collection (GA on 9.0+, preview on pre-9.0) with an encrypted "foobarbaz" document. - BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), - new EncryptOptions("String") - .keyId(key1Id) - .contentionFactor(0L) - .stringOptions(new StringOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse( - "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}")))); - explicitEncryptedDatabase.getCollection(gaSupported ? "substring" : "substring-preview") - .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); + // Seed the substring collection: GA "substring" on 9.0+/libmongocrypt 1.20+, preview on pre-9.0. + // Skipped on 9.0+ with libmongocrypt < 1.20, where neither path is available. + if (substringGaSupported || !gaSupported) { + BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), + new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .substringOptions(BsonDocument.parse( + "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}")))); + explicitEncryptedDatabase.getCollection(gaSupported ? "substring" : "substring-preview") + .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); + } } @Test @@ -210,6 +219,8 @@ public void test4AssertNoDocumentFoundBySuffix() { @Test @DisplayName("Case 5: can find a document by substring") public void test5CanFindADocumentBySubstring() { + // On 9.0+, GA substring needs libmongocrypt 1.20+; on pre-9.0 the preview path applies. + assumeTrue(substringGaSupported || !gaSupported); String queryType = gaSupported ? "substring" : "substringPreview"; String collection = gaSupported ? "substring" : "substring-preview"; BsonBinary encrypted = encryptForSubstring("bar", queryType, true, true); @@ -221,6 +232,8 @@ public void test5CanFindADocumentBySubstring() { @Test @DisplayName("Case 6: assert no document found by substring") public void test6AssertNoDocumentFoundBySubstring() { + // On 9.0+, GA substring needs libmongocrypt 1.20+; on pre-9.0 the preview path applies. + assumeTrue(substringGaSupported || !gaSupported); String queryType = gaSupported ? "substring" : "substringPreview"; String collection = gaSupported ? "substring" : "substring-preview"; BsonBinary encrypted = encryptForSubstring("qux", queryType, true, true); @@ -285,10 +298,10 @@ public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring") public void test10AutoEncryptedCaseInsensitiveSubstring() { // GA substring auto-encryption requires server 9.0+ and libmongocrypt 1.20+, like the prefix/suffix ci-di - // cases 8/9. The substring-ci-di collection is created only on 9.0+, so this case runs there. + // cases 8/9. The substring-ci-di collection is created only under those conditions, so this case runs there. // TODO-JAVA-6244: the spec's "skip on 9.0.0+" for this case contradicts its own substring-ci-di 9.0+ setup // requirement; running on 9.0+ pending upstream spec clarification (DRIVERS-3540). - assumeTrue(gaSupported); + assumeTrue(substringGaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") .insertOne(new Document("encryptedText", "FooBarBaz")); @@ -301,8 +314,8 @@ public void test10AutoEncryptedCaseInsensitiveSubstring() { @Test @DisplayName("Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring") public void test11AutoEncryptedDiacriticInsensitiveSubstring() { - // Runs on server 9.0+ with GA substring, like Case 10 (see its note). - assumeTrue(gaSupported); + // Runs on server 9.0+ with GA substring (libmongocrypt 1.20+), like Case 10 (see its note). + assumeTrue(substringGaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") .insertOne(new Document("encryptedText", "foocafébaz")); From 5045c856c4c9b696eb929c421d0e8b05d704cbc0 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 19:58:48 +0100 Subject: [PATCH 21/29] test(JAVA-6244): address Copilot - rename Text test class to String, fix TODO format - Rename AbstractClientEncryptionTextExplicitEncryptionTest and both concrete subclasses (sync + reactive) to ...StringExplicitEncryptionTest, matching the Text->String API rename. - Fix TODO comment to the repo convention 'TODO JAVA-6244' (was 'TODO-JAVA-6244'). --- ...java => ClientEncryptionStringExplicitEncryptionTest.java} | 4 ++-- ...AbstractClientEncryptionStringExplicitEncryptionTest.java} | 4 ++-- ...java => ClientEncryptionStringExplicitEncryptionTest.java} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/{ClientEncryptionTextExplicitEncryptionTest.java => ClientEncryptionStringExplicitEncryptionTest.java} (87%) rename driver-sync/src/test/functional/com/mongodb/client/{AbstractClientEncryptionTextExplicitEncryptionTest.java => AbstractClientEncryptionStringExplicitEncryptionTest.java} (99%) rename driver-sync/src/test/functional/com/mongodb/client/{ClientEncryptionTextExplicitEncryptionTest.java => ClientEncryptionStringExplicitEncryptionTest.java} (90%) diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java similarity index 87% rename from driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java rename to driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java index 2c7eda55aae..d53f4b2c331 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java @@ -18,14 +18,14 @@ import com.mongodb.ClientEncryptionSettings; import com.mongodb.MongoClientSettings; -import com.mongodb.client.AbstractClientEncryptionTextExplicitEncryptionTest; +import com.mongodb.client.AbstractClientEncryptionStringExplicitEncryptionTest; import com.mongodb.client.MongoClient; import com.mongodb.client.vault.ClientEncryption; import com.mongodb.reactivestreams.client.syncadapter.SyncClientEncryption; import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; import com.mongodb.reactivestreams.client.vault.ClientEncryptions; -public class ClientEncryptionTextExplicitEncryptionTest extends AbstractClientEncryptionTextExplicitEncryptionTest { +public class ClientEncryptionStringExplicitEncryptionTest extends AbstractClientEncryptionStringExplicitEncryptionTest { @Override protected MongoClient createMongoClient(final MongoClientSettings settings) { return new SyncMongoClient(settings); diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java similarity index 99% rename from driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java rename to driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java index b6d4de83d5a..669ebf7af1a 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java @@ -60,7 +60,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import static util.JsonPoweredTestHelper.getTestDocument; -public abstract class AbstractClientEncryptionTextExplicitEncryptionTest { +public abstract class AbstractClientEncryptionStringExplicitEncryptionTest { private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); // GA "substring" (server 9.0+) needs libmongocrypt 1.20+; prefix/suffix GA and the preview paths need only 1.19/1.18. @@ -299,7 +299,7 @@ public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { public void test10AutoEncryptedCaseInsensitiveSubstring() { // GA substring auto-encryption requires server 9.0+ and libmongocrypt 1.20+, like the prefix/suffix ci-di // cases 8/9. The substring-ci-di collection is created only under those conditions, so this case runs there. - // TODO-JAVA-6244: the spec's "skip on 9.0.0+" for this case contradicts its own substring-ci-di 9.0+ setup + // TODO JAVA-6244 the spec's "skip on 9.0.0+" for this case contradicts its own substring-ci-di 9.0+ setup // requirement; running on 9.0+ pending upstream spec clarification (DRIVERS-3540). assumeTrue(substringGaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java similarity index 90% rename from driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java rename to driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java index 23bd9ec135d..e0614a0d450 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java @@ -21,7 +21,7 @@ import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryptions; -public class ClientEncryptionTextExplicitEncryptionTest extends AbstractClientEncryptionTextExplicitEncryptionTest { +public class ClientEncryptionStringExplicitEncryptionTest extends AbstractClientEncryptionStringExplicitEncryptionTest { @Override protected MongoClient createMongoClient(final MongoClientSettings settings) { return MongoClients.create(settings); From 71d8e8a7929c08499205a22990bca08f3a6d7e7a Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 20:50:54 +0100 Subject: [PATCH 22/29] test(JAVA-6244): use primitive boolean for String/TextOptions sensitivity flags caseSensitive/diacriticSensitive are required flags with primitive (non-null) getters, so use primitive boolean fields (matching IndexOptions-style required flags) rather than boxed Boolean. Auto-defaults to false and removes the unboxing-NPE risk, superseding the earlier '= false' patch. --- .../main/com/mongodb/client/model/vault/StringOptions.java | 4 ++-- .../src/main/com/mongodb/client/model/vault/TextOptions.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java index 50b6721c456..98795335082 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java @@ -32,8 +32,8 @@ */ @Alpha(Reason.SERVER) public class StringOptions { - private Boolean caseSensitive = false; - private Boolean diacriticSensitive = false; + private boolean caseSensitive; + private boolean diacriticSensitive; @Nullable private BsonDocument prefixOptions; @Nullable diff --git a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java index bcd5a7ff2da..a14ca54378e 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java @@ -34,8 +34,8 @@ @Deprecated @Alpha(Reason.SERVER) public class TextOptions { - private Boolean caseSensitive = false; - private Boolean diacriticSensitive = false; + private boolean caseSensitive; + private boolean diacriticSensitive; @Nullable private BsonDocument prefixOptions; @Nullable From f39e5e2e79491b6bab220808f88829df66d3dfbc Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 21:48:19 +0100 Subject: [PATCH 23/29] docs(JAVA-6244): sync getQueryType javadoc, clarify @param and version comment - EncryptOptions.getQueryType() javadoc referenced only equality/range; point it to queryType(String) so it stays consistent with the supported-types list. - MongoExplicitEncryptOptions.textOptions @param: clarify it takes the string options as a BSON document. - Prose test: reword the libmongocrypt version comment to reflect the class-level 1.19.1 floor. --- .../main/com/mongodb/client/model/vault/EncryptOptions.java | 2 +- .../AbstractClientEncryptionStringExplicitEncryptionTest.java | 3 ++- .../internal/crypt/capi/MongoExplicitEncryptOptions.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java index 4f85bd2571e..e39deea3eec 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java @@ -169,7 +169,7 @@ public EncryptOptions queryType(@Nullable final String queryType) { /** * Gets the QueryType. * - *

    Currently, we support only "equality" or "range" queryType.

    + *

    See {@link #queryType(String)} for the supported query types.

    * @see #queryType(String) * @return the queryType or null * @since 4.7 diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java index 669ebf7af1a..011cf8325fe 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java @@ -63,7 +63,8 @@ public abstract class AbstractClientEncryptionStringExplicitEncryptionTest { private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); - // GA "substring" (server 9.0+) needs libmongocrypt 1.20+; prefix/suffix GA and the preview paths need only 1.19/1.18. + // GA "substring" (server 9.0+) additionally needs libmongocrypt 1.20+; the class-level 1.19.1 minimum below + // already covers prefix/suffix GA and the pre-9.0 preview query types. private static final ServerVersion SUBSTRING_GA_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 20, 0)); private boolean gaSupported; private boolean substringGaSupported; diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java index f94929448c0..8b37549ca37 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java @@ -131,7 +131,7 @@ public Builder rangeOptions(final BsonDocument rangeOptions) { *

    Only applies when algorithm is "String". The method name mirrors the libmongocrypt {@code textOptions} * BSON field, which is unchanged.

    * - * @param textOptions the string options + * @param textOptions the string options, as a BSON document * @return this * @since 5.6 */ From d93d7931f67352833b98936dc2ecc370e1461d5e Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 21:50:53 +0100 Subject: [PATCH 24/29] docs(JAVA-6244): make libmongocrypt signature-asset KDoc example version-agnostic The example hard-coded 1.18.1, now stale after the 1.20.0 bump; use a placeholder. --- mongodb-crypt/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 0d233d410dd..411113f6cfc 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -76,7 +76,7 @@ val downloadUrlBase = "https://github.com/mongodb/libmongocrypt/releases/downloa * layout differ per platform, so both must be tracked explicitly. * * libmongocrypt's signature assets replace the `.tar.gz` suffix with `.asc` (e.g. - * `libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-1.18.1.asc`). + * `libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-.asc`). */ data class CryptBinary(val jnaPlatform: String, val tarball: String, val libPathInTarball: String) { val signature: String = tarball.removeSuffix(".tar.gz") + ".asc" From 97ad4a2d0a103c2fce131cc11c455a9485ec8754 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 4 Jul 2026 23:13:09 +0100 Subject: [PATCH 25/29] docs(JAVA-6244): fix stale gpg keyring comment in mongodb-crypt build The VerifyLibmongocryptTask KDoc still said the keyring lives under build/ and that clean resets trust; the homedir now lives under the system temp dir and verify() recreates it each run. Correct the comment to match. --- mongodb-crypt/build.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 411113f6cfc..208034beaad 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -138,8 +138,9 @@ tasks.register("downloadCryptLibs") { * Per DRIVERS-3441, drivers that bundle libmongocrypt must verify GPG signatures of * release tarballs against the official MongoDB libmongocrypt signing key. * - * The keyring is kept under `build/` so this task does not touch the developer's - * system GPG keyring and so `./gradlew clean` resets the trust state. + * The task uses a scratch keyring (not the developer's system GPG keyring), which {@code verify()} + * recreates on every run so the trust state is always reset. It lives under the system temp dir + * rather than {@code build/} — see the {@code gnupgHome} assignment in {@code verifyCryptLibs} for why. */ val skipCryptVerify = providers.gradleProperty("skipCryptVerify").map { it.toBoolean() }.orElse(false) From 179111038fd1c2c2ae0d7dfeb91d1dbadb969e40 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 6 Jul 2026 13:24:31 +0100 Subject: [PATCH 26/29] feat(JAVA-6244): mark GA String QE API stable (drop @Alpha) QE prefix/suffix/substring string queries are GA (libmongocrypt "Stable support" in 1.19/1.20, server 9.0), so the @Alpha(Reason.SERVER) marking no longer applies. Remove @Alpha from the StringOptions class and EncryptOptions.stringOptions()/getStringOptions(). The deprecated TextOptions path keeps @Alpha (removable without a major bump). @mongodb.server.release stays 8.2 (the feature is usable from 8.2 via the preview query types; GA query types need 9.0). --- .../main/com/mongodb/client/model/vault/EncryptOptions.java | 2 -- .../main/com/mongodb/client/model/vault/StringOptions.java | 5 ----- 2 files changed, 7 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java index e39deea3eec..bdd2eeb5be6 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java @@ -251,7 +251,6 @@ public TextOptions getTextOptions() { * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption */ - @Alpha(Reason.SERVER) public EncryptOptions stringOptions(@Nullable final StringOptions stringOptions) { this.stringOptions = stringOptions; return this; @@ -265,7 +264,6 @@ public EncryptOptions stringOptions(@Nullable final StringOptions stringOptions) * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption */ - @Alpha(Reason.SERVER) @Nullable public StringOptions getStringOptions() { return stringOptions; diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java index 98795335082..21e9ba3f782 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java @@ -16,21 +16,16 @@ package com.mongodb.client.model.vault; -import com.mongodb.annotations.Alpha; -import com.mongodb.annotations.Reason; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; /** * String options for a Queryable Encryption field that supports string queries (prefix, suffix, and substring). * - *

    Note: StringOptions is in Alpha and subject to backwards breaking changes. - * * @since 5.9 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption */ -@Alpha(Reason.SERVER) public class StringOptions { private boolean caseSensitive; private boolean diacriticSensitive; From a97b8d2435c1c5c96511fd73f43e38c202c52f2b Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 6 Jul 2026 14:43:36 +0100 Subject: [PATCH 27/29] test(JAVA-6244): gate substring cases on gaSupported only The driver bundles a fixed libmongocrypt (1.20.0), so getMongoCryptVersion() is constant in CI and the substring-GA libmongocrypt gate was always satisfied (substringGaSupported == gaSupported). Drop SUBSTRING_GA_LIB_MONGOCRYPT_VERSION/substringGaSupported and gate substring collection creation, seed, and cases 5/6/10/11 on gaSupported (server version, which does vary across CI). Also trims two over-verbose doc comments. --- .../vault/EncryptOptionsHelperTest.java | 2 - ...ncryptionStringExplicitEncryptionTest.java | 52 +++++++------------ .../capi/MongoExplicitEncryptOptions.java | 3 +- 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java index 65815e668a7..3835b715ce4 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java @@ -79,8 +79,6 @@ void shouldPreferStringOptionsOverDeprecatedTextOptionsWhenBothSet() { MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); - // stringOptions wins: prefix present (from stringOptions), no suffix (from the ignored textOptions), - // and caseSensitive/diacriticSensitive reflect stringOptions (true), not textOptions (false). assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, " + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), result.getTextOptions()); diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java index 011cf8325fe..8fef0a04a0e 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java @@ -63,11 +63,7 @@ public abstract class AbstractClientEncryptionStringExplicitEncryptionTest { private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); - // GA "substring" (server 9.0+) additionally needs libmongocrypt 1.20+; the class-level 1.19.1 minimum below - // already covers prefix/suffix GA and the pre-9.0 preview query types. - private static final ServerVersion SUBSTRING_GA_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 20, 0)); private boolean gaSupported; - private boolean substringGaSupported; private MongoClient explicitEncryptedClient; private MongoClient autoEncryptedClient; private MongoDatabase explicitEncryptedDatabase; @@ -86,7 +82,6 @@ public void setUp() { assumeFalse(isStandalone()); gaSupported = serverVersionAtLeast(9, 0); - substringGaSupported = gaSupported && getMongoCryptVersion().compareTo(SUBSTRING_GA_LIB_MONGOCRYPT_VERSION) >= 0; MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys"); BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json"); @@ -96,14 +91,12 @@ public void setUp() { if (gaSupported) { createEncryptedCollection("prefix-suffix", "encryptedFields-prefix-suffix.json"); createEncryptedCollection("prefix-suffix-ci-di", "encryptedFields-prefix-suffix-ci-di.json"); + createEncryptedCollection("substring", "encryptedFields-substring.json"); + createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); } else { createEncryptedCollection("prefix-suffix-preview", "encryptedFields-prefix-suffix-preview.json"); createEncryptedCollection("substring-preview", "encryptedFields-substring-preview.json"); } - if (substringGaSupported) { - createEncryptedCollection("substring", "encryptedFields-substring.json"); - createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); - } MongoCollection dataKeysCollection = getMongoClient() .getDatabase(dataKeysNamespace.getDatabaseName()) @@ -156,21 +149,18 @@ public void setUp() { .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); } - // Seed the substring collection: GA "substring" on 9.0+/libmongocrypt 1.20+, preview on pre-9.0. - // Skipped on 9.0+ with libmongocrypt < 1.20, where neither path is available. - if (substringGaSupported || !gaSupported) { - BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), - new EncryptOptions("String") - .keyId(key1Id) - .contentionFactor(0L) - .stringOptions(new StringOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse( - "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}")))); - explicitEncryptedDatabase.getCollection(gaSupported ? "substring" : "substring-preview") - .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); - } + // Seed the substring collection: GA "substring" on 9.0+, preview on pre-9.0. + BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), + new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .substringOptions(BsonDocument.parse( + "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}")))); + explicitEncryptedDatabase.getCollection(gaSupported ? "substring" : "substring-preview") + .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); } @Test @@ -220,8 +210,6 @@ public void test4AssertNoDocumentFoundBySuffix() { @Test @DisplayName("Case 5: can find a document by substring") public void test5CanFindADocumentBySubstring() { - // On 9.0+, GA substring needs libmongocrypt 1.20+; on pre-9.0 the preview path applies. - assumeTrue(substringGaSupported || !gaSupported); String queryType = gaSupported ? "substring" : "substringPreview"; String collection = gaSupported ? "substring" : "substring-preview"; BsonBinary encrypted = encryptForSubstring("bar", queryType, true, true); @@ -233,8 +221,6 @@ public void test5CanFindADocumentBySubstring() { @Test @DisplayName("Case 6: assert no document found by substring") public void test6AssertNoDocumentFoundBySubstring() { - // On 9.0+, GA substring needs libmongocrypt 1.20+; on pre-9.0 the preview path applies. - assumeTrue(substringGaSupported || !gaSupported); String queryType = gaSupported ? "substring" : "substringPreview"; String collection = gaSupported ? "substring" : "substring-preview"; BsonBinary encrypted = encryptForSubstring("qux", queryType, true, true); @@ -298,11 +284,11 @@ public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { @Test @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring") public void test10AutoEncryptedCaseInsensitiveSubstring() { - // GA substring auto-encryption requires server 9.0+ and libmongocrypt 1.20+, like the prefix/suffix ci-di - // cases 8/9. The substring-ci-di collection is created only under those conditions, so this case runs there. + // GA substring auto-encryption requires server 9.0+ (and libmongocrypt 1.20+, which the driver bundles), + // like the prefix/suffix ci-di cases 8/9; the substring-ci-di collection is created only on 9.0+. // TODO JAVA-6244 the spec's "skip on 9.0.0+" for this case contradicts its own substring-ci-di 9.0+ setup // requirement; running on 9.0+ pending upstream spec clarification (DRIVERS-3540). - assumeTrue(substringGaSupported); + assumeTrue(gaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") .insertOne(new Document("encryptedText", "FooBarBaz")); @@ -315,8 +301,8 @@ public void test10AutoEncryptedCaseInsensitiveSubstring() { @Test @DisplayName("Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring") public void test11AutoEncryptedDiacriticInsensitiveSubstring() { - // Runs on server 9.0+ with GA substring (libmongocrypt 1.20+), like Case 10 (see its note). - assumeTrue(substringGaSupported); + // Runs on server 9.0+ with GA substring, like Case 10 (see its note). + assumeTrue(gaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") .insertOne(new Document("encryptedText", "foocafébaz")); diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java index 8b37549ca37..006b00e034e 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java @@ -128,8 +128,7 @@ public Builder rangeOptions(final BsonDocument rangeOptions) { /** * The String Options. * - *

    Only applies when algorithm is "String". The method name mirrors the libmongocrypt {@code textOptions} - * BSON field, which is unchanged.

    + *

    Only applies when algorithm is "String".

    * * @param textOptions the string options, as a BSON document * @return this From b8135a9145f5c4b78064007ffbe1f19cb3cfbc1c Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 6 Jul 2026 15:36:21 +0100 Subject: [PATCH 28/29] test(JAVA-6244): trim explanatory comments on substring ci-di cases 10/11 --- ...AbstractClientEncryptionStringExplicitEncryptionTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java index 8fef0a04a0e..8ffad7e865a 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java @@ -284,10 +284,6 @@ public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { @Test @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring") public void test10AutoEncryptedCaseInsensitiveSubstring() { - // GA substring auto-encryption requires server 9.0+ (and libmongocrypt 1.20+, which the driver bundles), - // like the prefix/suffix ci-di cases 8/9; the substring-ci-di collection is created only on 9.0+. - // TODO JAVA-6244 the spec's "skip on 9.0.0+" for this case contradicts its own substring-ci-di 9.0+ setup - // requirement; running on 9.0+ pending upstream spec clarification (DRIVERS-3540). assumeTrue(gaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") .insertOne(new Document("encryptedText", "FooBarBaz")); @@ -301,7 +297,6 @@ public void test10AutoEncryptedCaseInsensitiveSubstring() { @Test @DisplayName("Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring") public void test11AutoEncryptedDiacriticInsensitiveSubstring() { - // Runs on server 9.0+ with GA substring, like Case 10 (see its note). assumeTrue(gaSupported); autoEncryptedDatabase.getCollection("substring-ci-di") .insertOne(new Document("encryptedText", "foocafébaz")); From e452d7c63a1697a729ab021d62678667decfda7d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 6 Jul 2026 23:46:37 +0100 Subject: [PATCH 29/29] feat(JAVA-6244): address review - add StringOptions/TextOptions toString, rename lib-version constant - Add toString() to StringOptions and TextOptions (parity; aids debugging) + a toString unit test. - Rename REQUIRED_LIB_MONGOCRYPT_VERSION -> MIN_SUPPORTED_LIB_MONGOCRYPT_VERSION (clearer intent) and set it to 1.20.0 (the bundled version); GA/preview server-version branching (gaSupported) kept. --- .../mongodb/client/model/vault/StringOptions.java | 11 +++++++++++ .../com/mongodb/client/model/vault/TextOptions.java | 10 ++++++++++ .../client/model/vault/StringOptionsTest.java | 13 +++++++++++++ ...lientEncryptionStringExplicitEncryptionTest.java | 4 ++-- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java index 21e9ba3f782..5563f84fcce 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java @@ -178,4 +178,15 @@ public StringOptions substringOptions(@Nullable final BsonDocument substringOpti public BsonDocument getSubstringOptions() { return substringOptions; } + + @Override + public String toString() { + return "StringOptions{" + + "caseSensitive=" + caseSensitive + + ", diacriticSensitive=" + diacriticSensitive + + ", prefixOptions=" + prefixOptions + + ", suffixOptions=" + suffixOptions + + ", substringOptions=" + substringOptions + + '}'; + } } diff --git a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java index a14ca54378e..2520cbc9bf4 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java @@ -186,4 +186,14 @@ public BsonDocument getSubstringOptions() { return substringOptions; } + @Override + public String toString() { + return "TextOptions{" + + "caseSensitive=" + caseSensitive + + ", diacriticSensitive=" + diacriticSensitive + + ", prefixOptions=" + prefixOptions + + ", suffixOptions=" + suffixOptions + + ", substringOptions=" + substringOptions + + '}'; + } } diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java index 04f20239423..4568f1b53eb 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java @@ -60,4 +60,17 @@ void shouldDefaultBooleanFlagsToFalse() { assertFalse(options.getCaseSensitive()); assertFalse(options.getDiacriticSensitive()); } + + @Test + void shouldIncludeAllFieldsInToString() { + String result = new StringOptions() + .caseSensitive(true) + .diacriticSensitive(false) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + .toString(); + + assertEquals("StringOptions{caseSensitive=true, diacriticSensitive=false, " + + "prefixOptions={\"strMaxQueryLength\": 10, \"strMinQueryLength\": 2}, " + + "suffixOptions=null, substringOptions=null}", result); + } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java index 8ffad7e865a..89ed6b61e55 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java @@ -62,7 +62,7 @@ public abstract class AbstractClientEncryptionStringExplicitEncryptionTest { - private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); + private static final ServerVersion MIN_SUPPORTED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 20, 0)); private boolean gaSupported; private MongoClient explicitEncryptedClient; private MongoClient autoEncryptedClient; @@ -77,7 +77,7 @@ public abstract class AbstractClientEncryptionStringExplicitEncryptionTest { @BeforeEach public void setUp() { assumeTrue(hasEncryptionTestsEnabled(), "String explicit encryption tests disabled"); - assumeTrue(getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0, "Requires newer MongoCrypt version"); + assumeTrue(getMongoCryptVersion().compareTo(MIN_SUPPORTED_LIB_MONGOCRYPT_VERSION) >= 0, "Requires newer MongoCrypt version"); assumeTrue(serverVersionAtLeast(8, 2)); assumeFalse(isStandalone());