xcraft-core-pickaxe 0.1.26 → 0.1.27

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.
@@ -185,6 +185,20 @@ const operators = {
185
185
  )})`;
186
186
  },
187
187
 
188
+ case({whenConditions, elseValue}, context) {
189
+ const whenSql = whenConditions
190
+ .map(([condition, value]) => {
191
+ const conditionSql = sql(condition, context);
192
+ if (!conditionSql) {
193
+ return '';
194
+ }
195
+ return `WHEN (${conditionSql}) THEN (${sql(value, context)})`;
196
+ })
197
+ .filter(Boolean)
198
+ .join(' ');
199
+ return `CASE ${whenSql} ELSE (${sql(elseValue, context)}) END`;
200
+ },
201
+
188
202
  abs({value}, context) {
189
203
  return `ABS(${sql(value, context)})`;
190
204
  },
package/lib/operators.js CHANGED
@@ -234,6 +234,17 @@ const operators = {
234
234
  });
235
235
  },
236
236
 
237
+ case(whenConditions, elseValue) {
238
+ return /** @type {const} */ ({
239
+ operator: 'case',
240
+ whenConditions: whenConditions.map(([condition, value]) => [
241
+ op(condition),
242
+ op(value),
243
+ ]),
244
+ elseValue: op(elseValue),
245
+ });
246
+ },
247
+
237
248
  abs(value) {
238
249
  value = op(value);
239
250
  return /** @type {const} */ ({
@@ -0,0 +1,17 @@
1
+ const {ObjectType} = require('xcraft-core-stones');
2
+
3
+ /**
4
+ * @template {ObjectType} T
5
+ * @template {(keyof T["properties"])[]} F
6
+ * @param {T} objectType
7
+ * @param {F} fields
8
+ * @returns {Pick<T["properties"], F[number]>}
9
+ */
10
+ function partialObjectShape(objectType, fields) {
11
+ const properties = objectType.properties;
12
+ return /** @type {Pick<T["properties"], F[number]>} */ (Object.fromEntries(
13
+ fields.map((fieldName) => [fieldName, properties[fieldName]])
14
+ ));
15
+ }
16
+
17
+ module.exports = partialObjectShape;
@@ -29,6 +29,11 @@ const {getPickOrValueType} = require('./pick-or-value.js');
29
29
  * @typedef {import('./pick-or-value.js').PickOrValueType<T>} PickOrValueType
30
30
  */
31
31
 
32
+ /**
33
+ * @template {PickOrValue[]} T
34
+ * @typedef {{[K in keyof T]: PickOrValueType<T[K]>}} PickOrValueTypes
35
+ */
36
+
32
37
  const pickOperators = {
33
38
  ...$o,
34
39
 
@@ -76,6 +81,30 @@ const pickOperators = {
76
81
  return new ValuePick(newType, $o.if(condition, a, b));
77
82
  },
78
83
 
84
+ /**
85
+ * @template {PickOrValue[]} T
86
+ * @typedef {{[K in keyof T]: [BooleanPick | boolean, T[K]]}} CaseConditions
87
+ */
88
+
89
+ /**
90
+ * @template {[PickOrValue, ...PickOrValue[]]} T
91
+ * @template {PickOrValue} U
92
+ * @param {[...CaseConditions<T>, U]} conditions
93
+ * @returns {ValuePick<UnionType<[...PickOrValueTypes<T>, PickOrValueType<U>]>>}
94
+ */
95
+ case(...conditions) {
96
+ const whenConditions = /** @type {CaseConditions<T>} */ (
97
+ //
98
+ conditions.slice(0, -1)
99
+ );
100
+ const whenTypes = /** @type {PickOrValueTypes<T>} */ (whenConditions.map(
101
+ (condition) => getPickOrValueType(condition[1])
102
+ ));
103
+ const elseValue = /** @type {U} */ (conditions.at(-1));
104
+ const newType = union(...whenTypes, getPickOrValueType(elseValue));
105
+ return new ValuePick(newType, $o.case(whenConditions, elseValue));
106
+ },
107
+
79
108
  /**
80
109
  * @param {BasePick} [field]
81
110
  * @param {boolean} [distinct]
@@ -6,6 +6,7 @@ const {
6
6
  NumberType,
7
7
  string,
8
8
  StringType,
9
+ ValueType,
9
10
  boolean,
10
11
  BooleanType,
11
12
  } = require('xcraft-core-stones');
@@ -13,7 +14,7 @@ const {BasePick, makeTypePick} = require('./picks.js');
13
14
  const $o = require('./operators.js');
14
15
 
15
16
  /**
16
- * @typedef {string | number | boolean} PickValue
17
+ * @typedef {string | number | boolean | null} PickValue
17
18
  */
18
19
 
19
20
  /**
@@ -22,7 +23,7 @@ const $o = require('./operators.js');
22
23
 
23
24
  /**
24
25
  * @template {PickValue} T
25
- * @typedef {T extends boolean ? BooleanType : T extends number ? NumberType : T extends string ? StringType : never} GetValueType
26
+ * @typedef {T extends boolean ? BooleanType : T extends number ? NumberType : T extends string ? StringType<T> : T extends null ? ValueType<null> : never} GetValueType
26
27
  */
27
28
 
28
29
  /**
@@ -45,6 +46,9 @@ function getValueType(value) {
45
46
  if (typeof value === 'boolean') {
46
47
  return /** @type {any} */ (boolean);
47
48
  }
49
+ if (value === null) {
50
+ return /** @type {any} */ (new ValueType(null));
51
+ }
48
52
  throw new Error('Unknown value type');
49
53
  }
50
54
 
package/lib/picks.js CHANGED
@@ -186,6 +186,14 @@ class StringPick extends ValuePick {
186
186
  substr(start, length) {
187
187
  return new StringPick(string, $o.substr(this.expression, start, length));
188
188
  }
189
+
190
+ /**
191
+ * @param {(StringPick | string)[]} values
192
+ * @returns {StringPick}
193
+ */
194
+ concat(...values) {
195
+ return new StringPick(string, $o.stringConcat(this.expression, ...values));
196
+ }
189
197
  }
190
198
 
191
199
  /**
@@ -467,22 +475,19 @@ function isAnyPick(value) {
467
475
  * @extends {BasePick<ObjectType<T>>}
468
476
  */
469
477
  class RowPick extends BasePick {
470
- /**
471
- * @type {string | null}
472
- */
473
- #tableName = null;
478
+ #context;
474
479
 
475
480
  /**
476
481
  * @param {ObjectType<T>} type
477
- * @param {{expression: Expression} | {tableName: string}} context
482
+ * @param {{expression: Expression} | {tableName: string} | null} context
478
483
  */
479
484
  constructor(type, context) {
480
- if ('expression' in context) {
485
+ if (context && 'expression' in context) {
481
486
  super(type, context.expression);
482
487
  } else {
483
488
  super(type, null);
484
- this.#tableName = context.tableName;
485
489
  }
490
+ this.#context = context;
486
491
  }
487
492
 
488
493
  /**
@@ -491,10 +496,16 @@ class RowPick extends BasePick {
491
496
  * @returns {PickOf<T[K]>}
492
497
  */
493
498
  field(fieldName) {
494
- if (this.#tableName) {
499
+ if (!this.#context) {
495
500
  return makePick(
496
501
  this.type.properties[fieldName],
497
- $o.field(fieldName, this.#tableName)
502
+ $o.field(fieldName, null)
503
+ );
504
+ }
505
+ if ('tableName' in this.#context) {
506
+ return makePick(
507
+ this.type.properties[fieldName],
508
+ $o.field(fieldName, this.#context.tableName)
498
509
  );
499
510
  }
500
511
  return makePick(
@@ -514,16 +525,11 @@ class RowPick extends BasePick {
514
525
  }
515
526
 
516
527
  isRoot() {
517
- return Boolean(this.#tableName);
528
+ return Boolean(!this.#context || 'tableName' in this.#context);
518
529
  }
519
530
 
520
531
  toOptional() {
521
- return new RowPick(
522
- optionalObjectType(this.type),
523
- this.#tableName
524
- ? {tableName: this.#tableName}
525
- : {expression: this.expression}
526
- );
532
+ return new RowPick(optionalObjectType(this.type), this.#context);
527
533
  }
528
534
  }
529
535
 
@@ -22,6 +22,7 @@ const {
22
22
  } = require('./picks.js');
23
23
  const {joinOperators} = require('./join-operators.js');
24
24
  const {queryToSql} = require('./query-to-sql.js');
25
+ const partialObjectShape = require('./partial-object-shape.js');
25
26
 
26
27
  /**
27
28
  * @typedef {import("./operators.js").Operators} Operators
@@ -74,18 +75,108 @@ const {queryToSql} = require('./query-to-sql.js');
74
75
  */
75
76
 
76
77
  /**
77
- * @typedef {BasePick} SelectValue
78
+ * @template {BasePick} T
79
+ * @typedef {T["type"]} PickType
78
80
  */
79
81
  /**
80
- * @template T
81
- * @typedef {T extends SelectValue ? T["type"]: never} SelectValueType
82
+ * @typedef {[BasePick, ...BasePick[]]} SelectPicksTuple
83
+ * @typedef {Record<string, BasePick>} SelectPicksObject
82
84
  */
83
85
  /**
84
- * @typedef {Record<string, SelectValue> | [SelectValue, ...SelectValue[]]} SelectResult
86
+ * @typedef {SelectPicksObject | SelectPicksTuple} SelectPicks
85
87
  */
88
+
89
+ /**
90
+ * @template {SelectPicksTuple} T
91
+ * @typedef {{[K in keyof T]: PickType<T[K]>}} SelectTypeTuple
92
+ */
93
+ /**
94
+ * @template {SelectPicksTuple} T
95
+ * @param {T} selectPicks
96
+ * @returns {SelectTypeTuple<T>}
97
+ */
98
+ function selectTypeTuple(selectPicks) {
99
+ return /** @type {SelectTypeTuple<T>} */ (selectPicks.map(
100
+ (result) => result.type
101
+ ));
102
+ }
103
+
104
+ /**
105
+ * @template {SelectPicksObject} T
106
+ * @typedef {flatten<{[K in keyof T]: PickType<T[K]>}>} SelectTypeObject
107
+ */
108
+ /**
109
+ * @template {SelectPicksObject} T
110
+ * @param {T} selectPicks
111
+ * @returns {SelectTypeObject<T>}
112
+ */
113
+ function selectTypeObject(selectPicks) {
114
+ return /** @type {SelectTypeObject<T>} */ (Object.fromEntries(
115
+ Object.entries(selectPicks).map(([name, pick]) => [name, pick.type])
116
+ ));
117
+ }
118
+
119
+ /**
120
+ * @template {SelectPicks} T
121
+ * @typedef {T extends SelectPicksTuple ? {values: SelectTypeTuple<T>} : T extends SelectPicksObject ? {object: SelectTypeObject<T>} : never} SelectResultOf
122
+ */
123
+ /**
124
+ * @template {SelectPicks} T
125
+ * @param {T} selectPicks
126
+ * @returns {SelectResultOf<T>}
127
+ */
128
+ function selectResultOf(selectPicks) {
129
+ if (Array.isArray(selectPicks)) {
130
+ return /** @type {SelectResultOf<T>} */ ({
131
+ values: selectTypeTuple(selectPicks),
132
+ });
133
+ }
134
+ return /** @type {SelectResultOf<T>} */ ({
135
+ object: selectTypeObject(selectPicks),
136
+ });
137
+ }
138
+
139
+ /**
140
+ * @typedef {{value: AnyTypeOrShape}} SelectResultValue
141
+ * @typedef {{values: [AnyTypeOrShape, ...AnyTypeOrShape[]]}} SelectResultValues
142
+ * @typedef {{object: Record<string, AnyTypeOrShape>}} SelectResultObject
143
+ */
144
+
145
+ /**
146
+ * @typedef {SelectResultValue | SelectResultValues | SelectResultObject} SelectResult
147
+ */
148
+
149
+ /**
150
+ * @template {SelectResult} T
151
+ * @typedef {T extends SelectResultObject ? RowPick<T['object']> : null} SelectResultPick
152
+ */
153
+
86
154
  /**
87
155
  * @template {SelectResult} T
88
- * @typedef {{[K in keyof T]: t<SelectValueType<T[K]>>}} QueryResultOf
156
+ * @param {T} selectResult
157
+ * @returns {SelectResultPick<T>}
158
+ */
159
+ function selectResultPick(selectResult) {
160
+ if ('object' in selectResult) {
161
+ const rowPick = new RowPick(object(selectResult.object), null);
162
+ return /** @type {SelectResultPick<T>} */ (rowPick);
163
+ }
164
+ return /** @type {SelectResultPick<T>} */ (null);
165
+ }
166
+
167
+ /**
168
+ * @template {SelectResultValues['values']} T
169
+ * @typedef {{[K in keyof T]: t<T[K]>}} QueryReturnValues
170
+ */
171
+
172
+ /**
173
+ * @template {SelectResultObject['object']} T
174
+ * @typedef {flatten<{[K in keyof T]: t<T[K]>}>} QueryReturnObject
175
+ */
176
+
177
+ /**
178
+ * @template {SelectResult} T
179
+ * @typedef {T extends SelectResultValue ? t<T['value']> : T extends SelectResultValues ? QueryReturnValues<T['values']> : T extends SelectResultObject ? QueryReturnObject<T['object']> : never} QueryReturn
89
180
  */
90
181
 
91
182
  /**
@@ -157,6 +248,12 @@ function getTableName(table) {
157
248
  * @typedef {[...objs: WrapRowPick<T>, operators: typeof pickOperators]} FctArgs
158
249
  */
159
250
 
251
+ /**
252
+ * @template {[...ObjectShape[], ObjectShape]} T
253
+ * @template {SelectResult} Q
254
+ * @typedef {[...objs: WrapRowPick<T>, operators: typeof pickOperators, selectFields: SelectResultPick<Q>]} FctArgs2
255
+ */
256
+
160
257
  /**
161
258
  * @typedef {{
162
259
  * explain?: boolean,
@@ -164,7 +261,7 @@ function getTableName(table) {
164
261
  * from: QueryTable,
165
262
  * scope?: ObjectPick<any>
166
263
  * joins?: JoinsResult[],
167
- * select: SelectResult | '*',
264
+ * select: SelectPicks | '*',
168
265
  * selectOneField?: boolean,
169
266
  * distinct?: boolean,
170
267
  * where?: BooleanPick,
@@ -426,23 +523,26 @@ class FinalQuery {
426
523
 
427
524
  /**
428
525
  * @template {[...ObjectShape[], ObjectShape]} T
429
- * @template R
430
- * @extends {FinalQuery<R>}
526
+ * @template {SelectResult} S
527
+ * @extends {FinalQuery<QueryReturn<S>>}
431
528
  */
432
529
  class SelectQuery extends FinalQuery {
433
530
  #database;
434
531
  #picks;
532
+ #selectPick;
435
533
  #queryParts;
436
534
 
437
535
  /**
438
536
  * @param {WrapRowPick<T>} picks
537
+ * @param {SelectResultPick<S>} selectPick
439
538
  * @param {QueryParts<'from' | 'select'>} queryParts
440
539
  * @param {*} database
441
540
  */
442
- constructor(picks, queryParts, database) {
541
+ constructor(picks, selectPick, queryParts, database) {
443
542
  super(database, queryParts);
444
543
  this.#database = database;
445
544
  this.#picks = picks;
545
+ this.#selectPick = selectPick;
446
546
  this.#queryParts = queryParts;
447
547
  }
448
548
 
@@ -451,77 +551,107 @@ class SelectQuery extends FinalQuery {
451
551
  }
452
552
 
453
553
  /**
454
- * @returns {SelectQuery<T,R>}
554
+ * @returns {SelectQuery<T,S>}
455
555
  */
456
556
  distinct() {
457
557
  const queryParts = {
458
558
  ...this.#queryParts,
459
559
  distinct: true,
460
560
  };
461
- return new SelectQuery(this.#picks, queryParts, this.#database);
561
+ return new SelectQuery(
562
+ this.#picks,
563
+ this.#selectPick,
564
+ queryParts,
565
+ this.#database
566
+ );
462
567
  }
463
568
 
464
569
  /**
465
- * @param {(...args: FctArgs<T>) => BooleanPick} fct
466
- * @returns {SelectQuery<T,R>}
570
+ * @param {(...args: FctArgs2<T,S>) => BooleanPick} fct
571
+ * @returns {SelectQuery<T,S>}
467
572
  */
468
573
  where(fct) {
469
574
  const queryParts = {
470
575
  ...this.#queryParts,
471
576
  where: mergeWhere(
472
577
  this.#queryParts.where,
473
- fct(...this.#picks, pickOperators)
578
+ fct(...this.#picks, pickOperators, this.#selectPick)
474
579
  ),
475
580
  };
476
- return new SelectQuery(this.#picks, queryParts, this.#database);
581
+ return new SelectQuery(
582
+ this.#picks,
583
+ this.#selectPick,
584
+ queryParts,
585
+ this.#database
586
+ );
477
587
  }
478
588
 
479
589
  /**
480
- * @param {(...args: FctArgs<T>) => OrderByResult} fct
481
- * @returns {SelectQuery<T,R>}
590
+ * @param {(...args: FctArgs2<T,S>) => OrderByResult} fct
591
+ * @returns {SelectQuery<T,S>}
482
592
  */
483
593
  orderBy(fct) {
484
594
  const queryParts = {
485
595
  ...this.#queryParts,
486
- orderBy: fct(...this.#picks, pickOperators),
596
+ orderBy: fct(...this.#picks, pickOperators, this.#selectPick),
487
597
  };
488
- return new SelectQuery(this.#picks, queryParts, this.#database);
598
+ return new SelectQuery(
599
+ this.#picks,
600
+ this.#selectPick,
601
+ queryParts,
602
+ this.#database
603
+ );
489
604
  }
490
605
 
491
606
  /**
492
- * @param {(...args: FctArgs<T>) => GroupByResult} fct
493
- * @returns {SelectQuery<T,R>}
607
+ * @param {(...args: FctArgs2<T,S>) => GroupByResult} fct
608
+ * @returns {SelectQuery<T,S>}
494
609
  */
495
610
  groupBy(fct) {
496
611
  const queryParts = {
497
612
  ...this.#queryParts,
498
- groupBy: fct(...this.#picks, pickOperators),
613
+ groupBy: fct(...this.#picks, pickOperators, this.#selectPick),
499
614
  };
500
- return new SelectQuery(this.#picks, queryParts, this.#database);
615
+ return new SelectQuery(
616
+ this.#picks,
617
+ this.#selectPick,
618
+ queryParts,
619
+ this.#database
620
+ );
501
621
  }
502
622
 
503
623
  /**
504
624
  * @param {number} count
505
- * @returns {SelectQuery<T,R>}
625
+ * @returns {SelectQuery<T,S>}
506
626
  */
507
627
  limit(count) {
508
628
  const queryParts = {
509
629
  ...this.#queryParts,
510
630
  limit: count,
511
631
  };
512
- return new SelectQuery(this.#picks, queryParts, this.#database);
632
+ return new SelectQuery(
633
+ this.#picks,
634
+ this.#selectPick,
635
+ queryParts,
636
+ this.#database
637
+ );
513
638
  }
514
639
 
515
640
  /**
516
641
  * @param {number} count
517
- * @returns {SelectQuery<T,R>}
642
+ * @returns {SelectQuery<T,S>}
518
643
  */
519
644
  offset(count) {
520
645
  const queryParts = {
521
646
  ...this.#queryParts,
522
647
  offset: count,
523
648
  };
524
- return new SelectQuery(this.#picks, queryParts, this.#database);
649
+ return new SelectQuery(
650
+ this.#picks,
651
+ this.#selectPick,
652
+ queryParts,
653
+ this.#database
654
+ );
525
655
  }
526
656
  }
527
657
 
@@ -587,12 +717,6 @@ class FromQuery {
587
717
  );
588
718
  }
589
719
 
590
- /**
591
- * @template {AnyObjectShape} U
592
- * @template {[...ObjectShape[], ObjectShape]} V
593
- * @typedef {(tableName: string, shape: U, joinFct: (...args: FctArgs<AllShapes<T,U>>) => BooleanPick) => FromQuery<V>} JoinFunction
594
- */
595
-
596
720
  /**
597
721
  * @template {AnyObjectShape} U
598
722
  * @param {string} tableName
@@ -672,7 +796,7 @@ class FromQuery {
672
796
  * Select field
673
797
  * @template {keyof (T[0])} F
674
798
  * @param {F} field
675
- * @returns {SelectQuery<T, t<T[0][F]>>}
799
+ * @returns {SelectQuery<T, {value: T[0][F]}>}
676
800
  */
677
801
  field(field) {
678
802
  /** @type {RowPick<T[0]>} */
@@ -682,14 +806,14 @@ class FromQuery {
682
806
  select: {[field]: row.field(field)},
683
807
  selectOneField: true,
684
808
  };
685
- return new SelectQuery(this.#picks, queryParts, this.#database);
809
+ return new SelectQuery(this.#picks, null, queryParts, this.#database);
686
810
  }
687
811
 
688
812
  /**
689
813
  * Select fields
690
814
  * @template {(keyof T[0])[]} F
691
815
  * @param {F} fields
692
- * @returns {SelectQuery<T, t<Pick<T[0], F[number]>>>}
816
+ * @returns {SelectQuery<T, {object: Pick<T[0], F[number]>}>}
693
817
  */
694
818
  fields(fields) {
695
819
  /** @type {RowPick<T[0]>} */
@@ -700,43 +824,43 @@ class FromQuery {
700
824
  fields.map((fieldName) => [fieldName, row.field(fieldName)])
701
825
  ),
702
826
  };
703
- return new SelectQuery(this.#picks, queryParts, this.#database);
827
+ const shape = partialObjectShape(row.type, fields);
828
+ const selectPick = selectResultPick({object: shape});
829
+ return new SelectQuery(this.#picks, selectPick, queryParts, this.#database);
704
830
  }
705
831
 
706
832
  /**
707
- * @template {SelectResult} R
708
- * @param {(...args: FctArgs<T>) => R} fct
709
- * @returns {SelectQuery<T, QueryResultOf<R>>}
833
+ * @template {SelectPicks} U
834
+ * @param {(...args: FctArgs<T>) => U} fct
835
+ * @returns {SelectQuery<T, SelectResultOf<U>>}
710
836
  */
711
837
  select(fct) {
838
+ const selectPicks = fct(...this.#picks, pickOperators);
712
839
  const queryParts = {
713
840
  ...this.#queryParts,
714
- select: fct(...this.#picks, pickOperators),
841
+ select: selectPicks,
715
842
  };
716
- return new SelectQuery(this.#picks, queryParts, this.#database);
843
+ const selectPick = selectResultPick(selectResultOf(selectPicks));
844
+ return new SelectQuery(this.#picks, selectPick, queryParts, this.#database);
717
845
  }
718
846
 
719
847
  /**
720
- * @returns {SelectQuery<T, t<T[number]>>}
848
+ * @returns {SelectQuery<T, T[number]>}
721
849
  */
722
850
  selectAll() {
723
851
  const isRoot = this.#picks.every((pick) => pick.isRoot());
724
- /** @type {QueryObj["select"]} */
725
- let select;
726
- if (isRoot) {
727
- select = '*';
728
- } else {
729
- select = Object.fromEntries(
730
- this.#picks.flatMap((pick) =>
731
- Object.keys(pick.type.properties).map((key) => [key, pick.get(key)])
732
- )
733
- );
734
- }
852
+ const selectPicks = Object.fromEntries(
853
+ this.#picks.flatMap((pick) =>
854
+ Object.keys(pick.type.properties).map((key) => [key, pick.get(key)])
855
+ )
856
+ );
735
857
  const queryParts = {
736
858
  ...this.#queryParts,
737
- select,
859
+ select: isRoot ? /** @type {const} */ ('*') : selectPicks,
738
860
  };
739
- return new SelectQuery(this.#picks, queryParts, this.#database);
861
+ /** @type {RowPick<T[number]>} */
862
+ const selectPick = selectResultPick(selectResultOf(selectPicks));
863
+ return new SelectQuery(this.#picks, selectPick, queryParts, this.#database);
740
864
  }
741
865
 
742
866
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xcraft-core-pickaxe",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Query builder",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/sql.spec.js CHANGED
@@ -388,6 +388,45 @@ describe('xcraft.pickaxe', function () {
388
388
  expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
389
389
  });
390
390
 
391
+ it('reuse selected value', function () {
392
+ const builder = new QueryBuilder()
393
+ .from('test_table', TestUserShape)
394
+ .select((user, $) => ({
395
+ id: user.get('id'),
396
+ fullname: user.get('firstname').concat(user.get('lastname')),
397
+ kind: $.case(
398
+ [user.get('age').lt(13), 'kid'],
399
+ [user.get('age').lt(18), 'teenager'],
400
+ [user.get('age').lt(65), 'adult'],
401
+ 'elder'
402
+ ),
403
+ }))
404
+ .where((user, $, fields) =>
405
+ $.or(fields.get('fullname').length.gt(10), fields.get('kind').eq('kid'))
406
+ );
407
+
408
+ const result = queryToSql(builder.query, null);
409
+
410
+ const sql = `
411
+ SELECT
412
+ id,
413
+ (firstname || lastname) AS fullname,
414
+ CASE
415
+ WHEN age < 13 THEN 'kid'
416
+ WHEN age < 18 THEN 'teenager'
417
+ WHEN age < 65 THEN 'adult'
418
+ ELSE 'elder'
419
+ END AS kind
420
+ FROM test_table
421
+ WHERE (
422
+ LENGTH(fullname) > 10 OR
423
+ kind IS 'kid'
424
+ )
425
+ `;
426
+
427
+ expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
428
+ });
429
+
391
430
  it('join tables', function () {
392
431
  const builder = new QueryBuilder()
393
432
  .from('users', TestUserShape)