Skip to content
7 changes: 7 additions & 0 deletions lib/plan-builder-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ function castArg(arg, funcName, paramName, argPos, paramTypes) {
if(arg._ns === 'vec'){
return arg;
}
// cts.param() is a valid placeholder wherever a cts.query is accepted (e.g. as a direct
// sub-query argument to cts.orQuery, cts.andQuery, cts.notQuery, etc.).
// Serialisation already works: exportObject converts _ns/_fn/_args -> ns/fn/args.
if (arg._ns === 'cts' && arg._fn === 'param' &&
paramTypes.includes(types.CtsQuery)) {
return arg;
}
throw new Error(
`${argLabel(funcName, paramName, argPos)} must have type ${typeLabel(paramTypes)}`
);
Expand Down
10 changes: 5 additions & 5 deletions lib/plan-builder-generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -7420,12 +7420,12 @@ class PlanPlan extends types.ServerType {
* @method planBuilder.Plan#bindParam
* @since 2.1.1
* @param { PlanParamName } [param] -
* @param { PlanParamBinding } [literal] -
* @param { PlanParamBinding|CtsQuery } [literal] -
* @returns { planBuilder.Plan }
*/
bindParam(...args) {
const namer = bldrbase.getNamer(args, 'param');
const paramdefs = [['param', [PlanParam, types.XsString], true, false], ['literal', [PlanParamBinding], true, false]];
const paramdefs = [['param', [PlanParam, types.XsString], true, false], ['literal', [PlanParamBinding, types.CtsQuery], true, false]];
const checkedArgs = (namer !== null) ?
bldrbase.makeNamedArgs(namer, 'PlanPlan.bindParam', 2, new Set(['param', 'literal']), paramdefs, args) :
bldrbase.makePositionalArgs('PlanPlan.bindParam', 2, false, paramdefs, args);
Expand Down Expand Up @@ -8114,7 +8114,7 @@ union(...args) {
* @returns { planBuilder.ModifyPlan }
*/
where(...args) {
const paramdef = ['condition', [types.XsBoolean, PlanColumn, types.CtsQuery, types.SemStore, PlanCondition], true, false];
const paramdef = ['condition', [types.XsBoolean, PlanColumn, types.CtsQuery, types.SemStore, PlanCondition, PlanParam], true, false];
const checkedArgs = bldrbase.makeSingleArgs('PlanModifyPlan.where', 1, paramdef, args);
return new PlanModifyPlan(this, 'op', 'where', checkedArgs);
}
Expand Down Expand Up @@ -8998,7 +8998,7 @@ fromSQL(...args) {
*/
fromSearchDocs(...args) {
const namer = bldrbase.getNamer(args, 'query');
const paramdefs = [['query', [types.XsString, types.CtsQuery], true, false], ['qualifierName', [types.XsString], false, false], ['option', [PlanSearchOption], false, false]];
const paramdefs = [['query', [types.XsString, types.CtsQuery, PlanParam], true, false], ['qualifierName', [types.XsString], false, false], ['option', [PlanSearchOption], false, false]];
const checkedArgs = (namer !== null) ?
bldrbase.makeNamedArgs(namer, 'PlanBuilder.fromSearchDocs', 1, new Set(['query', 'qualifierName', 'option']), paramdefs, args) :
bldrbase.makePositionalArgs('PlanBuilder.fromSearchDocs', 1, false, paramdefs, args);
Expand All @@ -9017,7 +9017,7 @@ fromSearchDocs(...args) {
*/
fromSearch(...args) {
const namer = bldrbase.getNamer(args, 'query');
const paramdefs = [['query', [types.XsString, types.CtsQuery], true, false], ['columns', [PlanExprCol, PlanColumn, types.XsString], false, true], ['qualifierName', [types.XsString], false, false], ['option', [PlanSearchOption], false, false]];
const paramdefs = [['query', [types.XsString, types.CtsQuery, PlanParam], true, false], ['columns', [PlanExprCol, PlanColumn, types.XsString], false, true], ['qualifierName', [types.XsString], false, false], ['option', [PlanSearchOption], false, false]];
const checkedArgs = (namer !== null) ?
bldrbase.makeNamedArgs(namer, 'PlanBuilder.fromSearch', 1, new Set(['query', 'columns', 'qualifierName', 'option']), paramdefs, args) :
bldrbase.makePositionalArgs('PlanBuilder.fromSearch', 1, false, paramdefs, args);
Expand Down
4 changes: 2 additions & 2 deletions lib/requester.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';
const createAuthInitializer = require('./www-authenticate-patched/www-authenticate');
Expand Down Expand Up @@ -400,7 +400,7 @@ function multipartRequester(request) {

form.setBoundary(mlutil.multipartBoundary);
form.append('query', query, {contentType: 'application/json', filename: 'fromParam-AST.js'});
form.append(key, JSON.stringify(binding), {contentType: 'application/json', filename: 'data.json'});
form.append(key, JSON.stringify(binding), {contentType: 'application/json', filename: 'data.json'});

if(attachments && attachments instanceof Array && attachments.length) {
for (let i = 0; i < attachments.length; i++) {
Expand Down
82 changes: 77 additions & 5 deletions lib/rows.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';

Expand All @@ -8,6 +8,7 @@ const requester = require('./requester.js'),
mlutil = require('./mlutil.js'),
Operation = require('./operation.js'),
planBuilder = require('./plan-builder.js');
const bldrbase = require('./plan-builder-base.js');
const stream = require('stream');
const bigInt = require('big-integer');

Expand Down Expand Up @@ -63,6 +64,42 @@ function Rows(client) {
* binding.
* @returns {Promise} A Promise.
*/
// Walk an exported plan JSON node and replace every cts:param / op:param
// reference whose name appears in the replacements map with the mapped value.
// A single traversal handles all bindings, avoiding O(planSize × bindings) cost.
function substitutePlanParams(node, replacements) {
if (node === null || typeof node !== 'object') { return node; }
if (Array.isArray(node)) {
return node.map(item => substitutePlanParams(item, replacements));
}
if ((node.ns === 'cts' || node.ns === 'op') && node.fn === 'param' &&
Array.isArray(node.args) && Object.prototype.hasOwnProperty.call(replacements, node.args[0])) {
return replacements[node.args[0]];
}
const result = {};
for (const key of Object.keys(node)) {
result[key] = substitutePlanParams(node[key], replacements);
}
return result;
}

// Partitions bindings into plan-builder node entries (exported {ns,fn,args[]} shapes)
// collected into planNodeMap, and the remainder for normal query-param binding.
function partitionExportedBindings(bindings) {
const planNodeMap = {};
const remaining = {};
for (const [k, v] of Object.entries(bindings)) {
const exported = (v !== null && typeof v === 'object') ? bldrbase.exportArg(v) : null;
if (exported && typeof exported === 'object' && typeof exported.ns === 'string' &&
typeof exported.fn === 'string' && Array.isArray(exported.args)) {
planNodeMap[k] = exported;
} else {
remaining[k] = v;
}
}
return { planNodeMap, remaining };
}

Rows.prototype.query = function queryRows() {
const args = mlutil.asArray.apply(null, arguments);

Expand Down Expand Up @@ -149,10 +186,39 @@ function queryRowsOperationImpl(self, builtPlan, streamType, options, bindingArg
}
}

// Substitute plan-builder bindings (CTS queries etc.) from both
// options.bindings and bindingArg into the exported plan JSON before any
// network serialisation. This lets callers pass CtsQuery objects via either
// the second-arg (options.bindings) or the third-arg (bindingArg) form.
let substitutedOptionBindings = options.bindings || null;
if (builtPlan instanceof planBuilder.Plan && (substitutedOptionBindings || bindingArg)) {
Comment thread
RitaChen609 marked this conversation as resolved.
const replacements = {};

if (substitutedOptionBindings) {
const { planNodeMap, remaining } = partitionExportedBindings(substitutedOptionBindings);
if (Object.keys(planNodeMap).length > 0) {
Object.assign(replacements, planNodeMap);
substitutedOptionBindings = Object.keys(remaining).length > 0 ? remaining : null;
}
}

if (bindingArg) {
const { planNodeMap, remaining } = partitionExportedBindings(bindingArg);
if (Object.keys(planNodeMap).length > 0) {
Object.assign(replacements, planNodeMap);
bindingArg = Object.keys(remaining).length > 0 ? remaining : null;
}
}

if (Object.keys(replacements).length > 0) {
builtPlan = JSON.stringify(substitutePlanParams(builtPlan.export(), replacements));
}
}

const contentTypeHeader = graphqlQuery ? 'application/graphql' : queryContentType(builtPlan, options.queryType);
let endpoint = createRowsEndpoint(options, graphqlQuery, structure);
if (options.bindings) {
endpoint += mlutil.makeBindingsParams(options.bindings, sep);
if (substitutedOptionBindings) {
endpoint += mlutil.makeBindingsParams(substitutedOptionBindings, sep);
if (sep === '?') { sep = '&'; }
}

Expand All @@ -163,7 +229,7 @@ function queryRowsOperationImpl(self, builtPlan, streamType, options, bindingArg
const bindingKey = keys[0];
const attachments = keys[1];
const metadata = keys[2];
const query = JSON.stringify(builtPlan.export());
const query = typeof builtPlan === 'string' ? builtPlan : JSON.stringify(builtPlan.export());
const multipartBoundary = mlutil.multipartBoundary;
const endpoint = createRowsEndpoint(options,graphqlQuery, structure);
const requestOptions = mlutil.newRequestOptions(connectionParams, endpoint, 'POST');
Expand All @@ -181,8 +247,14 @@ function queryRowsOperationImpl(self, builtPlan, streamType, options, bindingArg
'Content-Type': 'multipart/form-data; boundary=' + multipartBoundary,
};

const rawBindingVal = bindingArg[bindingKey];
const parsedBindingVal = (typeof rawBindingVal === 'string' &&
rawBindingVal.length > 0 &&
(rawBindingVal[0] === '{' || rawBindingVal[0] === '['))
? JSON.parse(rawBindingVal)
: rawBindingVal;
operation.bindingParam = {
[bindingKey]: (typeof bindingArg[bindingKey] === 'string')?JSON.parse(bindingArg[bindingKey]):bindingArg[bindingKey],
[bindingKey]: parsedBindingVal,
query: query,
key: bindingKey,
attachments: bindingArg[attachments],
Expand Down
Loading
Loading