Skip to content

Commit 2e84f3b

Browse files
committed
Improvements
1 parent 92a5885 commit 2e84f3b

5 files changed

Lines changed: 639 additions & 16 deletions

File tree

package-lock.json

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/grpc/client.node.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -238,34 +238,44 @@ export class GrpcFlightClient {
238238
/**
239239
* Substitutes parameters into a SQL query using positional placeholders ($1, $2, etc.)
240240
* This is a client-side substitution - the server will receive the final SQL.
241+
* Parameters are processed in reverse order to avoid $1 matching the '1' in $10.
241242
*/
242243
private substitutePositionalParameters(
243244
queryText: string,
244245
parameters: any[],
245246
): string {
246247
let result = queryText;
247-
for (let i = 0; i < parameters.length; i++) {
248-
const placeholder = `$${i + 1}`;
248+
// Process parameters in reverse order (highest to lowest) to avoid
249+
// $1 replacing the '1' in $10, $11, etc.
250+
for (let i = parameters.length - 1; i >= 0; i--) {
249251
const value = this.formatParameterValue(parameters[i]);
250-
// Replace all occurrences of this placeholder
251-
result = result.split(placeholder).join(value);
252+
// Use regex to match $N not followed by another digit
253+
const regex = new RegExp(`\\$${i + 1}(?![0-9])`, 'g');
254+
result = result.replace(regex, value);
252255
}
253256
return result;
254257
}
255258

256259
/**
257-
* Substitutes named parameters into a SQL query (:name style)
260+
* Substitutes named parameters into a SQL query ($name style)
258261
* This is a client-side substitution - the server will receive the final SQL.
262+
* Uses PostgreSQL-style $param_name syntax for consistency with positional $1, $2 placeholders.
259263
*/
260264
private substituteNamedParameters(
261265
queryText: string,
262266
parameters: Record<string, any>,
263267
): string {
264268
let result = queryText;
265-
for (const [name, value] of Object.entries(parameters)) {
266-
// Match :name but not ::type (PostgreSQL cast syntax)
267-
// Use regex to match :name followed by non-alphanumeric or end of string
268-
const regex = new RegExp(`:${name}(?![a-zA-Z0-9_])`, 'g');
269+
// Sort parameter names by length (descending) to avoid partial matches
270+
// e.g., $name should be replaced before $n
271+
const sortedNames = Object.keys(parameters).sort(
272+
(a, b) => b.length - a.length,
273+
);
274+
for (const name of sortedNames) {
275+
const value = parameters[name];
276+
// Match $name followed by non-alphanumeric or end of string
277+
// This ensures $name_extra won't match when looking for $name
278+
const regex = new RegExp(`\\$${name}(?![a-zA-Z0-9_])`, 'g');
269279
result = result.replace(regex, this.formatParameterValue(value));
270280
}
271281
return result;
@@ -337,7 +347,7 @@ export class GrpcFlightClient {
337347
// Positional parameters: $1, $2, etc.
338348
finalQuery = this.substitutePositionalParameters(queryText, parameters);
339349
} else {
340-
// Named parameters: :name, :value, etc.
350+
// Named parameters: $name, $param, etc.
341351
finalQuery = this.substituteNamedParameters(queryText, parameters);
342352
}
343353
}

0 commit comments

Comments
 (0)