@digitalwalletcorp/sql-builder 1.6.1 → 2.1.0

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.
@@ -1,7 +1,7 @@
1
1
  import * as common from './common';
2
2
  import { AbstractSyntaxTree } from './abstract-syntax-tree';
3
3
 
4
- type TagType = 'BEGIN' | 'IF' | 'FOR' | 'END' | 'BIND';
4
+ type TagType = 'BEGIN' | 'IF' | 'ELSEIF' | 'ELSE' | 'FOR' | 'BIND' | 'TEXT' | 'END';
5
5
  type ExtractValueType<T extends 'string' | 'array' | 'object'>
6
6
  = T extends 'string'
7
7
  ? string | undefined
@@ -38,19 +38,54 @@ type BindParameterType<T extends BindType>
38
38
  : T extends 'mssql' ? Record<string, any>
39
39
  : undefined;
40
40
 
41
- interface TagContext {
42
- type: TagType;
43
- match: string;
44
- contents: string;
41
+ // すべてのタグの基底
42
+ interface BaseTagContext {
45
43
  startIndex: number;
46
44
  endIndex: number;
45
+ parent: ParentTagContext | null;
46
+ match: string;
47
+ }
48
+
49
+ // 子要素を持つ「親」になれるタグの共通定義
50
+ interface ParentTagContext extends BaseTagContext {
51
+ type: 'BEGIN' | 'IF' | 'ELSEIF' | 'ELSE' | 'FOR';
52
+ contents: string;
47
53
  sub: TagContext[];
48
- parent: TagContext | null;
49
- status: number; // 0: 初期、10: 成立 IFで条件が成立したかを判断するもの
50
- isPgArray?: boolean; // PostgreSQLがサポートする配列構文(ANY ($1::text[]) など)を示すフラグ
51
- pgArrayCast?: string; // ::text[] などのCAST部分を保持する
54
+ conditionMatched: boolean; // この要素自体の条件が成立したか
55
+ childConditionMatched: boolean; // 子要素のいずれかの条件が成立したか(BEGIN/IF用)
56
+ }
57
+
58
+ interface ContainerTagContext extends ParentTagContext {
59
+ type: 'BEGIN' | 'FOR';
60
+ }
61
+
62
+ interface BranchTagContext extends ParentTagContext {
63
+ type: 'IF' | 'ELSEIF' | 'ELSE';
64
+ }
65
+
66
+ interface BindTagContext extends BaseTagContext {
67
+ type: 'BIND';
68
+ contents: string;
69
+ isPgArray?: boolean;
70
+ pgArrayCast?: string;
71
+ }
72
+
73
+ interface TextTagContext extends BaseTagContext {
74
+ type: 'TEXT';
75
+ contents: string;
76
+ }
77
+
78
+ interface EndTagContext extends BaseTagContext {
79
+ type: 'END';
52
80
  }
53
81
 
82
+ type TagContext
83
+ = ContainerTagContext
84
+ | BranchTagContext
85
+ | TextTagContext
86
+ | BindTagContext
87
+ | EndTagContext;
88
+
54
89
  interface SharedIndex {
55
90
  index: number;
56
91
  }
@@ -195,45 +230,100 @@ export class SQLBuilder {
195
230
  const matches = template.matchAll(this.REGEX_TAG_PATTERN);
196
231
 
197
232
  // まず最初にREGEX_TAG_PATTERNで解析した情報をそのままフラットにTagContextの配列に格納
198
- let pos = 0;
233
+ let lastIndex = 0;
199
234
  const tagContexts: TagContext[] = [];
200
235
  for (const match of matches) {
201
236
  const matchContent = match[0];
202
237
  const index = match.index;
203
- pos = index + 1;
204
- const tagContext: TagContext = {
238
+
239
+ if (lastIndex < index) {
240
+ tagContexts.push({
241
+ type: 'TEXT',
242
+ match: '',
243
+ contents: template.substring(lastIndex, index),
244
+ startIndex: lastIndex,
245
+ endIndex: index,
246
+ parent: null
247
+ });
248
+ }
249
+
250
+ let tagContext: TagContext = {
205
251
  type: 'BIND', // ダミーの初期値。後続処理で適切なタイプに変更する。
206
- match: matchContent,
207
252
  contents: '',
208
253
  startIndex: index,
209
254
  endIndex: index + matchContent.length,
210
- sub: [],
211
255
  parent: null,
212
- status: 0
256
+ match: matchContent
213
257
  };
214
258
  switch (true) {
215
259
  case matchContent === '/*BEGIN*/': {
216
- tagContext.type = 'BEGIN';
260
+ tagContext = {
261
+ ...tagContext,
262
+ type: 'BEGIN',
263
+ sub: [],
264
+ conditionMatched: false,
265
+ childConditionMatched: false
266
+ } as ContainerTagContext;
217
267
  break;
218
268
  }
219
269
  case matchContent.startsWith('/*IF'): {
220
- tagContext.type = 'IF';
270
+ tagContext = {
271
+ ...tagContext,
272
+ type: 'IF',
273
+ sub: [],
274
+ conditionMatched: false,
275
+ childConditionMatched: false
276
+ } as BranchTagContext;
221
277
  const contentMatcher = matchContent.match(/^\/\*IF\s+(.*?)\*\/$/);
222
278
  tagContext.contents = contentMatcher && contentMatcher[1] || '';
223
279
  break;
224
280
  }
281
+ case matchContent.startsWith('/*ELSEIF'): {
282
+ tagContext = {
283
+ ...tagContext,
284
+ type: 'ELSEIF',
285
+ sub: [],
286
+ conditionMatched: false,
287
+ childConditionMatched: false
288
+ } as BranchTagContext;
289
+ const contentMatcher = matchContent.match(/^\/\*ELSEIF\s+(.*?)\*\/$/);
290
+ tagContext.contents = contentMatcher && contentMatcher[1] || '';
291
+ break;
292
+ }
293
+ case matchContent.startsWith('/*ELSE*/'): {
294
+ tagContext = {
295
+ ...tagContext,
296
+ type: 'ELSE',
297
+ sub: [],
298
+ conditionMatched: false,
299
+ childConditionMatched: false
300
+ } as BranchTagContext;
301
+ break;
302
+ }
225
303
  case matchContent.startsWith('/*FOR'): {
226
- tagContext.type = 'FOR';
304
+ tagContext = {
305
+ ...tagContext,
306
+ type: 'FOR',
307
+ sub: [],
308
+ conditionMatched: false,
309
+ childConditionMatched: false
310
+ } as ContainerTagContext;
227
311
  const contentMatcher = matchContent.match(/^\/\*FOR\s+(.*?)\*\/$/);
228
312
  tagContext.contents = contentMatcher && contentMatcher[1] || '';
229
313
  break;
230
314
  }
231
315
  case matchContent === '/*END*/': {
232
- tagContext.type = 'END';
316
+ tagContext = {
317
+ ...tagContext,
318
+ type: 'END'
319
+ } as EndTagContext;
233
320
  break;
234
321
  }
235
322
  default: {
236
- tagContext.type = 'BIND';
323
+ tagContext = {
324
+ ...tagContext,
325
+ type: 'BIND'
326
+ } as BindTagContext;
237
327
  const contentMatcher = matchContent.match(/\/\*(.*?)\*\//);
238
328
  tagContext.contents = contentMatcher && contentMatcher[1]?.trim() || '';
239
329
  // ダミー値の終了位置をendIndexに設定
@@ -253,26 +343,59 @@ export class SQLBuilder {
253
343
  }
254
344
  }
255
345
  }
346
+
256
347
  tagContexts.push(tagContext);
348
+
349
+ lastIndex = tagContext.endIndex;
350
+ }
351
+
352
+ // ループを抜けた後に残った部分をTEXTタグとして追加
353
+ if (lastIndex < template.length) {
354
+ tagContexts.push({
355
+ type: 'TEXT',
356
+ match: '',
357
+ contents: template.substring(lastIndex),
358
+ startIndex: lastIndex,
359
+ endIndex: template.length,
360
+ parent: null
361
+ });
257
362
  }
258
363
 
259
- // できあがったTagContextの配列から、BEGEN、IFの場合は次の対応するENDが出てくるまでをsubに入れ直して構造化し、
260
- // 以下のような構造の変更する
364
+ // できあがったTagContextの配列から、
365
+ // BEGENの場合は対応するENDが出てくるまで、
366
+ // IFの場合は次の対応するENDが出てくるまでをsubに入れ直して構造化し、
367
+ // 以下のような構造に変更する
261
368
  /**
262
369
  * ```
263
370
  * BEGIN
264
371
  * ├ IF
265
- * ├ BIND
266
- * ├ BIND
372
+ * ├ BIND(無いこともある)
373
+ * ├ ELSEIF
374
+ * ├ BIND(無いこともある)
375
+ * ├ ELSE
376
+ * ├ BIND(無いこともある)
267
377
  * ├ END
268
378
  * ├ BIND
269
379
  * END
270
380
  * ```
271
381
  */
272
- const parentTagContexts: TagContext[] = [];
382
+ const parentTagContexts: ParentTagContext[] = [];
273
383
  const newTagContexts: TagContext[] = [];
274
384
  for (const tagContext of tagContexts) {
275
385
  switch (tagContext.type) {
386
+ case 'TEXT':
387
+ case 'BIND': {
388
+ const parentTagContext = parentTagContexts[parentTagContexts.length - 1];
389
+ if (parentTagContext) {
390
+ // 親タグがある
391
+ tagContext.parent = parentTagContext;
392
+ parentTagContext.sub.push(tagContext);
393
+ } else {
394
+ // 親タグがない(最上位)
395
+ newTagContexts.push(tagContext);
396
+ }
397
+ break;
398
+ }
276
399
  case 'BEGIN':
277
400
  case 'IF':
278
401
  case 'FOR': {
@@ -289,23 +412,42 @@ export class SQLBuilder {
289
412
  parentTagContexts.push(tagContext);
290
413
  break;
291
414
  }
415
+ case 'ELSEIF':
416
+ case 'ELSE': {
417
+ const ifTagContext = this.findParentTagContext(parentTagContexts[parentTagContexts.length - 1], 'IF');
418
+ if (ifTagContext) {
419
+ // 親タグ(IF)がある
420
+ tagContext.parent = ifTagContext;
421
+ ifTagContext.sub.push(tagContext);
422
+ } else {
423
+ throw new Error(`[SQLBuilder] ${tagContext.type} must be inside IF.`);
424
+ }
425
+ // 後続処理で自身が親になるので自身を追加
426
+ parentTagContexts.push(tagContext); // これは暫定的なものなのでENDで削除する必要がある
427
+ break;
428
+ }
292
429
  case 'END': {
293
- const parentTagContext = parentTagContexts.pop()!;
294
- // ENDのときは必ず対応するIF/BEGINがあるので、親のsubに追加
430
+ let parentTagContext = parentTagContexts[parentTagContexts.length - 1];
431
+ if (!parentTagContext) {
432
+ throw new Error(`[SQLBuilder] 'END' tag without corresponding parent.`);
433
+ }
434
+
435
+ if (parentTagContext.type === 'ELSEIF' || parentTagContext.type === 'ELSE') {
436
+ // 暫定追加されたELSEIF/ELSEを除去
437
+ while(parentTagContexts.length && ['ELSEIF', 'ELSE'].includes(parentTagContexts[parentTagContexts.length - 1].type)) {
438
+ parentTagContexts.pop();
439
+ }
440
+ parentTagContext = parentTagContexts[parentTagContexts.length - 1];
441
+ }
295
442
  tagContext.parent = parentTagContext;
296
443
  parentTagContext.sub.push(tagContext);
444
+
445
+ parentTagContexts.pop();
297
446
  break;
298
447
  }
299
448
  default: {
300
- const parentTagContext = parentTagContexts[parentTagContexts.length - 1];
301
- if (parentTagContext) {
302
- // 親タグがある
303
- tagContext.parent = parentTagContext;
304
- parentTagContext.sub.push(tagContext);
305
- } else {
306
- // 親タグがない(最上位)
307
- newTagContexts.push(tagContext);
308
- }
449
+ // 型定義にない型という扱いになるのでanyキャストする(到達不可能?)
450
+ throw new Error(`[SQLBuilder] Unknown TagType '${(tagContext as any).type}`);
309
451
  }
310
452
  }
311
453
  }
@@ -319,14 +461,14 @@ export class SQLBuilder {
319
461
  * @param {SharedIndex} pos 現在処理している文字列の先頭インデックス
320
462
  * @param {string} template
321
463
  * @param {Record<string, any>} entity
322
- * @param {TagContext[]} tagContexts
464
+ * @param {(TagContext | ParentTagContext)[]} tagContexts
323
465
  * @param {*} [options]
324
466
  * ├ bindType BindType
325
467
  * ├ bindIndex number
326
468
  * ├ bindParams BindParameterType<T>
327
469
  * @returns {string}
328
470
  */
329
- private parse<T extends BindType>(pos: SharedIndex, template: string, entity: Record<string, any>, tagContexts: TagContext[], options?: {
471
+ private parse<T extends BindType>(pos: SharedIndex, template: string, entity: Record<string, any>, tagContexts: (TagContext | ParentTagContext)[], options?: {
330
472
  bindType: T,
331
473
  bindIndex: number,
332
474
  bindParams: BindParameterType<T>
@@ -334,6 +476,14 @@ export class SQLBuilder {
334
476
  let result = '';
335
477
  for (const tagContext of tagContexts) {
336
478
  switch (tagContext.type) {
479
+ case 'TEXT': {
480
+ // テキストは無条件に書き出し
481
+ // 改行だけなど、trimすると空になる文字列もあるが、
482
+ // どの部分がスキップされたのかわかるようにあえて残すことにする
483
+ result += tagContext.contents;
484
+ pos.index = tagContext.endIndex;
485
+ break;
486
+ }
337
487
  case 'BEGIN': {
338
488
  result += template.substring(pos.index, tagContext.startIndex);
339
489
  pos.index = tagContext.endIndex;
@@ -341,45 +491,92 @@ export class SQLBuilder {
341
491
  const beginBlockResult = this.parse(pos, template, entity, tagContext.sub, options);
342
492
  // BEGIN内のIF、FORのいずれかで成立したものがあった場合は結果を出力
343
493
  if (tagContext.sub.some(sub =>
344
- (sub.type === 'IF' || sub.type === 'FOR') && sub.status === 10
494
+ (sub.type === 'IF' || sub.type === 'FOR') && sub.conditionMatched
345
495
  )) {
346
496
  result += beginBlockResult;
347
497
  }
348
498
  break;
349
499
  }
350
500
  case 'IF': {
351
- result += template.substring(pos.index, tagContext.startIndex);
352
- pos.index = tagContext.endIndex;
353
- if (this.evaluateCondition(tagContext.contents, entity)) {
354
- // IF条件が成立する場合はsubに対して再帰呼び出し
355
- tagContext.status = 10; // 成立(→/*BEGIN*/を使っている場合の判定条件になる)
356
- // IFの結果自体は他の要素に影響されないので直接resultに還元可能
357
- result += this.parse(pos, template, entity, tagContext.sub, options);
358
- } else {
359
- // IF条件が成立しない場合は再帰呼び出しせず、subのENDタグのendIndexをposに設定
501
+ const conditionMatched = this.evaluateCondition(tagContext.contents, entity);
502
+ if (conditionMatched) {
503
+ tagContext.conditionMatched = true; // 自身の評価結果
504
+ pos.index = tagContext.endIndex;
505
+ // 条件が成立したので、自身のsubにある条件タグを排除する
506
+ const children = tagContext.sub.filter(a => a.type !== 'ELSEIF' && a.type !== 'ELSE');
507
+ result += this.parse(pos, template, entity, children, options);
508
+
509
+ // IFタグからENDを探すときは、自身のsubの最後の要素
360
510
  const endTagContext = tagContext.sub[tagContext.sub.length - 1];
361
511
  pos.index = endTagContext.endIndex;
512
+ } else {
513
+ // 条件不成立
514
+ const nextBranch = tagContext.sub.find(a => a.type === 'ELSEIF' || a.type === 'ELSE');
515
+ if (nextBranch) {
516
+ pos.index = nextBranch.startIndex;
517
+ result += this.parse(pos, template, entity, [nextBranch], options);
518
+ } else {
519
+ // 次の条件がない場合はENDの後ろまでポインタを飛ばす
520
+ const endTagContext = tagContext.sub[tagContext.sub.length - 1];
521
+ pos.index = endTagContext.endIndex;
522
+ }
523
+ }
524
+ break;
525
+ }
526
+ case 'ELSEIF':
527
+ case 'ELSE': {
528
+ const conditionMatched = tagContext.type === 'ELSE' || this.evaluateCondition(tagContext.contents, entity);
529
+ if (conditionMatched) {
530
+ tagContext.conditionMatched = true; // 自身の評価結果
531
+ pos.index = tagContext.endIndex;
532
+ // 条件が成立したので、自身のsubにある条件タグを排除する
533
+ const children = tagContext.sub.filter(a => a.type !== 'ELSEIF' && a.type !== 'ELSE');
534
+ result += this.parse(pos, template, entity, children, options);
535
+
536
+ // ELSEIF/ELSEからIFを探すときは自身の親そのもの
537
+ const ifTagContext = tagContext.parent!;
538
+ ifTagContext.childConditionMatched = true; // 親が持っている評価結果にtrueを設定
539
+
540
+ // ELSEIF/ELSEからENDを探すときは、自身の兄弟の最後の要素
541
+ const endTagContext = this.seekSiblingTagContext(tagContext, 'END', 'next')!;
542
+ pos.index = endTagContext.endIndex;
543
+ } else {
544
+ // 条件不成立
545
+ const nextBranch = this.seekSiblingTagContext(tagContext, ['ELSEIF', 'ELSE'], 'next');
546
+ if (nextBranch) {
547
+ pos.index = nextBranch.startIndex;
548
+ result += this.parse(pos, template, entity, [nextBranch], options);
549
+ } else {
550
+ // 次の条件がない場合はENDの後ろまでポインタを飛ばす
551
+ const endTagContext = tagContext.sub[tagContext.sub.length - 1];
552
+ pos.index = endTagContext.endIndex;
553
+ }
362
554
  }
363
555
  break;
364
556
  }
365
557
  case 'FOR': {
366
- result += template.substring(pos.index, tagContext.startIndex);
367
- pos.index = tagContext.endIndex;
368
558
  const [bindName, collectionName] = tagContext.contents.split(':').map(a => a.trim());
369
559
  const array = common.getProperty(entity, collectionName);
370
560
  if (Array.isArray(array) && array.length) {
371
- tagContext.status = 10; // 成立(→/*BEGIN*/を使っている場合の判定条件になる)
561
+ tagContext.conditionMatched = true;
372
562
  for (const value of array) {
373
- // 再帰呼び出しによりposが進むので、ループのたびにposを戻す必要がある
374
- pos.index = tagContext.endIndex;
375
- // FORの結果自体は他の要素に影響されないので直接resultに還元可能
376
- result += this.parse(pos, template, {
377
- ...entity,
378
- [bindName]: value
379
- }, tagContext.sub, options);
380
- // FORループするときは各行で改行する
563
+ const children = tagContext.sub.filter(a => a.type !== 'END');
564
+ result += this.parse(
565
+ pos,
566
+ template,
567
+ {
568
+ ...entity,
569
+ [bindName]: value
570
+ },
571
+ children,
572
+ options
573
+ );
381
574
  result += '\n';
382
575
  }
576
+
577
+ // FORのENDまでポインタを進める
578
+ const endTag = tagContext.sub[tagContext.sub.length - 1];
579
+ pos.index = endTag.endIndex;
383
580
  } else {
384
581
  // FORブロックを丸ごとスキップ
385
582
  const endTagContext = tagContext.sub[tagContext.sub.length - 1];
@@ -388,20 +585,26 @@ export class SQLBuilder {
388
585
  break;
389
586
  }
390
587
  case 'END': {
391
- switch (true) {
392
- case tagContext.parent?.type === 'BEGIN'
393
- && tagContext.parent?.sub.some(a => ['IF', 'FOR'].includes(a.type))
394
- && !tagContext.parent?.sub.some(a => ['IF', 'FOR'].includes(a.type) && a.status === 10):
395
- // BEGINに対応するENDの場合
396
- // ・子要素にIFまたはFORが存在する
397
- // ・子要素のIFまたはFORにstatus=10(成功)を示すものが1つもない
398
- case tagContext.parent?.type === 'IF' && tagContext.parent.status !== 10:
399
- // IFに対応するENDの場合、IFのstatusがstatus=10(成功)になっていない
400
- case tagContext.parent?.type === 'FOR' && tagContext.parent.status !== 10:
401
- // FORに対応するENDの場合、FORのstatusがstatus=10(成功)になっていない
402
- pos.index = tagContext.endIndex;
403
- return '';
404
- default:
588
+ const parent = tagContext.parent;
589
+ if (parent) {
590
+ switch (true) {
591
+ // BEGIN/IF/FORに対応するENDは、それぞれの処理で考慮すると複雑化するので
592
+ // すべてENDタグでどういう状態になったのは判定して動きを制御する
593
+ case parent.type === 'BEGIN'
594
+ && parent.sub.some(a => a.type === 'IF' || a.type === 'FOR')
595
+ && !parent.sub.some(a => a.type === 'IF' || a.type === 'FOR' && a.conditionMatched):
596
+ // BEGINに対応するENDの場合
597
+ // ・子要素にIFまたはFORが存在する
598
+ // ・子要素のIFまたはFORstatus=10(成功)を示すものが1つもない
599
+ case parent.type === 'IF' && !parent.conditionMatched:
600
+ // IFに対応するENDの場合、IFのstatusがstatus=10(成功)になっていない
601
+ case parent.type === 'IF' && parent.sub.some(a => a.type === 'ELSEIF' || a.type === 'ELSE'):
602
+ case parent.type === 'FOR' && !parent.conditionMatched:
603
+ // FORに対応するENDの場合、FORのstatusがstatus=10(成功)になっていない
604
+ pos.index = tagContext.endIndex;
605
+ return result;
606
+ default:
607
+ }
405
608
  }
406
609
  result += template.substring(pos.index, tagContext.startIndex);
407
610
  pos.index = tagContext.endIndex;
@@ -410,14 +613,13 @@ export class SQLBuilder {
410
613
  case 'BIND': {
411
614
  result += template.substring(pos.index, tagContext.startIndex);
412
615
  pos.index = tagContext.endIndex;
616
+ const propertyResult = common.getPropertyResult(entity, tagContext.contents);
413
617
  // ★ UNKNOWN_TAG 判定
414
- const hasProperty = Object.prototype.hasOwnProperty.call(entity, tagContext.contents);
415
- if (!hasProperty) {
618
+ if (!propertyResult.exists) {
416
619
  // UNKNOWN_TAG → エラーを発行
417
- throw new Error(`[SQLBuilder] The property "${tagContext.contents}" is not found in the bind entity. (Template index: ${tagContext.startIndex})`);
620
+ throw new Error(`[SQLBuilder] The property '${tagContext.contents}' is not found in the bind entity. (Template index: ${tagContext.startIndex})`);
418
621
  }
419
- const rawValue = common.getProperty(entity, tagContext.contents);
420
- const value = rawValue === undefined ? null : rawValue;
622
+ const value = propertyResult.value === undefined ? null : propertyResult.value;
421
623
  switch (options?.bindType) {
422
624
  case 'postgres': {
423
625
  // PostgreSQL形式の場合、$Nでバインドパラメータを展開
@@ -438,7 +640,7 @@ export class SQLBuilder {
438
640
  placeholders.push(`$${options.bindIndex++}`);
439
641
  (options.bindParams as any[]).push(item);
440
642
  }
441
- result += `(${placeholders.join(',')})`; // IN ($1,$2,$3)
643
+ result += placeholders.join(','); // IN ($1,$2,$3)
442
644
  } else {
443
645
  (options.bindParams as any[]).push(value);
444
646
  result += `$${options.bindIndex++}`;
@@ -453,7 +655,7 @@ export class SQLBuilder {
453
655
  placeholders.push('?');
454
656
  (options.bindParams as any[]).push(item);
455
657
  }
456
- result += `(${placeholders.join(',')})`; // IN (?,?,?)
658
+ result += placeholders.join(','); // IN (?,?,?)
457
659
  } else {
458
660
  (options.bindParams as any[]).push(value);
459
661
  result += '?';
@@ -470,7 +672,7 @@ export class SQLBuilder {
470
672
  placeholders.push(`:${paramName}`);
471
673
  (options.bindParams as Record<string, any>)[paramName] = value[i];
472
674
  }
473
- result += `(${placeholders.join(',')})`; // IN (:p_0,:p_1,:p3)
675
+ result += placeholders.join(','); // IN (:p_0,:p_1,:p3)
474
676
  } else {
475
677
  (options.bindParams as Record<string, any>)[tagContext.contents] = value;
476
678
  result += `:${tagContext.contents}`;
@@ -487,7 +689,7 @@ export class SQLBuilder {
487
689
  placeholders.push(`@${paramName}`);
488
690
  (options.bindParams as Record<string, any>)[paramName] = value[i];
489
691
  }
490
- result += `(${placeholders.join(',')})`; // IN (:p_0,:p_1,:p3)
692
+ result += placeholders.join(','); // IN (:p_0,:p_1,:p3)
491
693
  } else {
492
694
  (options.bindParams as Record<string, any>)[tagContext.contents] = value;
493
695
  result += `@${tagContext.contents}`;
@@ -510,11 +712,75 @@ export class SQLBuilder {
510
712
  }
511
713
  }
512
714
 
715
+ if (tagContexts.length && tagContexts[0].parent) {
716
+ // IFタグ解析中のELSEIF/ELSEなどが残っている場合は以降の文字列を連結せず、IFの解析結果のみ返す
717
+ return result;
718
+ }
719
+
513
720
  // 最後に余った部分を追加する
514
721
  result += template.substring(pos.index);
515
722
  return result;
516
723
  }
517
724
 
725
+ /**
726
+ * 指定したタグから親を遡り、指定したタグタイプのタグコンテキストを返す
727
+ * 見つからない場合はundefinedを返す
728
+ *
729
+ * @param {TagContext | ParentTagContext | null} tagContext
730
+ * @param {T} tagType
731
+ * @returns {(TagContext & { type: T }) | undefined}
732
+ */
733
+ private findParentTagContext<T extends TagType>(tagContext: TagContext | ParentTagContext | null, tagType: T): (TagContext & { type: T }) | undefined {
734
+ let targetTagContext = tagContext;
735
+ while (targetTagContext != null) {
736
+ if (targetTagContext.type === tagType) {
737
+ return targetTagContext as Extract<TagContext, { type: T }>;
738
+ }
739
+ targetTagContext = targetTagContext.parent;
740
+ }
741
+ return undefined;
742
+ }
743
+
744
+ /**
745
+ * 指定したタグの兄弟をたどり、指定したタグタイプのタグコンテキストを返す
746
+ * 見つからない場合はundefinedを返す
747
+ * ユースケースとしては、ELSEIF/ELSEから同じIFに属するENDを探す
748
+ *
749
+ * @param {TagContext} tagContext
750
+ * @param {TagType | TagType[]} tagType
751
+ * @param {'previous' | 'next'} direction
752
+ * @returns {TagContext | undefined}
753
+ */
754
+ private seekSiblingTagContext(tagContext: TagContext, tagType: TagType | TagType[], direction: 'previous' | 'next' = 'next'): TagContext | undefined {
755
+ const parent = tagContext.parent;
756
+ if (parent) {
757
+ // 自身が所属するインデックスを取得
758
+ const startIndex = parent.sub.indexOf(tagContext);
759
+ if (startIndex < 0) {
760
+ return undefined;
761
+ }
762
+ const tagTypes = Array.isArray(tagType) ? tagType : [tagType];
763
+ switch (direction) {
764
+ case 'previous':
765
+ for (let i = startIndex - 1; 0 <= i; i--) {
766
+ if (tagTypes.some(a => a === parent.sub[i].type)) {
767
+ return parent.sub[i];
768
+ }
769
+ }
770
+ break;
771
+ case 'next':
772
+ for (let i = startIndex + 1; i < parent.sub.length; i++) {
773
+ if (tagTypes.some(a => a === parent.sub[i].type)) {
774
+ return parent.sub[i];
775
+ }
776
+ }
777
+ break;
778
+ default:
779
+ }
780
+ }
781
+ return undefined;
782
+ }
783
+
518
784
  /**
519
785
  * ダミーパラメータの終了インデックスを返す
520
786
  *
@@ -559,10 +825,12 @@ export class SQLBuilder {
559
825
  break;
560
826
  case c === '(':
561
827
  // 丸括弧開始
562
- bracket = true;
563
- break;
828
+ // bracket = true;
829
+ // break;
830
+ return i;
564
831
  case c === ')':
565
- throw new Error(`[SQLBuilder] 括弧が開始されていません [index: ${i}, subsequence: '${template.substring(Math.max(i - 20, 0), i + 20)}']`);
832
+ // throw new Error(`[SQLBuilder] 括弧が開始されていません [index: ${i}, subsequence: '${template.substring(Math.max(i - 20, 0), i + 20)}']`);
833
+ return i;
566
834
  case c === '*' && 1 < i && chars[i - 1] === '/':
567
835
  // 次ノード開始
568
836
  return i - 1;
@@ -635,7 +903,11 @@ export class SQLBuilder {
635
903
  private extractValue<T extends 'string' | 'array' | 'object' = 'string'>(property: string, entity: Record<string, any>, options?: {
636
904
  responseType?: T
637
905
  }): ExtractValueType<T> {
638
- const value = common.getProperty(entity, property);
906
+ const propertyResult = common.getPropertyResult(entity, property);
907
+ if (!propertyResult.exists) {
908
+ throw new Error(`[SQLBuilder] The property '${property}' is not found in the entity: ${JSON.stringify(entity)}`);
909
+ }
910
+ const value = propertyResult.value;
639
911
  if (value == null) {
640
912
  return 'NULL' as ExtractValueType<T>;
641
913
  }
@@ -647,7 +919,7 @@ export class SQLBuilder {
647
919
  default:
648
920
  // string
649
921
  if (Array.isArray(value)) {
650
- result = `(${value.map(v => typeof v === 'string' ? `'${this.escape(v)}'` : v).join(',')})`;
922
+ result = value.map(v => typeof v === 'string' ? `'${this.escape(v)}'` : v).join(',');
651
923
  } else {
652
924
  result = typeof value === 'string' ? `'${this.escape(value)}'` : value;
653
925
  }