diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..6eef34e45
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,50 @@
+# Project Guidelines
+
+## Architecture
+- `marklogic-client-api`: core Java client library for interacting with the MarkLogic REST API.
+- `ml-development-tools`: Gradle plugin and generators for Data Services endpoint proxies/tests.
+- `marklogic-client-api-functionaltests`: functional/regression-style tests, split into fragile/fast/slow groups.
+- `test-app`: ml-gradle deployment project that provisions test infrastructure in MarkLogic.
+- `examples`: supporting code used by tests and usage examples.
+
+## Build And Test
+- Prefer Gradle from the repo root.
+- Quick compile verification: `./gradlew clean build -x test`
+- Core module tests: `./gradlew marklogic-client-api:test`
+- Plugin tests (includes generated tests workflow): `./gradlew ml-development-tools:test`
+- Functional tests must run in this order to reduce flakiness:
+ 1. `./gradlew marklogic-client-api-functionaltests:runFragileTests`
+ 2. `./gradlew marklogic-client-api-functionaltests:runFastFunctionalTests`
+ 3. `./gradlew marklogic-client-api-functionaltests:runSlowFunctionalTests`
+
+## Test Environment
+- Java 17+ is required for current releases.
+- Most tests require a running MarkLogic instance and deployed test resources.
+- Typical setup sequence:
+ 1. `docker compose up -d --build`
+ 2. `./gradlew -i mlWaitTillReady`
+ 3. `./gradlew -i mlDeploy`
+ 4. Run module tests
+- Override local MarkLogic connection settings via `gradle-local.properties` (`mlHost`, `mlPassword`).
+
+## Quality Controls
+- Treat compile warnings as failures: project builds enforce `-Xlint:unchecked`, `-Xlint:deprecation`, and `-Werror`.
+- Keep dependency security constraints intact (e.g. forced/excluded dependencies for CVE mitigation in Gradle files).
+- When adding or changing first-party Java/Kotlin code, run security scanning steps used by this workspace workflow before finalizing changes.
+- Do not relax quality gates (tests/compilation) to make a change pass; fix the underlying issue.
+
+## Code Generation And Automation
+- Data Services proxy generation is automated; use `generateEndpointProxies` instead of hand-writing proxy classes.
+- `ml-development-tools` test automation uses `generateTests` and `fixMjsModulesForMarkLogic12` before `test`.
+- Generated sources commonly include an "IMPORTANT: Do not edit" header. Regenerate from source declarations instead of editing generated output directly.
+- For changes affecting generation logic, validate both generator behavior and generated artifact compilation/tests.
+
+## Conventions For Changes
+- Keep edits scoped to the target module; avoid cross-module churn unless required.
+- Prefer existing patterns in nearby code over introducing new abstractions.
+- For test-related fixes, document whether behavior changes impact unit tests, functional tests, or deployment setup.
+
+## Docs To Link (Do Not Duplicate)
+- `README.md`: product overview, dependency usage, Java compatibility.
+- `CONTRIBUTING.md`: local build/test workflow and MarkLogic test setup.
+- `ml-development-tools/src/test/example-project/README.md`: plugin-focused usage/testing notes.
diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/expression/CtsExpr.java b/marklogic-client-api/src/main/java/com/marklogic/client/expression/CtsExpr.java
index a01b41fe1..8e957a865 100644
--- a/marklogic-client-api/src/main/java/com/marklogic/client/expression/CtsExpr.java
+++ b/marklogic-client-api/src/main/java/com/marklogic/client/expression/CtsExpr.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.expression;
@@ -38,6 +38,7 @@
// 2023-10-24 Exception: Manual changes have been made to this to expose the string constructors for cts.point and
// cts.polygon. These changes can be removed once optic-defs.json in the xdmp repository is updated to define these
// constructors.
+// 2026-04-15 Exception: Manual changes have been made to expose cts:param prior to generator support.
/**
* Builds expressions to call functions in the cts server library for a row
@@ -2545,6 +2546,24 @@ public interface CtsExpr {
* @return a server expression with the cts:query server data type
*/
public CtsQueryExpr orQuery(ServerExpression queries, XsStringSeqVal options);
+/**
+ * Returns a parameter placeholder for a cts expression.
+ *
+ *
+ * Provides a client interface to the cts:param server function.
+ * @param name The parameter name. (of xs:string)
+ * @return a server expression with the xs:anyAtomicType server data type
+ */
+ public ServerExpression param(String name);
+/**
+ * Returns a parameter placeholder for a cts expression.
+ *
+ *
+ * Provides a client interface to the cts:param server function.
+ * @param name The parameter name. (of xs:string)
+ * @return a server expression with the xs:anyAtomicType server data type
+ */
+ public ServerExpression param(XsStringVal name);
/**
* Returns the part of speech for a cts:token, if any.
*
diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/BaseTypeImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/BaseTypeImpl.java
index 4c88251a8..86069b8c3 100644
--- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/BaseTypeImpl.java
+++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/BaseTypeImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.impl;
@@ -150,7 +150,7 @@ static class ServerExpressionListImpl extends BaseListImpl implemen
}
static class ServerExpressionCallImpl extends BaseCallImpl implements ServerExpression {
ServerExpressionCallImpl(String fnPrefix, String fnName, Object[] fnArgs) {
- super(fnPrefix, fnName, convertList(fnArgs));
+ super(fnPrefix, fnName, convertList(validateNoOpticParamInCtsCall(fnPrefix, fnName, fnArgs)));
}
}
@@ -394,6 +394,46 @@ static private void astifyObject(StringBuilder strb, Object value) {
}
}
+ static private Object[] validateNoOpticParamInCtsCall(String fnPrefix, String fnName, Object[] fnArgs) {
+ if (!"cts".equals(fnPrefix) || fnArgs == null) {
+ return fnArgs;
+ }
+ if (containsPlanParam(fnArgs)) {
+ throw new IllegalArgumentException(
+ "Cannot pass op:param() to cts:" + fnName + "(). Use cts:param() for cts namespace expressions."
+ );
+ }
+ return fnArgs;
+ }
+
+ static private boolean containsPlanParam(Object value) {
+ if (value == null) {
+ return false;
+ }
+ if (value instanceof PlanParamExpr) {
+ return true;
+ }
+ if (value instanceof Object[]) {
+ for (Object item : (Object[]) value) {
+ if (containsPlanParam(item)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (value instanceof BaseListImpl) {
+ return containsPlanParam(((BaseListImpl>) value).getArgsImpl());
+ }
+ if (value instanceof BaseMapImpl) {
+ return containsPlanParam(((BaseMapImpl) value).getMap().values().toArray());
+ }
+ if (value instanceof java.util.Map, ?>) {
+ java.util.Map, ?> mapValue = (java.util.Map, ?>) value;
+ return containsPlanParam(mapValue.keySet().toArray()) || containsPlanParam(mapValue.values().toArray());
+ }
+ return false;
+ }
+
static BaseArgImpl[] convertList(Object[] items) {
return convertList(items, BaseArgImpl.class);
}
diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/CtsExprImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/CtsExprImpl.java
index ac90029ed..4b32c17df 100644
--- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/CtsExprImpl.java
+++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/CtsExprImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.impl;
@@ -41,6 +41,7 @@
// 2023-10-24 Exception: Manual changes have been made to this to expose the string constructors for cts.point and
// cts.polygon. These changes can be removed once optic-defs.json in the xdmp repository is updated to define these
// constructors.
+// 2026-04-15 Exception: Manual changes have been made to expose cts:param prior to generator support.
class CtsExprImpl implements CtsExpr {
@@ -1684,6 +1685,24 @@ public CtsQueryExpr orQuery(ServerExpression queries, XsStringSeqVal options) {
}
+ @Override
+ public ServerExpression param(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("name parameter for param() cannot be null");
+ }
+ return param(new XsValueImpl.StringValImpl(name));
+ }
+
+
+ @Override
+ public ServerExpression param(XsStringVal name) {
+ if (name == null) {
+ throw new IllegalArgumentException("name parameter for param() cannot be null");
+ }
+ return new XsExprImpl.AnyAtomicTypeCallImpl("cts", "param", new Object[]{ name });
+ }
+
+
@Override
public ServerExpression partOfSpeech(ServerExpression token) {
if (token == null) {
diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderSubImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderSubImpl.java
index 2d6580458..a4e23db9e 100644
--- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderSubImpl.java
+++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderSubImpl.java
@@ -694,6 +694,7 @@ public PlanPrefixer prefixer(String base) {
public PlanParamExpr param(String name) {
return new PlanParamBase(name);
}
+
@Override
public PlanParamExpr param(XsStringVal name) {
if (name == null) {
diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/impl/CtsParamExprTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/impl/CtsParamExprTest.java
new file mode 100644
index 000000000..d29e9c309
--- /dev/null
+++ b/marklogic-client-api/src/test/java/com/marklogic/client/impl/CtsParamExprTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ */
+package com.marklogic.client.impl;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.marklogic.client.expression.PlanBuilder;
+import com.marklogic.client.io.JacksonHandle;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class CtsParamExprTest {
+
+ @Test
+ void exportsCtsParamInCollectionQuery() {
+ PlanBuilderSubImpl p = new PlanBuilderSubImpl();
+
+ PlanBuilder.ModifyPlan employeesPlan = p
+ .fromView("main", "employees")
+ .select(p.col("EmployeeID"), p.col("FirstName"), p.col("LastName"))
+ .where(p.cts.collectionQuery(p.cts.param("collection")));
+
+ JacksonHandle handle = new JacksonHandle();
+ employeesPlan.export(handle);
+ ObjectNode exportNode = (ObjectNode) handle.get();
+
+ assertEquals("op", exportNode.path("$optic").path("ns").asText());
+ assertEquals("operators", exportNode.path("$optic").path("fn").asText());
+ assertEquals("from-view", exportNode.path("$optic").path("args").get(0).path("fn").asText());
+ assertEquals("select", exportNode.path("$optic").path("args").get(1).path("fn").asText());
+ assertEquals("where", exportNode.path("$optic").path("args").get(2).path("fn").asText());
+ assertEquals("collection-query", exportNode.path("$optic").path("args").get(2).path("args").get(0).path("fn").asText());
+ assertEquals("param", exportNode.path("$optic").path("args").get(2).path("args").get(0).path("args").get(0).path("fn").asText());
+ assertEquals("cts", exportNode.path("$optic").path("args").get(2).path("args").get(0).path("args").get(0).path("ns").asText());
+ assertEquals("collection", exportNode.path("$optic").path("args").get(2).path("args").get(0).path("args").get(0).path("args").get(0).asText());
+ }
+
+ @Test
+ void rejectsOpParamInCtsNamespace() {
+ PlanBuilderSubImpl p = new PlanBuilderSubImpl();
+
+ IllegalArgumentException ex = assertThrows(
+ IllegalArgumentException.class,
+ () -> p.cts.collectionQuery(p.param("collection"))
+ );
+
+ assertEquals(
+ "Cannot pass op:param() to cts:collection-query(). Use cts:param() for cts namespace expressions.",
+ ex.getMessage()
+ );
+ }
+}