From 0f9b30cafaf8a527b3acc37690ebacc510ff3c9b Mon Sep 17 00:00:00 2001 From: Steve Biondi Date: Tue, 10 Feb 2026 12:43:48 -0800 Subject: [PATCH] MLE-23473 Implement fromDocs Add implementation of optic fromDocs to java client. Includes new context method to indicate current row for expressions. a --- .../client/expression/PlanBuilder.java | 84 ++++++ .../marklogic/client/expression/VecExpr.java | 5 + .../client/impl/ColumnBuilderImpl.java | 85 ++++++ .../client/impl/PlanBuilderImpl.java | 112 ++++++++ .../client/impl/PlanBuilderSubImpl.java | 1 + .../client/type/PlanColumnBuilder.java | 85 ++++++ .../client/type/PlanContextExprCall.java | 13 + .../client/test/rows/FromDocsTest.java | 250 ++++++++++++++++++ .../optic/locations/collections.properties | 1 + .../ml-data/optic/locations/new-york.json | 7 + .../optic/locations/permissions.properties | 1 + .../ml-data/optic/locations/portland.json | 7 + .../optic/locations/san-francisco.json | 7 + .../main/ml-data/optic/locations/seattle.json | 7 + .../src/main/ml-data/optic/widgets/alpha.json | 7 + .../src/main/ml-data/optic/widgets/beta.json | 7 + .../optic/widgets/collections.properties | 1 + .../optic/widgets/permissions.properties | 1 + 18 files changed, 681 insertions(+) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/ColumnBuilderImpl.java create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/type/PlanColumnBuilder.java create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/type/PlanContextExprCall.java create mode 100644 marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocsTest.java create mode 100644 test-app/src/main/ml-data/optic/locations/collections.properties create mode 100644 test-app/src/main/ml-data/optic/locations/new-york.json create mode 100644 test-app/src/main/ml-data/optic/locations/permissions.properties create mode 100644 test-app/src/main/ml-data/optic/locations/portland.json create mode 100644 test-app/src/main/ml-data/optic/locations/san-francisco.json create mode 100644 test-app/src/main/ml-data/optic/locations/seattle.json create mode 100644 test-app/src/main/ml-data/optic/widgets/alpha.json create mode 100644 test-app/src/main/ml-data/optic/widgets/beta.json create mode 100644 test-app/src/main/ml-data/optic/widgets/collections.properties create mode 100644 test-app/src/main/ml-data/optic/widgets/permissions.properties diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilder.java b/marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilder.java index 7ad4ed0c4..14c91d5e7 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilder.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilder.java @@ -247,6 +247,11 @@ protected PlanBuilder( */ public abstract PatchBuilder patchBuilder(XsStringVal contextPath, Map namespaces); /** + * Create column definitions which can be used in op:from-docs. Below functions are used to create column definitions. op:add-column, op:type, op:xpath, op:expr, op:nullable, op:default, op:dimension, op:coordinate-system, op:units, op:collation. + * @return a PlanColumnBuilder object + */ + public abstract PlanColumnBuilder columnBuilder(); + /** * This function creates a placeholder for a literal value in an expression or as the offset or max for a limit. The op:result function throws in an error if the binding parameter does not specify a literal value for the parameter. *

* Provides a client interface to the op:param server function. @@ -403,6 +408,58 @@ protected PlanBuilder( */ public abstract AccessPlan fromView(XsStringVal schema, XsStringVal view, XsStringVal qualifierName, PlanSystemColumn sysCols); /** + * This function dynamically maps semi-structured data (JSON/XML) into rows and columns without deploying a TDE template. It enables ad-hoc queries similar to Virtual Template Views but with additional flexibility, such as node output and advanced column customization. + * @param query Query to select documents for row generation. The query can be a cts:query or a string as a shortcut for a cts:word-query. + * @param contextPath XPath applied to each matched document; each result becomes a row. + * @param columnSpec The column definitions created by using op:column-builder. + * @return a AccessPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. + */ + public abstract AccessPlan fromDocs(String query, String contextPath, PlanColumnBuilder columnSpec); + /** + * This function dynamically maps semi-structured data (JSON/XML) into rows and columns without deploying a TDE template. It enables ad-hoc queries similar to Virtual Template Views but with additional flexibility, such as node output and advanced column customization. + * @param query Query to select documents for row generation. The query can be a cts:query or a string as a shortcut for a cts:word-query. + * @param contextPath XPath applied to each matched document; each result becomes a row. + * @param columnSpec The column definitions created by using op:column-builder. + * @param qualifier Specifies a name for qualifying the column names in place of the combination of the schema and view names. Use cases for the qualifier include self joins. Using an empty string removes all qualification from the column names. + * @return a AccessPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. + */ + public abstract AccessPlan fromDocs(String query, String contextPath, PlanColumnBuilder columnSpec, String qualifier); + /** + * This function dynamically maps semi-structured data (JSON/XML) into rows and columns without deploying a TDE template. It enables ad-hoc queries similar to Virtual Template Views but with additional flexibility, such as node output and advanced column customization. + * @param query Query to select documents for row generation. The query can be a cts:query or a string as a shortcut for a cts:word-query. + * @param contextPath XPath applied to each matched document; each result becomes a row. + * @param columnSpec The column definitions created by using op:column-builder. + * @param qualifier Specifies a name for qualifying the column names in place of the combination of the schema and view names. Use cases for the qualifier include self joins. Using an empty string removes all qualification from the column names. + * @return a AccessPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. + */ + public abstract AccessPlan fromDocs(CtsQueryExpr query, String contextPath, PlanColumnBuilder columnSpec, String qualifier); + /** + * This function dynamically maps semi-structured data (JSON/XML) into rows and columns without deploying a TDE template. It enables ad-hoc queries similar to Virtual Template Views but with additional flexibility, such as node output and advanced column customization. + * @param query Query to select documents for row generation. The query can be a cts:query or a string as a shortcut for a cts:word-query. + * @param contextPath XPath applied to each matched document; each result becomes a row. + * @param columnSpec The column definitions created by using op:column-builder. + * @param qualifier Specifies a name for qualifying the column names in place of the combination of the schema and view names. Use cases for the qualifier include self joins. Using an empty string removes all qualification from the column names. + * @param systemCol An optional named fragment id column returned by op:fragment-id-col. One use case for fragment ids is in joins with lexicons or document content. + * @return a AccessPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. + */ + public abstract AccessPlan fromDocs(String query, String contextPath, PlanColumnBuilder columnSpec, String qualifier, PlanSystemColumn systemCol); + /** + * This function dynamically maps semi-structured data (JSON/XML) into rows and columns without deploying a TDE template. It enables ad-hoc queries similar to Virtual Template Views but with additional flexibility, such as node output and advanced column customization. + * @param query Query to select documents for row generation. The query can be a cts:query or a string as a shortcut for a cts:word-query. + * @param contextPath XPath applied to each matched document; each result becomes a row. + * @param columnSpec The column definitions created by using op:column-builder. + * @param qualifier Specifies a name for qualifying the column names in place of the combination of the schema and view names. Use cases for the qualifier include self joins. Using an empty string removes all qualification from the column names. + * @param systemCol An optional named fragment id column returned by op:fragment-id-col. One use case for fragment ids is in joins with lexicons or document content. + * @param namespaces Namespaces prefix (key) and uri (value). + * @return a AccessPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. + */ + public abstract AccessPlan fromDocs(String query, String contextPath, PlanColumnBuilder columnSpec, String qualifier, PlanSystemColumn systemCol, PlanNamespaceBindingsSeq namespaces); + /** * This function factory returns a new function that takes a name parameter and returns a sem:iri, prepending the specified base URI onto the name. * @param base The base URI to be prepended to the name. * @return a PlanPrefixer object @@ -1130,6 +1187,11 @@ protected PlanBuilder( */ public abstract PlanCase when(ServerExpression condition, ServerExpression... value); /** + * This helper function returns the node from the current processing row. It is to be used in op:xpath, to reference the 'current item' instead of a doc column. + * @return a PlanContextExprCall object + */ + public abstract PlanContextExprCall context(); + /** * This function extracts a sequence of child nodes from a column with node values -- especially, the document nodes from a document join. The path is an XPath (specified as a string) to apply to each node to generate a sequence of nodes as an expression value. *

* Provides a client interface to the op:xpath server function. @@ -1167,6 +1229,24 @@ protected PlanBuilder( * @return a server expression with the node server data type */ public abstract ServerExpression xpath(PlanColumn column, ServerExpression path, PlanNamespaceBindingsSeq namespaceBindings); + /** + * This function extracts a sequence of child nodes from a server expression (such as op:context()) with node values. The path is an XPath (specified as a string) to apply to each node to generate a sequence of nodes as an expression value. + *

+ * Provides a client interface to the op:xpath server function. + * @param expression The server expression (such as op:context()) from which to extract the child nodes. + * @param path An XPath (specified as a string) to apply to each node. (of xs:string) + * @return a server expression with the node server data type + */ + public abstract ServerExpression xpath(ServerExpression expression, String path); + /** + * This function extracts a sequence of child nodes from a server expression (such as op:context()) with node values. The path is an XPath to apply to each node to generate a sequence of nodes as an expression value. + *

+ * Provides a client interface to the op:xpath server function. + * @param expression The server expression (such as op:context()) from which to extract the child nodes. + * @param path An XPath to apply to each node. (of xs:string) + * @return a server expression with the node server data type + */ +public abstract ServerExpression xpath(ServerExpression expression, ServerExpression path); /** * This function constructs a JSON document with the root content, which must be exactly one JSON object or array node. *

@@ -2048,6 +2128,7 @@ public interface ModifyPlan extends PreparePlan, PlanBuilderBase.ModifyPlanBase * @param start The column is the starting node of the traversal. The column can be named with a string or a column function such as op:col, op:view-col, or op:schema-col, or constructed from an expression with the op:as function. See {@link PlanBuilder#col(XsStringVal)} * @param end The column is the end node of the traversal. The column can be named with a string or a column function such as op:col, op:view-col, or op:schema-col, or constructed from an expression with the op:as function. See {@link PlanBuilder#col(XsStringVal)} * @return a ModifyPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public abstract ModifyPlan transitiveClosure(String start, String end); /** @@ -2055,6 +2136,7 @@ public interface ModifyPlan extends PreparePlan, PlanBuilderBase.ModifyPlanBase * @param start The column is the starting node of the traversal. The column can be named with a string or a column function such as op:col, op:view-col, or op:schema-col, or constructed from an expression with the op:as function. See {@link PlanBuilder#col(XsStringVal)} * @param end The column is the end node of the traversal. The column can be named with a string or a column function such as op:col, op:view-col, or op:schema-col, or constructed from an expression with the op:as function. See {@link PlanBuilder#col(XsStringVal)} * @return a ModifyPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public abstract ModifyPlan transitiveClosure(PlanExprCol start, PlanExprCol end); /** @@ -2063,6 +2145,7 @@ public interface ModifyPlan extends PreparePlan, PlanBuilderBase.ModifyPlanBase * @param end The column is the end node of the traversal. The column can be named with a string or a column function such as op:col, op:view-col, or op:schema-col, or constructed from an expression with the op:as function. See {@link PlanBuilder#col(XsStringVal)} * @param options This is either an array of strings or an object containing keys and values for the options to this operator. Options include: min-length This option is the minimum number of steps (edges) required in the path. It should be a non-negative integer, and the default is 1.max-length This option Maximum number of steps (edges) allowed in the path. It should be a non-negative integer, and the default is unlimited. * @return a ModifyPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public abstract ModifyPlan transitiveClosure(String start, String end, PlanTransitiveClosureOptions options); /** @@ -2071,6 +2154,7 @@ public interface ModifyPlan extends PreparePlan, PlanBuilderBase.ModifyPlanBase * @param end The column is the end node of the traversal. The column can be named with a string or a column function such as op:col, op:view-col, or op:schema-col, or constructed from an expression with the op:as function. See {@link PlanBuilder#col(XsStringVal)} * @param options This is either an array of strings or an object containing keys and values for the options to this operator. Options include: min-length This option is the minimum number of steps (edges) required in the path. It should be a non-negative integer, and the default is 1.max-length This option Maximum number of steps (edges) allowed in the path. It should be a non-negative integer, and the default is unlimited. * @return a ModifyPlan object + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public abstract ModifyPlan transitiveClosure(PlanExprCol start, PlanExprCol end, PlanTransitiveClosureOptions options); } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/expression/VecExpr.java b/marklogic-client-api/src/main/java/com/marklogic/client/expression/VecExpr.java index 87753efb5..3942c4117 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/expression/VecExpr.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/expression/VecExpr.java @@ -156,6 +156,7 @@ public interface VecExpr { * Provides a client interface to the vec:precision server function. * @param vector The input vector to reduce precision. Can be a vector or an empty sequence. (of vec:vector) * @return a server expression with the vec:vector server data type + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public ServerExpression precision(ServerExpression vector); /** @@ -165,6 +166,7 @@ public interface VecExpr { * @param vector The input vector to reduce precision. Can be a vector or an empty sequence. (of vec:vector) * @param precision The number of mantissa bits to preserve (9-32 inclusive). Default is 16. Higher values preserve more precision. If the value is outside the valid range, throw VEC-INVALIDPRECISION. (of xs:unsignedInt) * @return a server expression with the vec:vector server data type + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public ServerExpression precision(ServerExpression vector, ServerExpression precision); /** @@ -210,6 +212,7 @@ public interface VecExpr { * Provides a client interface to the vec:trunc server function. * @param vector The input vector to truncate. (of vec:vector) * @return a server expression with the vec:vector server data type + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public ServerExpression trunc(ServerExpression vector); /** @@ -219,6 +222,7 @@ public interface VecExpr { * @param vector The input vector to truncate. (of vec:vector) * @param n The numbers of decimal places to truncate to. The default is 0. Negative values cause that many digits to the left of the decimal point to be truncated. (of xs:int) * @return a server expression with the vec:vector server data type + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public ServerExpression trunc(ServerExpression vector, int n); /** @@ -228,6 +232,7 @@ public interface VecExpr { * @param vector The input vector to truncate. (of vec:vector) * @param n The numbers of decimal places to truncate to. The default is 0. Negative values cause that many digits to the left of the decimal point to be truncated. (of xs:int) * @return a server expression with the vec:vector server data type + * @since 8.1.0; requires MarkLogic 12.1 or higher. */ public ServerExpression trunc(ServerExpression vector, ServerExpression n); /** diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/ColumnBuilderImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/ColumnBuilderImpl.java new file mode 100644 index 000000000..2acb3e593 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/ColumnBuilderImpl.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.impl; + +import com.marklogic.client.type.PlanColumnBuilder; +import com.marklogic.client.type.ServerExpression; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("unchecked") +class ColumnBuilderImpl extends BaseTypeImpl.BaseCallImpl implements PlanColumnBuilder, BaseTypeImpl.BaseArgImpl { + + ColumnBuilderImpl() { + super("op", "suboperators", + new BaseTypeImpl.BaseArgImpl[]{ + new BaseTypeImpl.BaseCallImpl("op", "column-builder", new BaseTypeImpl.BaseArgImpl[]{}) + }); + } + + private ColumnBuilderImpl(List args) { + super("op", "suboperators", args.toArray(new BaseTypeImpl.BaseArgImpl[]{})); + } + + public PlanColumnBuilder addColumn(String name) { + return addArg("add-column", name); + } + + public PlanColumnBuilder xpath(String path) { + return addArg("xpath", path); + } + + public PlanColumnBuilder type(String type) { + return addArg("type", type); + } + + public PlanColumnBuilder nullable(boolean nullable) { + return addArg("nullable", new XsValueImpl.BooleanValImpl(nullable)); + } + + public PlanColumnBuilder expr(ServerExpression expression) { + return addArg("expr", expression); + } + + public PlanColumnBuilder defaultValue(String value) { + return addArg("default", value); + } + + public PlanColumnBuilder collation(String collation) { + return addArg("collation", collation); + } + + public PlanColumnBuilder dimension(int dimension) { + return addArg("dimension", dimension); + } + + public PlanColumnBuilder coordinateSystem(String coordinateSystem) { + return addArg("coordinate-system", coordinateSystem); + } + + private PlanColumnBuilder addArg(String functionName, Object... args) { + BaseTypeImpl.BaseArgImpl newArg = new BaseTypeImpl.BaseCallImpl( + "op", functionName, makeArgs(args) + ); + List newArgs = new ArrayList<>(); + newArgs.addAll(Arrays.asList(getArgsImpl())); + newArgs.add(newArg); + return new ColumnBuilderImpl(newArgs); + } + + private BaseTypeImpl.BaseArgImpl[] makeArgs(Object... args) { + List argList = new ArrayList<>(); + for (Object arg : args) { + if (arg instanceof BaseTypeImpl.BaseArgImpl) { + argList.add((BaseTypeImpl.BaseArgImpl) arg); + } else { + // Use Literal for plain values (strings, numbers, etc.) + argList.add(new BaseTypeImpl.Literal(arg)); + } + } + return argList.toArray(new BaseTypeImpl.BaseArgImpl[]{}); + } +} diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderImpl.java index 61e095d2d..4adb266d4 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderImpl.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderImpl.java @@ -184,6 +184,18 @@ public PlanExprColSeq colSeq(PlanExprCol... col) { } + @Override + public PlanColumnBuilder columnBuilder() { + return new ColumnBuilderImpl(); + } + + + @Override + public PlanContextExprCall context() { + return new ContextExprCallCallImpl("op", "context", new Object[]{ }); + } + + @Override public PlanAggregateCol count(String name) { return count((name == null) ? (PlanColumn) null : col(name)); @@ -508,6 +520,81 @@ public AccessPlan fromView(XsStringVal schema, XsStringVal view, XsStringVal qua } + @Override + public AccessPlan fromDocs(String query, String contextPath, PlanColumnBuilder columnSpec) { + if (query == null) { + throw new IllegalArgumentException("query parameter for fromDocs() cannot be null"); + } + if (contextPath == null) { + throw new IllegalArgumentException("contextPath parameter for fromDocs() cannot be null"); + } + if (columnSpec == null) { + throw new IllegalArgumentException("columnSpec parameter for fromDocs() cannot be null"); + } + return new PlanBuilderSubImpl.AccessPlanSubImpl("op", "from-docs", new Object[]{ cts.wordQuery(query), xs.string(contextPath), columnSpec }); + } + + + @Override + public AccessPlan fromDocs(String query, String contextPath, PlanColumnBuilder columnSpec, String qualifier) { + if (query == null) { + throw new IllegalArgumentException("query parameter for fromDocs() cannot be null"); + } + if (contextPath == null) { + throw new IllegalArgumentException("contextPath parameter for fromDocs() cannot be null"); + } + if (columnSpec == null) { + throw new IllegalArgumentException("columnSpec parameter for fromDocs() cannot be null"); + } + return new PlanBuilderSubImpl.AccessPlanSubImpl("op", "from-docs", new Object[]{ cts.wordQuery(query), xs.string(contextPath), columnSpec, (qualifier == null) ? null : xs.string(qualifier) }); + } + + + @Override + public AccessPlan fromDocs(CtsQueryExpr query, String contextPath, PlanColumnBuilder columnSpec, String qualifier) { + if (query == null) { + throw new IllegalArgumentException("query parameter for fromDocs() cannot be null"); + } + if (contextPath == null) { + throw new IllegalArgumentException("contextPath parameter for fromDocs() cannot be null"); + } + if (columnSpec == null) { + throw new IllegalArgumentException("columnSpec parameter for fromDocs() cannot be null"); + } + return new PlanBuilderSubImpl.AccessPlanSubImpl("op", "from-docs", new Object[]{ query, xs.string(contextPath), columnSpec, (qualifier == null) ? null : xs.string(qualifier) }); + } + + + @Override + public AccessPlan fromDocs(String query, String contextPath, PlanColumnBuilder columnSpec, String qualifier, PlanSystemColumn systemCol) { + if (query == null) { + throw new IllegalArgumentException("query parameter for fromDocs() cannot be null"); + } + if (contextPath == null) { + throw new IllegalArgumentException("contextPath parameter for fromDocs() cannot be null"); + } + if (columnSpec == null) { + throw new IllegalArgumentException("columnSpec parameter for fromDocs() cannot be null"); + } + return new PlanBuilderSubImpl.AccessPlanSubImpl("op", "from-docs", new Object[]{ cts.wordQuery(query), xs.string(contextPath), columnSpec, (qualifier == null) ? null : xs.string(qualifier), systemCol }); + } + + + @Override + public AccessPlan fromDocs(String query, String contextPath, PlanColumnBuilder columnSpec, String qualifier, PlanSystemColumn systemCol, PlanNamespaceBindingsSeq namespaces) { + if (query == null) { + throw new IllegalArgumentException("query parameter for fromDocs() cannot be null"); + } + if (contextPath == null) { + throw new IllegalArgumentException("contextPath parameter for fromDocs() cannot be null"); + } + if (columnSpec == null) { + throw new IllegalArgumentException("columnSpec parameter for fromDocs() cannot be null"); + } + return new PlanBuilderSubImpl.AccessPlanSubImpl("op", "from-docs", new Object[]{ cts.wordQuery(query), xs.string(contextPath), columnSpec, (qualifier == null) ? null : xs.string(qualifier), systemCol, namespaces }); + } + + @Override public ServerExpression ge(ServerExpression left, ServerExpression right) { if (left == null) { @@ -1285,6 +1372,24 @@ public ServerExpression xpath(PlanColumn column, ServerExpression path, PlanName } + @Override + public ServerExpression xpath(ServerExpression expression, String path) { + return xpath(expression, (path == null) ? (ServerExpression) null : xs.string(path)); + } + + + @Override + public ServerExpression xpath(ServerExpression expression, ServerExpression path) { + if (expression == null) { + throw new IllegalArgumentException("expression parameter for xpath() cannot be null"); + } + if (path == null) { + throw new IllegalArgumentException("path parameter for xpath() cannot be null"); + } + return new BaseTypeImpl.NodeSeqCallImpl("op", "xpath", new Object[]{ expression, path }); + } + + // external type implementations static class AggregateColSeqListImpl extends PlanSeqListImpl implements PlanAggregateColSeq { @@ -1371,6 +1476,13 @@ static class ConditionCallImpl extends PlanCallImpl implements PlanCondition { } + static class ContextExprCallCallImpl extends PlanCallImpl implements PlanContextExprCall { + ContextExprCallCallImpl(String fnPrefix, String fnName, Object[] fnArgs) { + super(fnPrefix, fnName, fnArgs); + } + } + + static class DocColsIdentifierSeqListImpl extends PlanSeqListImpl implements PlanDocColsIdentifierSeq { DocColsIdentifierSeqListImpl(Object[] items) { super(items); 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 3c11bbf38..2d6580458 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 @@ -1246,6 +1246,7 @@ static class AccessPlanSubImpl case "from-doc-uris": case "from-param": case "from-doc-descriptors": + case "from-docs": if (fnArgs.length < 1) { throw new IllegalArgumentException("accessor constructor without parameters: "+fnArgs.length); } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/type/PlanColumnBuilder.java b/marklogic-client-api/src/main/java/com/marklogic/client/type/PlanColumnBuilder.java new file mode 100644 index 000000000..fe39ec616 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/type/PlanColumnBuilder.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.type; + +// IMPORTANT: Do not edit. This file is generated. + +/** + * An instance of a column builder returned by the columnBuilder() method + * in a row pipeline. Used to create column definitions for op:from-docs. + */ +public interface PlanColumnBuilder extends ServerExpression { + + /** + * Add a column definition. + * + * @param name The name of the column + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder addColumn(String name); + + /** + * Set the XPath expression for the current column. + * + * @param path The XPath expression + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder xpath(String path); + + /** + * Set the data type for the current column. + * + * @param type The data type (e.g., "string", "integer", "decimal", "vector", "point") + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder type(String type); + + /** + * Specify whether the column can be null. + * + * @param nullable Whether the column can be null + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder nullable(boolean nullable); + + /** + * Set an expression to compute the column value. + * + * @param expression The expression to compute the value + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder expr(ServerExpression expression); + + /** + * Set a default value for the column. + * + * @param value The default value + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder defaultValue(String value); + + /** + * Set the collation for the column. + * + * @param collation The collation URI + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder collation(String collation); + + /** + * Set the dimension for a vector column. + * + * @param dimension The vector dimension + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder dimension(int dimension); + + /** + * Set the coordinate system for a geospatial column. + * + * @param coordinateSystem The coordinate system (e.g., "wgs84") + * @return a PlanColumnBuilder object + */ + PlanColumnBuilder coordinateSystem(String coordinateSystem); +} diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/type/PlanContextExprCall.java b/marklogic-client-api/src/main/java/com/marklogic/client/type/PlanContextExprCall.java new file mode 100644 index 000000000..128e30f38 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/type/PlanContextExprCall.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.type; + +// IMPORTANT: Do not edit. This file is generated. + +/** + * An instance of a context expression call returned by the context() method + * in a row pipeline. + */ +public interface PlanContextExprCall extends ServerExpression { +} diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocsTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocsTest.java new file mode 100644 index 000000000..0087d5f3e --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocsTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.test.rows; + +import com.marklogic.client.expression.PlanBuilder; +import com.marklogic.client.expression.PlanBuilder.Plan; +import com.marklogic.client.row.RowManager; +import com.marklogic.client.row.RowRecord; +import com.marklogic.client.test.AbstractClientTest; +import com.marklogic.client.test.Common; +import com.marklogic.client.test.junit5.RequiresML12; +import com.marklogic.client.type.PlanColumnBuilder; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the new fromDocs ModifyPlan method that dynamically maps semi-structured + * data (JSON/XML) into rows and columns without deploying a TDE template. + */ +@ExtendWith(RequiresML12.class) +public class FromDocsTest extends AbstractClientTest { + + protected RowManager rowManager; + protected PlanBuilder op; + + @BeforeEach + public void setup() { + Common.client = Common.newClientBuilder().withUsername("rest-reader").build(); + rowManager = Common.client.newRowManager(); + op = rowManager.newPlanBuilder(); + } + + @Test + public void fromDocsBasic() { + PlanColumnBuilder columnSpec = op.columnBuilder() + .addColumn("lastName").xpath("./lastName").type("string") + .addColumn("firstName").xpath("./firstName").type("string") + .addColumn("dob").xpath("./dob").type("string") + .addColumn("instrument").xpath("./instrument").type("string"); + + PlanBuilder.AccessPlan plan = op.fromDocs( + op.cts.wordQuery("Coltrane"), + "/musician", + columnSpec, + "MusicianView" + ); + + List rows = resultRows(plan); + assertEquals(1, rows.size()); + + RowRecord row = rows.get(0); + assertEquals("Coltrane", row.getString("MusicianView.lastName")); + assertEquals("John", row.getString("MusicianView.firstName")); + assertEquals("1926-09-23", row.getString("MusicianView.dob")); + assertEquals("saxophone", row.getString("MusicianView.instrument")); + } + + @Test + public void fromDocsWithDefault() { + PlanColumnBuilder columnSpec = op.columnBuilder() + .addColumn("lastName").xpath("./lastName").type("string") + .collation("http://marklogic.com/collation/") + .addColumn("firstName").xpath("./firstName").type("string") + .collation("http://marklogic.com/collation/") + .addColumn("birthDate").xpath("./birthDate").type("string") + .defaultValue("Unknown") + .addColumn("instrument").xpath("./instrument").type("string") + .addColumn("genre").xpath("./genre").type("string") + .defaultValue("Jazz"); + + PlanBuilder.AccessPlan plan = op.fromDocs( + op.cts.wordQuery("Coltrane"), + "/musician", + columnSpec, + "MusicianView" + ); + + List rows = resultRows(plan); + assertEquals(1, rows.size()); + + RowRecord row = rows.get(0); + assertEquals("Coltrane", row.getString("MusicianView.lastName")); + assertEquals("John", row.getString("MusicianView.firstName")); + assertEquals("Unknown", row.getString("MusicianView.birthDate")); + assertEquals("saxophone", row.getString("MusicianView.instrument")); + assertEquals("Jazz", row.getString("MusicianView.genre")); + } + + @Test + public void fromDocsWithContextAndXpath() { + PlanColumnBuilder columnSpec = op.columnBuilder() + .addColumn("name").xpath("./name").type("string") + .addColumn("quantity").xpath("./quantity").type("integer") + .addColumn("price").xpath("./price").type("decimal") + .addColumn("totalCost") + .nullable(true) + .expr( + op.multiply( + op.xs.decimal(op.xpath(op.context(), op.xs.string("./price"))), + op.xs.integer(op.xpath(op.context(), op.xs.string("./quantity"))) + ) + ) + .type("decimal"); + + PlanBuilder.ModifyPlan plan = op.fromDocs( + op.cts.wordQuery("Widget"), + "/product", + columnSpec, + "ProductView" + ).orderBy(op.viewCol("ProductView", "name")); + + List rows = resultRows(plan); + assertEquals(2, rows.size()); + + RowRecord firstRow = rows.get(0); + assertEquals("Widget Alpha", firstRow.getString("ProductView.name")); + assertEquals(25.50, firstRow.getDouble("ProductView.price"), 0.01); + assertEquals(100, firstRow.getInt("ProductView.quantity")); + assertEquals(2550, firstRow.getDouble("ProductView.totalCost"), 0.01); + + RowRecord secondRow = rows.get(1); + assertEquals("Widget Beta", secondRow.getString("ProductView.name")); + assertEquals(42.99, secondRow.getDouble("ProductView.price"), 0.01); + assertEquals(250, secondRow.getInt("ProductView.quantity")); + assertEquals(10747.5, secondRow.getDouble("ProductView.totalCost"), 0.01); + } + + @Test + public void fromDocsWithGeospatialQuery() { + PlanColumnBuilder columnSpec = op.columnBuilder() + .addColumn("city").xpath("./city").type("string") + .addColumn("location-wgs84").xpath("./latLong").type("point") + .coordinateSystem("wgs84") + .addColumn("description").xpath("./description").type("string"); + + // find cities within 650 miles of Portland, OR (45.52, -122.68) + // use existing geospatial element index on latLong property, which is defined as a point with wgs84 coordinate system + PlanBuilder.ModifyPlan plan = op.fromDocs( + op.cts.collectionQuery("/optic/locations"), + "/location", + columnSpec, + "LocationView" + ).where( + op.cts.jsonPropertyGeospatialQuery( + "latLong", + op.cts.circle(650, op.cts.point(45.52, -122.68)), + "coordinate-system=wgs84" + ) + ); + + List rows = resultRows(plan); + + assertEquals(3, rows.size()); + + Set cities = rows.stream() + .map(row -> row.getString("LocationView.city")) + .collect(Collectors.toSet()); + + assertTrue(cities.contains("Portland")); + assertTrue(cities.contains("Seattle")); + assertTrue(cities.contains("San Francisco")); + assertFalse(cities.contains("New York")); + } + + @Test + public void fromDocsWithVectorAndDimension() { + PlanColumnBuilder columnSpec = op.columnBuilder() + .addColumn("name").xpath("./name").type("string") + .addColumn("summary").xpath("./summary").type("string") + .addColumn("embedding").xpath("vec:vector(./embedding)") + .type("vector") + .dimension(3) + .addColumn("cosineDistance") + .nullable(true) + .expr( + op.vec.cosineDistance( + op.vec.vector(op.xs.doubleSeq(1, 2, 3)), + op.vec.vector(op.xpath(op.context(), op.xs.string("./embedding"))) + ) + ) + .type("double") + .addColumn("cosine") + .nullable(true) + .expr( + op.vec.cosine( + op.vec.vector(op.xs.doubleSeq(1, 2, 3)), + op.vec.vector(op.xpath(op.context(), op.xs.string("./embedding"))) + ) + ) + .type("double") + .addColumn("euclideanDistance") + .nullable(true) + .expr( + op.math.trunc( + op.vec.euclideanDistance( + op.vec.vector(op.xs.doubleSeq(1, 2, 3)), + op.vec.vector(op.xpath(op.context(), op.xs.string("./embedding"))) + ), + 4 + ) + ) + .type("double"); + + PlanBuilder.ModifyPlan plan = op.fromDocs( + op.cts.wordQuery("*"), + "/person", + columnSpec, + "PersonView" + ).where( + op.lt( + op.vec.cosineDistance( + op.vec.vector(op.xs.doubleSeq(1, 2, 3)), + op.viewCol("PersonView", "embedding") + ), + op.xs.doubleVal(0.1) + ) + ).orderBy(op.viewCol("PersonView", "euclideanDistance")); + + List rows = resultRows(plan); + assertEquals(1, rows.size()); + + // Alice should return. + assertEquals(0.3741, rows.get(0).getDouble("PersonView.euclideanDistance"), 0.0001); + assertEquals(1, rows.get(0).getDouble("PersonView.cosine"), 0.0001); + assertEquals(0, rows.get(0).getDouble("PersonView.cosineDistance"), 0.0001); + + List names = rows.stream() + .map(row -> row.getString("PersonView.name")) + .toList(); + + assertTrue(names.contains("Alice")); + assertFalse(names.contains("Bob")); + } + + /** + * Convenience method for executing a plan and getting the rows back as a list. + */ + protected final List resultRows(Plan plan) { + return rowManager.resultRows(plan).stream().toList(); + } +} diff --git a/test-app/src/main/ml-data/optic/locations/collections.properties b/test-app/src/main/ml-data/optic/locations/collections.properties new file mode 100644 index 000000000..fa7e1d999 --- /dev/null +++ b/test-app/src/main/ml-data/optic/locations/collections.properties @@ -0,0 +1 @@ +*=/optic/locations,test-data diff --git a/test-app/src/main/ml-data/optic/locations/new-york.json b/test-app/src/main/ml-data/optic/locations/new-york.json new file mode 100644 index 000000000..6d7134d69 --- /dev/null +++ b/test-app/src/main/ml-data/optic/locations/new-york.json @@ -0,0 +1,7 @@ +{ + "location": { + "city": "New York", + "latLong": "40.71, -74.01", + "description": "The Big Apple" + } +} diff --git a/test-app/src/main/ml-data/optic/locations/permissions.properties b/test-app/src/main/ml-data/optic/locations/permissions.properties new file mode 100644 index 000000000..c97785496 --- /dev/null +++ b/test-app/src/main/ml-data/optic/locations/permissions.properties @@ -0,0 +1 @@ +*=rest-reader,read,rest-writer,update diff --git a/test-app/src/main/ml-data/optic/locations/portland.json b/test-app/src/main/ml-data/optic/locations/portland.json new file mode 100644 index 000000000..48df20d8b --- /dev/null +++ b/test-app/src/main/ml-data/optic/locations/portland.json @@ -0,0 +1,7 @@ +{ + "location": { + "city": "Portland", + "latLong": "45.52, -122.68", + "description": "City of Roses" + } +} diff --git a/test-app/src/main/ml-data/optic/locations/san-francisco.json b/test-app/src/main/ml-data/optic/locations/san-francisco.json new file mode 100644 index 000000000..5ebfc0361 --- /dev/null +++ b/test-app/src/main/ml-data/optic/locations/san-francisco.json @@ -0,0 +1,7 @@ +{ + "location": { + "city": "San Francisco", + "latLong": "37.77, -122.42", + "description": "City by the Bay" + } +} diff --git a/test-app/src/main/ml-data/optic/locations/seattle.json b/test-app/src/main/ml-data/optic/locations/seattle.json new file mode 100644 index 000000000..404773972 --- /dev/null +++ b/test-app/src/main/ml-data/optic/locations/seattle.json @@ -0,0 +1,7 @@ +{ + "location": { + "city": "Seattle", + "latLong": "47.61, -122.33", + "description": "Emerald City" + } +} diff --git a/test-app/src/main/ml-data/optic/widgets/alpha.json b/test-app/src/main/ml-data/optic/widgets/alpha.json new file mode 100644 index 000000000..d15651f9e --- /dev/null +++ b/test-app/src/main/ml-data/optic/widgets/alpha.json @@ -0,0 +1,7 @@ +{ + "product": { + "name": "Widget Alpha", + "price": 25.50, + "quantity": 100 + } +} diff --git a/test-app/src/main/ml-data/optic/widgets/beta.json b/test-app/src/main/ml-data/optic/widgets/beta.json new file mode 100644 index 000000000..1da0fb694 --- /dev/null +++ b/test-app/src/main/ml-data/optic/widgets/beta.json @@ -0,0 +1,7 @@ +{ + "product": { + "name": "Widget Beta", + "price": 42.99, + "quantity": 250 + } +} diff --git a/test-app/src/main/ml-data/optic/widgets/collections.properties b/test-app/src/main/ml-data/optic/widgets/collections.properties new file mode 100644 index 000000000..5676dbf5a --- /dev/null +++ b/test-app/src/main/ml-data/optic/widgets/collections.properties @@ -0,0 +1 @@ +*=/optic/widgets,test-data diff --git a/test-app/src/main/ml-data/optic/widgets/permissions.properties b/test-app/src/main/ml-data/optic/widgets/permissions.properties new file mode 100644 index 000000000..c97785496 --- /dev/null +++ b/test-app/src/main/ml-data/optic/widgets/permissions.properties @@ -0,0 +1 @@ +*=rest-reader,read,rest-writer,update