sqlstack 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/binders/sqlBinder.d.ts.map +1 -1
- package/dist/cjs/binders/sqlBinder.js +671 -13
- package/dist/cjs/binders/sqlBinder.js.map +1 -1
- package/dist/cjs/core/errors.d.ts +5 -0
- package/dist/cjs/core/errors.d.ts.map +1 -1
- package/dist/cjs/core/errors.js +10 -1
- package/dist/cjs/core/errors.js.map +1 -1
- package/dist/cjs/decorators/query.d.ts.map +1 -1
- package/dist/cjs/decorators/query.js +8 -1
- package/dist/cjs/decorators/query.js.map +1 -1
- package/dist/esm/binders/sqlBinder.js +671 -13
- package/dist/esm/binders/sqlBinder.js.map +1 -1
- package/dist/esm/core/errors.js +8 -0
- package/dist/esm/core/errors.js.map +1 -1
- package/dist/esm/decorators/query.js +9 -2
- package/dist/esm/decorators/query.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +297 -33
|
@@ -151,34 +151,692 @@ function createBinder(dialect) {
|
|
|
151
151
|
// Extract column list (between the parentheses)
|
|
152
152
|
const columnList = sql.slice(columnStart, j).trim();
|
|
153
153
|
const updateEnd = j + 1; // Include closing ')'
|
|
154
|
-
// Parse column
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
// Parse column list (split by comma, respecting quotes and parentheses)
|
|
155
|
+
const items = [];
|
|
156
|
+
let current = '';
|
|
157
|
+
let inSQ = false; // single quote string
|
|
158
|
+
let inDQ = false; // double quote identifier
|
|
159
|
+
let itemParenDepth = 0;
|
|
160
|
+
for (let k = 0; k < columnList.length; k++) {
|
|
161
|
+
const ch = columnList[k];
|
|
162
|
+
const next = k + 1 < columnList.length ? columnList[k + 1] : '';
|
|
163
|
+
if (inSQ) {
|
|
164
|
+
current += ch;
|
|
165
|
+
if (ch === "'" && next === "'") {
|
|
166
|
+
current += next;
|
|
167
|
+
k++;
|
|
168
|
+
}
|
|
169
|
+
else if (ch === "'") {
|
|
170
|
+
inSQ = false;
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (inDQ) {
|
|
175
|
+
current += ch;
|
|
176
|
+
if (ch === '"' && next === '"') {
|
|
177
|
+
current += next;
|
|
178
|
+
k++;
|
|
179
|
+
}
|
|
180
|
+
else if (ch === '"') {
|
|
181
|
+
inDQ = false;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ch === "'") {
|
|
186
|
+
current += ch;
|
|
187
|
+
inSQ = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === '"') {
|
|
191
|
+
current += ch;
|
|
192
|
+
inDQ = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (ch === '(') {
|
|
196
|
+
current += ch;
|
|
197
|
+
itemParenDepth++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (ch === ')') {
|
|
201
|
+
current += ch;
|
|
202
|
+
itemParenDepth--;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (ch === ',' && itemParenDepth === 0) {
|
|
206
|
+
const trimmed = current.trim();
|
|
207
|
+
if (trimmed.length > 0) {
|
|
208
|
+
items.push(trimmed);
|
|
209
|
+
}
|
|
210
|
+
current = '';
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
current += ch;
|
|
214
|
+
}
|
|
215
|
+
// Add the last item
|
|
216
|
+
const trimmed = current.trim();
|
|
217
|
+
if (trimmed.length > 0) {
|
|
218
|
+
items.push(trimmed);
|
|
219
|
+
}
|
|
220
|
+
if (items.length === 0) {
|
|
160
221
|
throw new Error(':update() requires at least one column name');
|
|
161
222
|
}
|
|
162
|
-
// Build SET clause with
|
|
223
|
+
// Build SET clause with literal expressions and parameterized columns
|
|
163
224
|
const obj = args;
|
|
164
225
|
const setParts = [];
|
|
165
226
|
const updateParams = [];
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
227
|
+
let hasParameterized = false;
|
|
228
|
+
for (const item of items) {
|
|
229
|
+
const trimmed = item.trim();
|
|
230
|
+
// Check if this is a literal expression (contains = not inside quotes)
|
|
231
|
+
let isLiteral = false;
|
|
232
|
+
let eqIndex = -1;
|
|
233
|
+
let inString = false;
|
|
234
|
+
let stringChar = '';
|
|
235
|
+
for (let k = 0; k < trimmed.length; k++) {
|
|
236
|
+
const ch = trimmed[k];
|
|
237
|
+
const next = k + 1 < trimmed.length ? trimmed[k + 1] : '';
|
|
238
|
+
if (!inString && (ch === "'" || ch === '"')) {
|
|
239
|
+
inString = true;
|
|
240
|
+
stringChar = ch;
|
|
241
|
+
}
|
|
242
|
+
else if (inString) {
|
|
243
|
+
if (ch === stringChar) {
|
|
244
|
+
if (next === stringChar) {
|
|
245
|
+
k++; // Skip escaped quote
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
inString = false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else if (ch === '=') {
|
|
253
|
+
eqIndex = k;
|
|
254
|
+
isLiteral = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (isLiteral && eqIndex >= 0) {
|
|
259
|
+
// Literal SQL expression - include as-is
|
|
260
|
+
setParts.push(trimmed);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Parameterized column - check if defined in args
|
|
264
|
+
hasParameterized = true;
|
|
265
|
+
if (Object.prototype.hasOwnProperty.call(obj, trimmed) && obj[trimmed] !== undefined) {
|
|
266
|
+
setParts.push(`${trimmed} = ${placeholder(pgIndex)}`);
|
|
267
|
+
updateParams.push(obj[trimmed]);
|
|
268
|
+
}
|
|
171
269
|
}
|
|
172
270
|
}
|
|
173
|
-
if (
|
|
271
|
+
// Only throw error if all parameterized columns are undefined (literals are always included)
|
|
272
|
+
if (hasParameterized && setParts.length === 0) {
|
|
174
273
|
throw new Error('All columns in :update() are undefined');
|
|
175
274
|
}
|
|
275
|
+
if (setParts.length === 0) {
|
|
276
|
+
throw new Error(':update() requires at least one column name');
|
|
277
|
+
}
|
|
176
278
|
// Replace :update(...) with SET clause
|
|
177
279
|
out.push('SET ', setParts.join(', '));
|
|
178
280
|
params.push(...updateParams);
|
|
179
281
|
i = updateEnd;
|
|
180
282
|
continue;
|
|
181
283
|
}
|
|
284
|
+
// Check for :insert(...) syntax (case-insensitive)
|
|
285
|
+
const insertMatch = /^:insert\s*\(/i.exec(sql.slice(i));
|
|
286
|
+
if (insertMatch) {
|
|
287
|
+
if (isArrayArgs) {
|
|
288
|
+
throw new Error(':insert() syntax only works with named parameters');
|
|
289
|
+
}
|
|
290
|
+
// Parse the column list inside parentheses
|
|
291
|
+
let j = i + insertMatch[0].length; // Skip ':insert('
|
|
292
|
+
const columnStart = j;
|
|
293
|
+
let parenDepth = 1;
|
|
294
|
+
let inString = false;
|
|
295
|
+
let stringChar = '';
|
|
296
|
+
// Find matching closing parenthesis
|
|
297
|
+
while (j < len && parenDepth > 0) {
|
|
298
|
+
const c = sql[j];
|
|
299
|
+
const cNext = j + 1 < len ? sql[j + 1] : '';
|
|
300
|
+
if (!inString) {
|
|
301
|
+
if (c === "'" || c === '"') {
|
|
302
|
+
inString = true;
|
|
303
|
+
stringChar = c;
|
|
304
|
+
}
|
|
305
|
+
else if (c === '(') {
|
|
306
|
+
parenDepth++;
|
|
307
|
+
}
|
|
308
|
+
else if (c === ')') {
|
|
309
|
+
parenDepth--;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
if (c === stringChar) {
|
|
314
|
+
// Check for escaped quote
|
|
315
|
+
if (cNext === stringChar) {
|
|
316
|
+
j++; // Skip escaped quote
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
inString = false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (parenDepth > 0)
|
|
324
|
+
j++;
|
|
325
|
+
}
|
|
326
|
+
if (parenDepth > 0) {
|
|
327
|
+
throw new Error('Unclosed :insert() parentheses');
|
|
328
|
+
}
|
|
329
|
+
// Extract column list (between the parentheses)
|
|
330
|
+
const columnList = sql.slice(columnStart, j).trim();
|
|
331
|
+
const insertEnd = j + 1; // Include closing ')'
|
|
332
|
+
// Parse column list (split by comma, respecting quotes and parentheses)
|
|
333
|
+
const items = [];
|
|
334
|
+
let current = '';
|
|
335
|
+
let inSQ = false; // single quote string
|
|
336
|
+
let inDQ = false; // double quote identifier
|
|
337
|
+
let itemParenDepth = 0;
|
|
338
|
+
for (let k = 0; k < columnList.length; k++) {
|
|
339
|
+
const ch = columnList[k];
|
|
340
|
+
const next = k + 1 < columnList.length ? columnList[k + 1] : '';
|
|
341
|
+
if (inSQ) {
|
|
342
|
+
current += ch;
|
|
343
|
+
if (ch === "'" && next === "'") {
|
|
344
|
+
current += next;
|
|
345
|
+
k++;
|
|
346
|
+
}
|
|
347
|
+
else if (ch === "'") {
|
|
348
|
+
inSQ = false;
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (inDQ) {
|
|
353
|
+
current += ch;
|
|
354
|
+
if (ch === '"' && next === '"') {
|
|
355
|
+
current += next;
|
|
356
|
+
k++;
|
|
357
|
+
}
|
|
358
|
+
else if (ch === '"') {
|
|
359
|
+
inDQ = false;
|
|
360
|
+
}
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (ch === "'") {
|
|
364
|
+
current += ch;
|
|
365
|
+
inSQ = true;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (ch === '"') {
|
|
369
|
+
current += ch;
|
|
370
|
+
inDQ = true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (ch === '(') {
|
|
374
|
+
current += ch;
|
|
375
|
+
itemParenDepth++;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (ch === ')') {
|
|
379
|
+
current += ch;
|
|
380
|
+
itemParenDepth--;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (ch === ',' && itemParenDepth === 0) {
|
|
384
|
+
const trimmed = current.trim();
|
|
385
|
+
if (trimmed.length > 0) {
|
|
386
|
+
items.push(trimmed);
|
|
387
|
+
}
|
|
388
|
+
current = '';
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
current += ch;
|
|
392
|
+
}
|
|
393
|
+
// Add the last item
|
|
394
|
+
const trimmed = current.trim();
|
|
395
|
+
if (trimmed.length > 0) {
|
|
396
|
+
items.push(trimmed);
|
|
397
|
+
}
|
|
398
|
+
if (items.length === 0) {
|
|
399
|
+
throw new Error(':insert() requires at least one column name');
|
|
400
|
+
}
|
|
401
|
+
// Build column list and VALUES clause
|
|
402
|
+
const obj = args;
|
|
403
|
+
const columnNames = [];
|
|
404
|
+
const valueParts = [];
|
|
405
|
+
const insertParams = [];
|
|
406
|
+
let hasParameterized = false;
|
|
407
|
+
for (const item of items) {
|
|
408
|
+
const trimmed = item.trim();
|
|
409
|
+
// Check if this is a literal expression (contains = not inside quotes)
|
|
410
|
+
let isLiteral = false;
|
|
411
|
+
let eqIndex = -1;
|
|
412
|
+
let inString = false;
|
|
413
|
+
let stringChar = '';
|
|
414
|
+
for (let k = 0; k < trimmed.length; k++) {
|
|
415
|
+
const ch = trimmed[k];
|
|
416
|
+
const next = k + 1 < trimmed.length ? trimmed[k + 1] : '';
|
|
417
|
+
if (!inString && (ch === "'" || ch === '"')) {
|
|
418
|
+
inString = true;
|
|
419
|
+
stringChar = ch;
|
|
420
|
+
}
|
|
421
|
+
else if (inString) {
|
|
422
|
+
if (ch === stringChar) {
|
|
423
|
+
if (next === stringChar) {
|
|
424
|
+
k++; // Skip escaped quote
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
inString = false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else if (ch === '=') {
|
|
432
|
+
eqIndex = k;
|
|
433
|
+
isLiteral = true;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (isLiteral && eqIndex >= 0) {
|
|
438
|
+
// Literal SQL expression - extract column name and value
|
|
439
|
+
const colName = trimmed.slice(0, eqIndex).trim();
|
|
440
|
+
const literalValue = trimmed.slice(eqIndex + 1).trim();
|
|
441
|
+
columnNames.push(colName);
|
|
442
|
+
valueParts.push(literalValue);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
// Parameterized column - check if defined in args
|
|
446
|
+
hasParameterized = true;
|
|
447
|
+
if (Object.prototype.hasOwnProperty.call(obj, trimmed) && obj[trimmed] !== undefined) {
|
|
448
|
+
columnNames.push(trimmed);
|
|
449
|
+
valueParts.push(placeholder(pgIndex));
|
|
450
|
+
insertParams.push(obj[trimmed]);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// Only throw error if all parameterized columns are undefined (literals are always included)
|
|
455
|
+
if (hasParameterized && columnNames.length === 0) {
|
|
456
|
+
throw new Error('All columns in :insert() are undefined');
|
|
457
|
+
}
|
|
458
|
+
if (columnNames.length === 0) {
|
|
459
|
+
throw new Error(':insert() requires at least one column name');
|
|
460
|
+
}
|
|
461
|
+
// Replace :insert(...) with column list and VALUES clause
|
|
462
|
+
out.push('(', columnNames.join(', '), ') VALUES (', valueParts.join(', '), ')');
|
|
463
|
+
params.push(...insertParams);
|
|
464
|
+
i = insertEnd;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
// Check for :filter(...) syntax (case-insensitive)
|
|
468
|
+
const filterMatch = /^:filter\s*\(/i.exec(sql.slice(i));
|
|
469
|
+
if (filterMatch) {
|
|
470
|
+
if (isArrayArgs) {
|
|
471
|
+
throw new Error(':filter() syntax only works with named parameters');
|
|
472
|
+
}
|
|
473
|
+
// Parse the condition list inside parentheses
|
|
474
|
+
let j = i + filterMatch[0].length; // Skip ':filter('
|
|
475
|
+
const conditionStart = j;
|
|
476
|
+
let parenDepth = 1;
|
|
477
|
+
let inString = false;
|
|
478
|
+
let stringChar = '';
|
|
479
|
+
// Find matching closing parenthesis
|
|
480
|
+
while (j < len && parenDepth > 0) {
|
|
481
|
+
const c = sql[j];
|
|
482
|
+
const cNext = j + 1 < len ? sql[j + 1] : '';
|
|
483
|
+
if (!inString) {
|
|
484
|
+
if (c === "'" || c === '"') {
|
|
485
|
+
inString = true;
|
|
486
|
+
stringChar = c;
|
|
487
|
+
}
|
|
488
|
+
else if (c === '(') {
|
|
489
|
+
parenDepth++;
|
|
490
|
+
}
|
|
491
|
+
else if (c === ')') {
|
|
492
|
+
parenDepth--;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
if (c === stringChar) {
|
|
497
|
+
// Check for escaped quote
|
|
498
|
+
if (cNext === stringChar) {
|
|
499
|
+
j++; // Skip escaped quote
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
inString = false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (parenDepth > 0)
|
|
507
|
+
j++;
|
|
508
|
+
}
|
|
509
|
+
if (parenDepth > 0) {
|
|
510
|
+
throw new Error('Unclosed :filter() parentheses');
|
|
511
|
+
}
|
|
512
|
+
// Extract condition list (between the parentheses)
|
|
513
|
+
const conditionList = sql.slice(conditionStart, j).trim();
|
|
514
|
+
const filterEnd = j + 1; // Include closing ')'
|
|
515
|
+
// Parse condition list (split by comma, respecting quotes and parentheses)
|
|
516
|
+
const items = [];
|
|
517
|
+
let current = '';
|
|
518
|
+
let inSQ = false; // single quote string
|
|
519
|
+
let inDQ = false; // double quote identifier
|
|
520
|
+
let itemParenDepth = 0;
|
|
521
|
+
for (let k = 0; k < conditionList.length; k++) {
|
|
522
|
+
const ch = conditionList[k];
|
|
523
|
+
const next = k + 1 < conditionList.length ? conditionList[k + 1] : '';
|
|
524
|
+
if (inSQ) {
|
|
525
|
+
current += ch;
|
|
526
|
+
if (ch === "'" && next === "'") {
|
|
527
|
+
current += next;
|
|
528
|
+
k++;
|
|
529
|
+
}
|
|
530
|
+
else if (ch === "'") {
|
|
531
|
+
inSQ = false;
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (inDQ) {
|
|
536
|
+
current += ch;
|
|
537
|
+
if (ch === '"' && next === '"') {
|
|
538
|
+
current += next;
|
|
539
|
+
k++;
|
|
540
|
+
}
|
|
541
|
+
else if (ch === '"') {
|
|
542
|
+
inDQ = false;
|
|
543
|
+
}
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (ch === "'") {
|
|
547
|
+
current += ch;
|
|
548
|
+
inSQ = true;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (ch === '"') {
|
|
552
|
+
current += ch;
|
|
553
|
+
inDQ = true;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (ch === '(') {
|
|
557
|
+
current += ch;
|
|
558
|
+
itemParenDepth++;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (ch === ')') {
|
|
562
|
+
current += ch;
|
|
563
|
+
itemParenDepth--;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (ch === ',' && itemParenDepth === 0) {
|
|
567
|
+
const trimmed = current.trim();
|
|
568
|
+
if (trimmed.length > 0) {
|
|
569
|
+
items.push(trimmed);
|
|
570
|
+
}
|
|
571
|
+
current = '';
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
current += ch;
|
|
575
|
+
}
|
|
576
|
+
// Add the last item
|
|
577
|
+
const trimmed = current.trim();
|
|
578
|
+
if (trimmed.length > 0) {
|
|
579
|
+
items.push(trimmed);
|
|
580
|
+
}
|
|
581
|
+
if (items.length === 0) {
|
|
582
|
+
throw new Error(':filter() requires at least one condition');
|
|
583
|
+
}
|
|
584
|
+
// Build WHERE/HAVING conditions
|
|
585
|
+
const obj = args;
|
|
586
|
+
const conditions = [];
|
|
587
|
+
const filterParams = [];
|
|
588
|
+
let hasParameterized = false;
|
|
589
|
+
// Helper function to expand :or(:param) patterns
|
|
590
|
+
const expandOr = (expr) => {
|
|
591
|
+
// Find all :or(:param) patterns
|
|
592
|
+
const orPattern = /:or\s*\(\s*:(\w+)\s*\)/gi;
|
|
593
|
+
const matches = [];
|
|
594
|
+
let match;
|
|
595
|
+
while ((match = orPattern.exec(expr)) !== null) {
|
|
596
|
+
matches.push({
|
|
597
|
+
fullMatch: match[0],
|
|
598
|
+
paramName: match[1],
|
|
599
|
+
index: match.index
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
if (matches.length === 0) {
|
|
603
|
+
return { condition: '', params: [] };
|
|
604
|
+
}
|
|
605
|
+
// Check if all parameters are defined
|
|
606
|
+
let shouldInclude = true;
|
|
607
|
+
const replacements = [];
|
|
608
|
+
for (const m of matches) {
|
|
609
|
+
if (!Object.prototype.hasOwnProperty.call(obj, m.paramName) || obj[m.paramName] === undefined) {
|
|
610
|
+
shouldInclude = false;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
const arr = obj[m.paramName];
|
|
614
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
615
|
+
shouldInclude = false;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
// Find the column expression for this :or() pattern
|
|
619
|
+
// Look backwards from the match to find the column name and operator
|
|
620
|
+
const beforeOr = expr.slice(0, m.index);
|
|
621
|
+
const afterOr = expr.slice(m.index + m.fullMatch.length);
|
|
622
|
+
// Try to match column name and operator pattern
|
|
623
|
+
// Match patterns like "col =", "col >", "col <=", "col LIKE", etc.
|
|
624
|
+
const beforeTrimmed = beforeOr.trim();
|
|
625
|
+
const operatorMatch = beforeTrimmed.match(/(\w+)\s+([=<>!]+|>=|<=|!=|<>|LIKE|IN|BETWEEN|IS(?:\s+NOT)?)\s*$/i);
|
|
626
|
+
if (operatorMatch) {
|
|
627
|
+
const colName = operatorMatch[1];
|
|
628
|
+
const operator = operatorMatch[2];
|
|
629
|
+
const parts = [];
|
|
630
|
+
const params = [];
|
|
631
|
+
for (const val of arr) {
|
|
632
|
+
if (val === undefined)
|
|
633
|
+
continue;
|
|
634
|
+
parts.push(`${colName} ${operator} ${placeholder(pgIndex)}`);
|
|
635
|
+
params.push(val);
|
|
636
|
+
}
|
|
637
|
+
if (parts.length > 0) {
|
|
638
|
+
// Replace the column/operator and :or() together
|
|
639
|
+
// Find the operator text in the original expr by searching backwards
|
|
640
|
+
const operatorText = operatorMatch[0];
|
|
641
|
+
// Find the last occurrence of this pattern before m.index
|
|
642
|
+
let operatorStart = -1;
|
|
643
|
+
for (let pos = m.index - operatorText.length; pos >= 0; pos--) {
|
|
644
|
+
if (expr.slice(pos, pos + operatorText.length) === operatorText) {
|
|
645
|
+
operatorStart = pos;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (operatorStart === -1) {
|
|
650
|
+
// Fallback: use m.index - operatorText.length
|
|
651
|
+
operatorStart = Math.max(0, m.index - operatorText.length);
|
|
652
|
+
}
|
|
653
|
+
replacements.push({
|
|
654
|
+
index: operatorStart,
|
|
655
|
+
length: m.index + m.fullMatch.length - operatorStart,
|
|
656
|
+
replacement: parts.join(' OR '),
|
|
657
|
+
params
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
shouldInclude = false;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
// Fallback: use the entire before/after context
|
|
667
|
+
const parts = [];
|
|
668
|
+
const params = [];
|
|
669
|
+
for (const val of arr) {
|
|
670
|
+
if (val === undefined)
|
|
671
|
+
continue;
|
|
672
|
+
parts.push(`${beforeTrimmed}${placeholder(pgIndex)}${afterOr.trim()}`);
|
|
673
|
+
params.push(val);
|
|
674
|
+
}
|
|
675
|
+
if (parts.length > 0) {
|
|
676
|
+
replacements.push({
|
|
677
|
+
index: m.index,
|
|
678
|
+
length: m.fullMatch.length,
|
|
679
|
+
replacement: parts.join(' OR '),
|
|
680
|
+
params
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
shouldInclude = false;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (!shouldInclude || replacements.length === 0) {
|
|
690
|
+
return { condition: '', params: [] };
|
|
691
|
+
}
|
|
692
|
+
// Replace from end to start to preserve indices
|
|
693
|
+
let resultExpr = expr;
|
|
694
|
+
const allParams = [];
|
|
695
|
+
for (const rep of replacements.reverse()) {
|
|
696
|
+
const before = resultExpr.slice(0, rep.index);
|
|
697
|
+
const after = resultExpr.slice(rep.index + rep.length);
|
|
698
|
+
resultExpr = before + rep.replacement + after;
|
|
699
|
+
allParams.unshift(...rep.params);
|
|
700
|
+
}
|
|
701
|
+
// Check if the original expression was already wrapped in parentheses
|
|
702
|
+
const trimmedExpr = expr.trim();
|
|
703
|
+
const alreadyWrapped = trimmedExpr.startsWith('(') && trimmedExpr.endsWith(')');
|
|
704
|
+
const wrappedCondition = alreadyWrapped ? resultExpr : `(${resultExpr})`;
|
|
705
|
+
return { condition: wrappedCondition, params: allParams };
|
|
706
|
+
};
|
|
707
|
+
for (const item of items) {
|
|
708
|
+
const trimmed = item.trim();
|
|
709
|
+
// Check for :or(:param) pattern first
|
|
710
|
+
const orResult = expandOr(trimmed);
|
|
711
|
+
if (orResult.condition) {
|
|
712
|
+
conditions.push(orResult.condition);
|
|
713
|
+
filterParams.push(...orResult.params);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
// Check if this is a simple column name (no operators, no :param)
|
|
717
|
+
let hasOperator = false;
|
|
718
|
+
let hasParam = false;
|
|
719
|
+
let inString = false;
|
|
720
|
+
let stringChar = '';
|
|
721
|
+
for (let k = 0; k < trimmed.length; k++) {
|
|
722
|
+
const ch = trimmed[k];
|
|
723
|
+
const next = k + 1 < trimmed.length ? trimmed[k + 1] : '';
|
|
724
|
+
if (!inString && (ch === "'" || ch === '"')) {
|
|
725
|
+
inString = true;
|
|
726
|
+
stringChar = ch;
|
|
727
|
+
}
|
|
728
|
+
else if (inString) {
|
|
729
|
+
if (ch === stringChar) {
|
|
730
|
+
if (next === stringChar) {
|
|
731
|
+
k++; // Skip escaped quote
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
inString = false;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
// Check for operators (outside strings)
|
|
740
|
+
if ((ch === '>' || ch === '<' || ch === '=' || ch === '!') ||
|
|
741
|
+
(ch === 'L' && trimmed.slice(k, k + 4).toUpperCase() === 'LIKE') ||
|
|
742
|
+
(ch === 'I' && trimmed.slice(k, k + 2).toUpperCase() === 'IN') ||
|
|
743
|
+
(ch === 'B' && trimmed.slice(k, k + 7).toUpperCase() === 'BETWEEN') ||
|
|
744
|
+
(ch === 'I' && trimmed.slice(k, k + 2).toUpperCase() === 'IS')) {
|
|
745
|
+
hasOperator = true;
|
|
746
|
+
}
|
|
747
|
+
// Check for :param pattern
|
|
748
|
+
if (ch === ':' && next && /[a-zA-Z_]/.test(next)) {
|
|
749
|
+
hasParam = true;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (!hasOperator && !hasParam) {
|
|
754
|
+
// Simple column name → equality
|
|
755
|
+
hasParameterized = true;
|
|
756
|
+
if (Object.prototype.hasOwnProperty.call(obj, trimmed) && obj[trimmed] !== undefined) {
|
|
757
|
+
conditions.push(`${trimmed} = ${placeholder(pgIndex)}`);
|
|
758
|
+
filterParams.push(obj[trimmed]);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
else if (hasParam) {
|
|
762
|
+
// Parameterized expression → extract and bind parameters
|
|
763
|
+
hasParameterized = true;
|
|
764
|
+
// Extract all :param patterns and check if they're defined
|
|
765
|
+
let shouldInclude = true;
|
|
766
|
+
const paramMatches = [];
|
|
767
|
+
let searchStart = 0;
|
|
768
|
+
while (true) {
|
|
769
|
+
const paramMatch = /:([a-zA-Z_][a-zA-Z0-9_]*)/g.exec(trimmed.slice(searchStart));
|
|
770
|
+
if (!paramMatch)
|
|
771
|
+
break;
|
|
772
|
+
const paramName = paramMatch[1];
|
|
773
|
+
const fullIndex = searchStart + paramMatch.index;
|
|
774
|
+
paramMatches.push({ name: paramName, index: fullIndex });
|
|
775
|
+
if (!Object.prototype.hasOwnProperty.call(obj, paramName) || obj[paramName] === undefined) {
|
|
776
|
+
shouldInclude = false;
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
searchStart = fullIndex + paramMatch[0].length;
|
|
780
|
+
}
|
|
781
|
+
if (shouldInclude && paramMatches.length > 0) {
|
|
782
|
+
// Replace :param with placeholders (from end to start to preserve indices)
|
|
783
|
+
let conditionExpr = trimmed;
|
|
784
|
+
const orderedParams = [];
|
|
785
|
+
for (const pm of paramMatches.reverse()) {
|
|
786
|
+
const paramValue = obj[pm.name];
|
|
787
|
+
const before = conditionExpr.slice(0, pm.index);
|
|
788
|
+
const after = conditionExpr.slice(pm.index + pm.name.length + 1);
|
|
789
|
+
// Check if this is an array parameter (for IN clauses)
|
|
790
|
+
if (Array.isArray(paramValue)) {
|
|
791
|
+
if (paramValue.length === 0) {
|
|
792
|
+
shouldInclude = false;
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
// Expand array: replace :ids with ?, ?, ?
|
|
796
|
+
const placeholders = paramValue.map(() => placeholder(pgIndex)).join(', ');
|
|
797
|
+
conditionExpr = before + placeholders + after;
|
|
798
|
+
orderedParams.unshift(...paramValue);
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
conditionExpr = before + placeholder(pgIndex) + after;
|
|
802
|
+
orderedParams.unshift(paramValue);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (shouldInclude) {
|
|
806
|
+
// Add params in original order
|
|
807
|
+
const finalParams = [];
|
|
808
|
+
for (const pm of paramMatches.reverse()) {
|
|
809
|
+
const paramValue = obj[pm.name];
|
|
810
|
+
if (Array.isArray(paramValue)) {
|
|
811
|
+
finalParams.push(...paramValue);
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
finalParams.push(paramValue);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
conditions.push(conditionExpr);
|
|
818
|
+
filterParams.push(...finalParams);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
// Literal expression → include as-is
|
|
824
|
+
conditions.push(trimmed);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// Only throw error if all parameterized conditions are undefined (literals are always included)
|
|
828
|
+
if (hasParameterized && conditions.length === 0) {
|
|
829
|
+
throw new Error('All conditions in :filter() are undefined');
|
|
830
|
+
}
|
|
831
|
+
if (conditions.length === 0) {
|
|
832
|
+
throw new Error(':filter() requires at least one condition');
|
|
833
|
+
}
|
|
834
|
+
// Replace :filter(...) with conditions joined by AND
|
|
835
|
+
out.push(conditions.join(' AND '));
|
|
836
|
+
params.push(...filterParams);
|
|
837
|
+
i = filterEnd;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
182
840
|
// Attempt to parse a parameter token
|
|
183
841
|
let j = i + 1;
|
|
184
842
|
const start = j;
|