@webda/ql 4.0.0-beta.6 → 4.0.0-beta.8

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.
Files changed (52) hide show
  1. package/README.md +42 -4
  2. package/lib/WebdaQLLexer.d.ts +10 -9
  3. package/lib/WebdaQLLexer.d.ts.map +1 -1
  4. package/lib/WebdaQLLexer.js +227 -363
  5. package/lib/WebdaQLLexer.js.map +1 -1
  6. package/lib/WebdaQLParserLexer.d.ts +10 -9
  7. package/lib/WebdaQLParserLexer.d.ts.map +1 -1
  8. package/lib/WebdaQLParserLexer.js +227 -363
  9. package/lib/WebdaQLParserLexer.js.map +1 -1
  10. package/lib/WebdaQLParserListener.d.ts +26 -2
  11. package/lib/WebdaQLParserListener.d.ts.map +1 -1
  12. package/lib/WebdaQLParserParser.d.ts +34 -9
  13. package/lib/WebdaQLParserParser.d.ts.map +1 -1
  14. package/lib/WebdaQLParserParser.js +585 -732
  15. package/lib/WebdaQLParserParser.js.map +1 -1
  16. package/lib/WebdaQLParserVisitor.d.ts +16 -1
  17. package/lib/WebdaQLParserVisitor.d.ts.map +1 -1
  18. package/lib/aggregation-select.spec.js +4 -3
  19. package/lib/aggregation-select.spec.js.map +1 -1
  20. package/lib/completion.d.ts +84 -0
  21. package/lib/completion.d.ts.map +1 -0
  22. package/lib/completion.js +467 -0
  23. package/lib/completion.js.map +1 -0
  24. package/lib/completion.spec.d.ts +25 -0
  25. package/lib/completion.spec.d.ts.map +1 -0
  26. package/lib/completion.spec.js +409 -0
  27. package/lib/completion.spec.js.map +1 -0
  28. package/lib/filter.d.ts +54 -0
  29. package/lib/filter.d.ts.map +1 -0
  30. package/lib/filter.js +135 -0
  31. package/lib/filter.js.map +1 -0
  32. package/lib/filter.spec.d.ts +2 -0
  33. package/lib/filter.spec.d.ts.map +1 -0
  34. package/lib/filter.spec.js +124 -0
  35. package/lib/filter.spec.js.map +1 -0
  36. package/lib/index.d.ts +2 -0
  37. package/lib/index.d.ts.map +1 -1
  38. package/lib/index.js +2 -0
  39. package/lib/index.js.map +1 -1
  40. package/lib/now.spec.d.ts +2 -0
  41. package/lib/now.spec.d.ts.map +1 -0
  42. package/lib/now.spec.js +258 -0
  43. package/lib/now.spec.js.map +1 -0
  44. package/lib/query.d.ts +134 -8
  45. package/lib/query.d.ts.map +1 -1
  46. package/lib/query.js +314 -39
  47. package/lib/query.js.map +1 -1
  48. package/lib/query.spec.js +1 -0
  49. package/lib/query.spec.js.map +1 -1
  50. package/lib/statements.spec.js +1 -0
  51. package/lib/statements.spec.js.map +1 -1
  52. package/package.json +3 -2
package/lib/query.js CHANGED
@@ -4,6 +4,151 @@ import { WebdaQLLexer } from "./WebdaQLLexer.js";
4
4
  import { CountAllItemContext, CountDistinctItemContext, FieldItemContext, MetricItemContext, OrderFieldExpressionContext, SubExpressionContext, WebdaQLParserParser } from "./WebdaQLParserParser.js";
5
5
  import { checkAlias, validateAggregation } from "./aggregation-query.js";
6
6
  import { escapeValue, WebdaQLError } from "./webdaql-string.js";
7
+ /**
8
+ * A value computed at evaluation time
9
+ */
10
+ export class DynamicValue {
11
+ }
12
+ /**
13
+ * Dynamic value representing the current timestamp (in milliseconds since epoch) with an optional duration offset
14
+ */
15
+ export class NowValue extends DynamicValue {
16
+ offsetMs;
17
+ raw;
18
+ name = "NOW";
19
+ /**
20
+ * @param offsetMs - offset in milliseconds to add to Date.now()
21
+ * @param raw - original duration string literal (if parsed from one)
22
+ */
23
+ constructor(offsetMs = 0, raw) {
24
+ super();
25
+ this.offsetMs = offsetMs;
26
+ this.raw = raw;
27
+ }
28
+ /**
29
+ * Resolve to current timestamp + offset
30
+ * @returns timestamp in milliseconds
31
+ */
32
+ resolve() {
33
+ return Date.now() + this.offsetMs;
34
+ }
35
+ /**
36
+ * String representation of NOW function call
37
+ * @returns WQL NOW function string
38
+ */
39
+ toString() {
40
+ if (this.raw !== undefined && this.raw !== "") {
41
+ return `NOW('${this.raw}')`;
42
+ }
43
+ if (this.offsetMs !== 0) {
44
+ if (this.offsetMs % 1000 === 0) {
45
+ return `NOW('${this.offsetMs / 1000}s')`;
46
+ }
47
+ return `NOW('${this.offsetMs}ms')`;
48
+ }
49
+ return "NOW()";
50
+ }
51
+ }
52
+ /**
53
+ * Supported duration units and their factor in seconds
54
+ */
55
+ const DURATION_UNITS = {
56
+ s: 1,
57
+ sec: 1,
58
+ secs: 1,
59
+ second: 1,
60
+ seconds: 1,
61
+ m: 60,
62
+ min: 60,
63
+ mins: 60,
64
+ minute: 60,
65
+ minutes: 60,
66
+ h: 3600,
67
+ hr: 3600,
68
+ hour: 3600,
69
+ hours: 3600,
70
+ d: 86400,
71
+ day: 86400,
72
+ days: 86400,
73
+ mo: 86400 * 30,
74
+ mon: 86400 * 30,
75
+ month: 86400 * 30,
76
+ months: 86400 * 30,
77
+ y: 86400 * 365,
78
+ yr: 86400 * 365,
79
+ year: 86400 * 365,
80
+ years: 86400 * 365,
81
+ ms: 0.001,
82
+ msec: 0.001,
83
+ msecs: 0.001,
84
+ millisecond: 0.001,
85
+ milliseconds: 0.001
86
+ };
87
+ /**
88
+ * Parse a duration string or numeric seconds into milliseconds.
89
+ *
90
+ * Supports an optional leading sign (`+` or `-`), and composite parts like `1d2h`, `10m`, `30`, `1.5h`.
91
+ * Numeric inputs are treated as seconds.
92
+ *
93
+ * @param input - duration string or numeric seconds
94
+ * @returns duration offset in milliseconds
95
+ * @throws {WebdaQLError} if the format or unit is invalid
96
+ */
97
+ export function parseDurationToMs(input) {
98
+ if (typeof input === "number") {
99
+ if (!Number.isFinite(input)) {
100
+ throw new WebdaQLError(`Invalid duration: ${input}`);
101
+ }
102
+ return Math.round(input * 1000);
103
+ }
104
+ const raw = String(input).trim();
105
+ if (!raw) {
106
+ throw new WebdaQLError("Duration value cannot be empty");
107
+ }
108
+ let sign = 1;
109
+ let rest = raw;
110
+ if (rest.startsWith("-")) {
111
+ sign = -1;
112
+ rest = rest.slice(1).trim();
113
+ }
114
+ else if (rest.startsWith("+")) {
115
+ rest = rest.slice(1).trim();
116
+ }
117
+ if (!rest) {
118
+ throw new WebdaQLError(`Invalid duration format: ${raw}`);
119
+ }
120
+ const re = /(\d+(?:\.\d+)?)\s*([a-zA-Z]+)?/g;
121
+ let totalSeconds = 0;
122
+ let matched = false;
123
+ let lastIndex = 0;
124
+ let m;
125
+ while ((m = re.exec(rest)) !== null) {
126
+ const skipped = rest.slice(lastIndex, m.index).trim();
127
+ if (skipped) {
128
+ throw new WebdaQLError(`Invalid duration format: ${raw}`);
129
+ }
130
+ matched = true;
131
+ lastIndex = re.lastIndex;
132
+ const num = parseFloat(m[1]);
133
+ const unitStr = m[2];
134
+ if (unitStr) {
135
+ const u = unitStr.toLowerCase();
136
+ const factor = DURATION_UNITS[u];
137
+ if (factor === undefined) {
138
+ throw new WebdaQLError(`Unsupported duration unit: ${unitStr}`);
139
+ }
140
+ totalSeconds += num * factor;
141
+ }
142
+ else {
143
+ totalSeconds += num;
144
+ }
145
+ }
146
+ const trailing = rest.slice(lastIndex).trim();
147
+ if (!matched || trailing) {
148
+ throw new WebdaQLError(`Invalid duration format: ${raw}`);
149
+ }
150
+ return Math.round(sign * totalSeconds * 1000);
151
+ }
7
152
  /**
8
153
  * Strip the quotes of a WebdaQL string literal and unescape its contents.
9
154
  *
@@ -151,8 +296,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
151
296
  * @param ctx - the order expression context
152
297
  */
153
298
  visitOrderExpression(ctx) {
154
- this.orderBy = ctx.children
155
- ?.filter(c => c instanceof OrderFieldExpressionContext)
299
+ this.orderBy = (ctx.children ?? [])
300
+ .filter(c => c instanceof OrderFieldExpressionContext)
156
301
  .map((c) => this.visitOrderFieldExpression(c));
157
302
  }
158
303
  /**
@@ -161,7 +306,9 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
161
306
  * @returns the built Query
162
307
  */
163
308
  visitWebdaql(ctx) {
164
- return ctx.statement() ? this.visitStatement(ctx.statement()) : this.visitFilterQuery(ctx.filterQuery());
309
+ const statement = ctx.statement();
310
+ const filter = ctx.filterQuery();
311
+ return statement ? this.visitStatement(statement) : filter ? this.visitFilterQuery(filter) : this.defaultResult();
165
312
  }
166
313
  /**
167
314
  * Visit a DELETE, UPDATE or SELECT statement
@@ -242,9 +389,19 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
242
389
  * @returns the assignment
243
390
  */
244
391
  visitAssignment(ctx) {
392
+ const raw = this.visit(ctx.getChild(2));
393
+ if (raw instanceof DynamicValue) {
394
+ return {
395
+ field: ctx.identifier().text,
396
+ rawValue: raw,
397
+ get value() {
398
+ return raw.resolve();
399
+ }
400
+ };
401
+ }
245
402
  return {
246
403
  field: ctx.identifier().text,
247
- value: this.visit(ctx.getChild(2))
404
+ value: raw
248
405
  };
249
406
  }
250
407
  /**
@@ -327,8 +484,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
327
484
  */
328
485
  getComparison(ctx) {
329
486
  const res = [];
330
- // eslint-disable-next-line prefer-const
331
- let [left, _, right] = ctx.children;
487
+ let left = ctx.getChild(0);
488
+ let right = ctx.getChild(2);
332
489
  if (right instanceof SubExpressionContext) {
333
490
  right = right.getChild(1);
334
491
  }
@@ -366,7 +523,9 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
366
523
  * @returns the comparison expression
367
524
  */
368
525
  visitBinaryComparisonExpression(ctx) {
369
- const [left, op, right] = ctx.children;
526
+ const left = ctx.getChild(0);
527
+ const op = ctx.getChild(1);
528
+ const right = ctx.getChild(2);
370
529
  // @ts-ignore
371
530
  return new ComparisonExpression(op.text, left.text, this.visit(right));
372
531
  }
@@ -376,7 +535,7 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
376
535
  * @returns the array of values
377
536
  */
378
537
  visitSetExpression(ctx) {
379
- return ctx.children.filter((_i, id) => id % 2).map(c => this.visit(c));
538
+ return (ctx.children ?? []).filter((_i, id) => id % 2).map(c => this.visit(c));
380
539
  }
381
540
  /**
382
541
  * Visit a LIKE expression (e.g. `field LIKE '%pattern_'`)
@@ -384,7 +543,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
384
543
  * @returns the comparison expression
385
544
  */
386
545
  visitLikeExpression(ctx) {
387
- const [left, _, right] = ctx.children;
546
+ const left = ctx.getChild(0);
547
+ const right = ctx.getChild(2);
388
548
  const value = this.visit(right);
389
549
  return new ComparisonExpression("LIKE", left.text, value);
390
550
  }
@@ -394,7 +554,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
394
554
  * @returns the comparison expression
395
555
  */
396
556
  visitInExpression(ctx) {
397
- const [left, _, right] = ctx.children;
557
+ const left = ctx.getChild(0);
558
+ const right = ctx.getChild(2);
398
559
  const value = this.visit(right);
399
560
  return new ComparisonExpression("IN", left.text, value);
400
561
  }
@@ -404,7 +565,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
404
565
  * @returns the comparison expression
405
566
  */
406
567
  visitContainsExpression(ctx) {
407
- const [left, _, right] = ctx.children;
568
+ const left = ctx.getChild(0);
569
+ const right = ctx.getChild(2);
408
570
  const value = this.visit(right);
409
571
  return new ComparisonExpression("CONTAINS", left.text, value);
410
572
  }
@@ -480,6 +642,34 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
480
642
  visitNumberLiteral(ctx) {
481
643
  return parseFloat(ctx.text);
482
644
  }
645
+ /**
646
+ * Visit a NOW() function call with optional duration offset
647
+ * @param ctx - the now function context
648
+ * @returns the NowValue
649
+ */
650
+ visitNowFunction(ctx) {
651
+ const stringLiteral = ctx.stringLiteral();
652
+ if (stringLiteral) {
653
+ const raw = unescapeStringLiteral(stringLiteral.text);
654
+ const offsetMs = parseDurationToMs(raw);
655
+ return new NowValue(offsetMs, raw);
656
+ }
657
+ const integerLiteral = ctx.integerLiteral();
658
+ if (integerLiteral) {
659
+ const seconds = parseInt(integerLiteral.text);
660
+ return new NowValue(seconds * 1000, integerLiteral.text);
661
+ }
662
+ const numberLiteral = ctx.numberLiteral();
663
+ if (numberLiteral) {
664
+ const seconds = parseFloat(numberLiteral.text);
665
+ return new NowValue(Math.round(seconds * 1000), numberLiteral.text);
666
+ }
667
+ const parameter = ctx.parameter();
668
+ if (parameter) {
669
+ return this.visitParameter(parameter);
670
+ }
671
+ return new NowValue(0);
672
+ }
483
673
  /**
484
674
  * A `?` or `:name` placeholder reaching evaluation was never bound
485
675
  * @param ctx - the parameter context
@@ -514,9 +704,9 @@ export const UNARY_COMPARISON_OPERATORS = ["IS NULL", "IS NOT NULL"];
514
704
  */
515
705
  export class ComparisonExpression extends Expression {
516
706
  /**
517
- * Right side of the comparison
707
+ * Right side of the comparison (raw parsed value, possibly DynamicValue)
518
708
  */
519
- value;
709
+ rawValue;
520
710
  /**
521
711
  * Attribute to read from the object (split by .)
522
712
  */
@@ -524,13 +714,45 @@ export class ComparisonExpression extends Expression {
524
714
  /**
525
715
  * @param operator - comparison operator
526
716
  * @param attribute - dot-notation path to the object property (e.g. `"user.name"`)
527
- * @param value - literal value or array of values (for IN operator) to compare against
717
+ * @param rawValue - literal value, DynamicValue, or array of values (for IN operator) to compare against
528
718
  */
529
- constructor(operator, attribute, value) {
719
+ constructor(operator, attribute, rawValue) {
530
720
  super(operator);
531
- this.value = value;
721
+ this.rawValue = rawValue;
532
722
  this.attribute = attribute.split(".");
533
723
  }
724
+ /**
725
+ * Resolved value at evaluation time
726
+ * @returns the resolved scalar or array value
727
+ */
728
+ get value() {
729
+ if (this.rawValue instanceof DynamicValue) {
730
+ return this.rawValue.resolve();
731
+ }
732
+ if (Array.isArray(this.rawValue)) {
733
+ return this.rawValue.map(v => (v instanceof DynamicValue ? v.resolve() : v));
734
+ }
735
+ return this.rawValue;
736
+ }
737
+ /**
738
+ * Set the raw value
739
+ * @param val - new value
740
+ */
741
+ set value(val) {
742
+ this.rawValue = val;
743
+ }
744
+ /**
745
+ * @override
746
+ */
747
+ isDynamic() {
748
+ if (this.rawValue instanceof DynamicValue) {
749
+ return true;
750
+ }
751
+ if (Array.isArray(this.rawValue)) {
752
+ return this.rawValue.some(v => v instanceof DynamicValue);
753
+ }
754
+ return false;
755
+ }
534
756
  /**
535
757
  * Convert a SQL LIKE pattern to a JavaScript RegExp
536
758
  *
@@ -597,19 +819,27 @@ export class ComparisonExpression extends Expression {
597
819
  * @override
598
820
  */
599
821
  eval(target) {
600
- const left = ComparisonExpression.getAttributeValue(target, this.attribute);
822
+ let left = ComparisonExpression.getAttributeValue(target, this.attribute);
823
+ // WebdaQL has no date literal: a date compared with a string compares as its ISO 8601 form, the way it is
824
+ // serialized; comparisons with numbers keep the native Date behavior
825
+ if (left instanceof Date &&
826
+ !isNaN(left.getTime()) &&
827
+ this.operator !== "LIKE" &&
828
+ (typeof this.value === "string" || (Array.isArray(this.value) && this.value.some(v => typeof v === "string")))) {
829
+ left = left.toISOString();
830
+ }
601
831
  switch (this.operator) {
602
832
  case "=":
603
833
  // ignore strong type on purpose
604
834
  return left == this.value;
605
835
  case "<=":
606
- return left <= this.value;
836
+ return left <= (this.value ?? 0);
607
837
  case ">=":
608
- return left >= this.value;
838
+ return left >= (this.value ?? 0);
609
839
  case "<":
610
- return left < this.value;
840
+ return left < (this.value ?? 0);
611
841
  case ">":
612
- return left > this.value;
842
+ return left > (this.value ?? 0);
613
843
  case "!=":
614
844
  return left != this.value;
615
845
  case "LIKE":
@@ -638,10 +868,14 @@ export class ComparisonExpression extends Expression {
638
868
  * Serialize a value to its WebdaQL string representation
639
869
  *
640
870
  * Strings are double-quoted, booleans are uppercased, arrays are bracket-wrapped.
871
+ * Dynamic values serialize to their canonical function call string (e.g. `NOW('-10m')`).
641
872
  * @param value - the value to serialize
642
873
  * @returns the serialized string
643
874
  */
644
875
  toStringValue(value) {
876
+ if (value instanceof DynamicValue) {
877
+ return value.toString();
878
+ }
645
879
  if (Array.isArray(value)) {
646
880
  return `[${value.map(v => this.toStringValue(v)).join(", ")}]`;
647
881
  }
@@ -651,7 +885,7 @@ export class ComparisonExpression extends Expression {
651
885
  case "boolean":
652
886
  return value.toString().toUpperCase();
653
887
  }
654
- return value?.toString();
888
+ return value === null ? "NULL" : value.toString();
655
889
  }
656
890
  /**
657
891
  * Allow subclass to create different display
@@ -674,7 +908,7 @@ export class ComparisonExpression extends Expression {
674
908
  if (UNARY_COMPARISON_OPERATORS.includes(this.operator)) {
675
909
  return `${this.toStringAttribute()} ${this.toStringOperator()}`;
676
910
  }
677
- return `${this.toStringAttribute()} ${this.toStringOperator()} ${this.toStringValue(this.value)}`;
911
+ return `${this.toStringAttribute()} ${this.toStringOperator()} ${this.toStringValue(this.rawValue)}`;
678
912
  }
679
913
  }
680
914
  /**
@@ -704,6 +938,12 @@ export class LogicalExpression extends Expression {
704
938
  }
705
939
  return this.children.map(c => c.toString(depth + 1)).join(` ${this.operator} `);
706
940
  }
941
+ /**
942
+ * @override
943
+ */
944
+ isDynamic() {
945
+ return this.children.some(c => c.isDynamic());
946
+ }
707
947
  }
708
948
  /**
709
949
  * Logical AND expression — evaluates to `true` only if all children are `true`
@@ -786,6 +1026,12 @@ export class BooleanExpression extends Expression {
786
1026
  toString() {
787
1027
  return this.operator;
788
1028
  }
1029
+ /**
1030
+ * @override
1031
+ */
1032
+ isDynamic() {
1033
+ return false;
1034
+ }
789
1035
  }
790
1036
  /**
791
1037
  * Build an AND / OR expression, folding `TRUE` / `FALSE` constants away
@@ -839,18 +1085,32 @@ function throwingErrorListener(sql) {
839
1085
  };
840
1086
  }
841
1087
  /**
842
- * Create a lexer and a parser for a query, both throwing on errors
1088
+ * Create a lexer and a parser for a query
843
1089
  * @param sql - the query string
844
- * @returns the lexer and the parser
1090
+ * @param options - parser options
1091
+ * @returns the lexer, parser and token stream
845
1092
  */
846
- function createParser(sql) {
1093
+ export function createParser(sql, options) {
847
1094
  const lexer = new WebdaQLLexer(CharStreams.fromString(sql || ""));
848
1095
  lexer.removeErrorListeners();
849
- lexer.addErrorListener(throwingErrorListener(sql));
850
- const parser = new WebdaQLParserParser(new CommonTokenStream(lexer));
1096
+ if (!options?.tolerant) {
1097
+ lexer.addErrorListener(throwingErrorListener(sql));
1098
+ }
1099
+ const tokens = new CommonTokenStream(lexer);
1100
+ const parser = new WebdaQLParserParser(tokens);
851
1101
  parser.removeErrorListeners();
852
- parser.addErrorListener(throwingErrorListener(sql));
853
- return { lexer, parser };
1102
+ if (!options?.tolerant) {
1103
+ parser.addErrorListener(throwingErrorListener(sql));
1104
+ }
1105
+ return { lexer, parser, tokens };
1106
+ }
1107
+ /**
1108
+ * Create a tolerant lexer and parser that do not throw on syntax errors
1109
+ * @param sql - the query string
1110
+ * @returns the lexer, parser and token stream
1111
+ */
1112
+ export function createTolerantParser(sql) {
1113
+ return createParser(sql, { tolerant: true });
854
1114
  }
855
1115
  /**
856
1116
  * Check a query against the WebdaQL grammar without evaluating it
@@ -942,7 +1202,7 @@ export class QueryValidator {
942
1202
  * @param type - logical operator to combine filters (`"AND"` or `"OR"`)
943
1203
  * @returns `this` for chaining
944
1204
  */
945
- merge(query, type = "AND") {
1205
+ merge(query = "", type = "AND") {
946
1206
  const adds = new QueryValidator(query);
947
1207
  if (adds.query.type !== "SELECT" || adds.query.fields) {
948
1208
  throw new SyntaxError(`Only a filter can be merged into a query, not a statement (Query: ${query})`);
@@ -1010,6 +1270,13 @@ export class QueryValidator {
1010
1270
  eval(target) {
1011
1271
  return this.query.filter.eval(target);
1012
1272
  }
1273
+ /**
1274
+ * Check whether this query's filter contains any dynamic values
1275
+ * @returns true if any child comparison holds a DynamicValue
1276
+ */
1277
+ isDynamic() {
1278
+ return this.query.filter.isDynamic();
1279
+ }
1013
1280
  /**
1014
1281
  * Display parse tree back as query
1015
1282
  * @param tree - the parse tree to display
@@ -1205,7 +1472,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
1205
1472
  * @returns the partial comparison expression
1206
1473
  */
1207
1474
  visitLikeExpression(ctx) {
1208
- const [left, _, right] = ctx.children;
1475
+ const left = ctx.getChild(0);
1476
+ const right = ctx.getChild(2);
1209
1477
  const value = this.visit(right);
1210
1478
  return new PartialComparisonExpression(this, "LIKE", left.text, value);
1211
1479
  }
@@ -1215,7 +1483,9 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
1215
1483
  * @returns the partial comparison expression
1216
1484
  */
1217
1485
  visitBinaryComparisonExpression(ctx) {
1218
- const [left, op, right] = ctx.children;
1486
+ const left = ctx.getChild(0);
1487
+ const op = ctx.getChild(1);
1488
+ const right = ctx.getChild(2);
1219
1489
  // @ts-ignore
1220
1490
  return new PartialComparisonExpression(this, op.text, left.text, this.visit(right));
1221
1491
  }
@@ -1225,7 +1495,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
1225
1495
  * @returns the partial comparison expression
1226
1496
  */
1227
1497
  visitInExpression(ctx) {
1228
- const [left, _, right] = ctx.children;
1498
+ const left = ctx.getChild(0);
1499
+ const right = ctx.getChild(2);
1229
1500
  const value = this.visit(right);
1230
1501
  return new PartialComparisonExpression(this, "IN", left.text, value);
1231
1502
  }
@@ -1235,7 +1506,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
1235
1506
  * @returns the partial comparison expression
1236
1507
  */
1237
1508
  visitContainsExpression(ctx) {
1238
- const [left, _, right] = ctx.children;
1509
+ const left = ctx.getChild(0);
1510
+ const right = ctx.getChild(2);
1239
1511
  const value = this.visit(right);
1240
1512
  return new PartialComparisonExpression(this, "CONTAINS", left.text, value);
1241
1513
  }
@@ -1282,7 +1554,7 @@ export function stringifyQuery(query) {
1282
1554
  head = "DELETE";
1283
1555
  }
1284
1556
  else if (query.type === "UPDATE") {
1285
- head = `UPDATE SET ${(query.assignments ?? []).map(a => new ComparisonExpression("=", a.field, a.value).toString()).join(", ")}`;
1557
+ head = `UPDATE SET ${(query.assignments ?? []).map(a => new ComparisonExpression("=", a.field, a.rawValue ?? a.value).toString()).join(", ")}`;
1286
1558
  }
1287
1559
  else if (query.aggregation) {
1288
1560
  const metrics = Object.entries(query.aggregation.metrics).map(([alias, metric]) => {
@@ -1321,7 +1593,7 @@ const FIELD_SEGMENT = /^[A-Za-z0-9_]+$/;
1321
1593
  * @returns the dotted path
1322
1594
  * @throws {WebdaQLError} when a segment is empty or holds anything but letters, digits and `_`
1323
1595
  */
1324
- function checkFieldPath(field) {
1596
+ export function checkFieldPath(field) {
1325
1597
  // Copy first: what is checked is what is printed, whatever the object does on later reads
1326
1598
  const segments = Array.isArray(field) ? snapshot(field) : typeof field === "string" ? field.split(".") : undefined;
1327
1599
  if (!segments?.length || segments.some(segment => typeof segment !== "string" || !FIELD_SEGMENT.test(segment))) {
@@ -1349,6 +1621,9 @@ function snapshot(value) {
1349
1621
  * @throws {WebdaQLError} for anything but a string, a finite number or a boolean
1350
1622
  */
1351
1623
  function checkScalar(value) {
1624
+ if (value instanceof DynamicValue) {
1625
+ return value.toString();
1626
+ }
1352
1627
  if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
1353
1628
  throw new WebdaQLError(`Invalid value in WebdaQL query: ${typeof value}`);
1354
1629
  }
@@ -1387,7 +1662,7 @@ function printExpression(expression, nested) {
1387
1662
  if (expression instanceof ComparisonExpression) {
1388
1663
  const attribute = checkFieldPath(expression.attribute);
1389
1664
  const operator = expression.operator;
1390
- const value = expression.value;
1665
+ const value = expression.rawValue !== undefined ? expression.rawValue : expression.value;
1391
1666
  if (typeof operator !== "string") {
1392
1667
  throw new WebdaQLError("Invalid operator in WebdaQL query");
1393
1668
  }
@@ -1448,7 +1723,7 @@ export function normalizeQuery(query) {
1448
1723
  head = `UPDATE SET ${assignments
1449
1724
  .map(a => {
1450
1725
  const field = a?.field;
1451
- const value = a?.value;
1726
+ const value = a?.rawValue ?? a?.value;
1452
1727
  return `${checkFieldPath(field)} = ${checkScalar(value)}`;
1453
1728
  })
1454
1729
  .join(", ")}`;