@malloydata/malloy-query-builder 0.0.237-dev250225144145

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.
@@ -0,0 +1,4654 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import * as Malloy from '@malloydata/malloy-interfaces';
8
+ import {Tag, TagSetValue} from '@malloydata/malloy-tag';
9
+ import * as Filter from '@malloydata/malloy-filter';
10
+
11
+ export type ParsedFilter =
12
+ | {kind: 'string'; clauses: Filter.StringClause[]}
13
+ | {kind: 'number'; clauses: Filter.NumberClause[]}
14
+ | {kind: 'boolean'; clauses: Filter.BooleanClause[]}
15
+ | {kind: 'date'; clauses: Filter.DateClause[]};
16
+
17
+ export type PathSegment = number | string;
18
+ export type Path = PathSegment[];
19
+
20
+ type ASTAny = ASTNode<unknown>;
21
+
22
+ type ASTChildren<T> = {
23
+ [Key in keyof T]: LiteralOrNode<T[Key]>;
24
+ };
25
+
26
+ type NonOptionalASTNode<T> = T extends undefined ? never : ASTNode<T>;
27
+
28
+ type LiteralOrNode<T> = T extends string
29
+ ? T
30
+ : T extends number
31
+ ? T
32
+ : T extends string[]
33
+ ? T
34
+ : T extends boolean
35
+ ? T
36
+ : undefined extends T
37
+ ? NonOptionalASTNode<T> | undefined
38
+ : ASTNode<T>;
39
+
40
+ abstract class ASTNode<T> {
41
+ /**
42
+ * @internal
43
+ */
44
+ edited = false;
45
+
46
+ /**
47
+ * @internal
48
+ */
49
+ public _parent: ASTAny | undefined;
50
+
51
+ abstract build(): T;
52
+
53
+ /**
54
+ * @internal
55
+ */
56
+ edit() {
57
+ this.edited = true;
58
+ if (this._parent) this._parent.edit();
59
+ return this;
60
+ }
61
+
62
+ /**
63
+ * @internal
64
+ */
65
+ abstract find(path: Path): ASTAny;
66
+
67
+ /**
68
+ * Returns this node as an `ASTQuery`. Throws if it is not an `ASTQuery`.
69
+ *
70
+ * There are variants of this method for _all_ ASTXYZ nodes `asXYZ`, but they
71
+ * are not shown here so the docs aren't crazy big.
72
+ *
73
+ * @returns Returns this node as an `ASTQuery`.
74
+ */
75
+ asQuery(): ASTQuery {
76
+ if (this instanceof ASTQuery) return this;
77
+ throw new Error('Not an ASTQuery');
78
+ }
79
+
80
+ /**
81
+ * Finds the AST node at the given `path`. Throws if it is not an `ASTQuery`.
82
+ *
83
+ * There are variants of this method for _all_ ASTXYZ nodes `findXYZ`, but they
84
+ * are not shown here so the docs aren't crazy big.
85
+ *
86
+ * @param path Path to the desired ASTNode, e.g. `['source', 'parameters', 0]`
87
+ * @returns Returns this node as an `ASTQuery`.
88
+ */
89
+ findQuery(path: Path): ASTQuery {
90
+ return this.find(path).asQuery();
91
+ }
92
+
93
+ /**
94
+ * @hidden
95
+ */
96
+ asReference(): ASTReference {
97
+ if (this instanceof ASTReference) return this;
98
+ throw new Error('Not an ASTReference');
99
+ }
100
+
101
+ /**
102
+ * @hidden
103
+ */
104
+ findReference(path: Path): ASTReference {
105
+ return this.find(path).asReference();
106
+ }
107
+
108
+ /**
109
+ * @hidden
110
+ */
111
+ asSourceReference(): ASTSourceReference {
112
+ if (this instanceof ASTSourceReference) return this;
113
+ throw new Error('Not an ASTSourceReference');
114
+ }
115
+
116
+ /**
117
+ * @hidden
118
+ */
119
+ findSourceReference(path: Path): ASTSourceReference {
120
+ return this.find(path).asSourceReference();
121
+ }
122
+
123
+ /**
124
+ * @hidden
125
+ */
126
+ asParameterValueList(): ASTParameterValueList {
127
+ if (this instanceof ASTParameterValueList) return this;
128
+ throw new Error('Not an ASTParameterValueList');
129
+ }
130
+
131
+ /**
132
+ * @hidden
133
+ */
134
+ findParameterValueList(path: Path): ASTParameterValueList {
135
+ return this.find(path).asParameterValueList();
136
+ }
137
+
138
+ /**
139
+ * @hidden
140
+ */
141
+ asWhere(): ASTWhere {
142
+ if (this instanceof ASTWhere) return this;
143
+ throw new Error('Not an ASTWhere');
144
+ }
145
+
146
+ /**
147
+ * @hidden
148
+ */
149
+ findWhere(path: Path): ASTWhere {
150
+ return this.find(path).asWhere();
151
+ }
152
+
153
+ /**
154
+ * @hidden
155
+ */
156
+ asWhereList(): ASTWhereList {
157
+ if (this instanceof ASTWhereList) return this;
158
+ throw new Error('Not an ASTWhereList');
159
+ }
160
+
161
+ /**
162
+ * @hidden
163
+ */
164
+ findWhereList(path: Path): ASTWhereList {
165
+ return this.find(path).asWhereList();
166
+ }
167
+
168
+ /**
169
+ * @hidden
170
+ */
171
+ asParameterValue(): ASTParameterValue {
172
+ if (this instanceof ASTParameterValue) return this;
173
+ throw new Error('Not an ASTParameterValue');
174
+ }
175
+
176
+ /**
177
+ * @hidden
178
+ */
179
+ findParameterValue(path: Path): ASTParameterValue {
180
+ return this.find(path).asParameterValue();
181
+ }
182
+
183
+ /**
184
+ * @hidden
185
+ */
186
+ asStringLiteralValue(): ASTStringLiteralValue {
187
+ if (this instanceof ASTStringLiteralValue) return this;
188
+ throw new Error('Not an ASTStringLiteralValue');
189
+ }
190
+
191
+ /**
192
+ * @hidden
193
+ */
194
+ findStringLiteralValue(path: Path): ASTStringLiteralValue {
195
+ return this.find(path).asStringLiteralValue();
196
+ }
197
+
198
+ /**
199
+ * @hidden
200
+ */
201
+ asNumberLiteralValue(): ASTNumberLiteralValue {
202
+ if (this instanceof ASTNumberLiteralValue) return this;
203
+ throw new Error('Not an ASTNumberLiteralValue');
204
+ }
205
+
206
+ /**
207
+ * @hidden
208
+ */
209
+ findNumberLiteralValue(path: Path): ASTNumberLiteralValue {
210
+ return this.find(path).asNumberLiteralValue();
211
+ }
212
+
213
+ /**
214
+ * @hidden
215
+ */
216
+ asViewOperationList(): ASTViewOperationList {
217
+ if (this instanceof ASTViewOperationList) return this;
218
+ throw new Error('Not an ASTViewOperationList');
219
+ }
220
+
221
+ /**
222
+ * @hidden
223
+ */
224
+ findViewOperationList(path: Path): ASTViewOperationList {
225
+ return this.find(path).asViewOperationList();
226
+ }
227
+
228
+ /**
229
+ * @hidden
230
+ */
231
+ asGroupByViewOperation(): ASTGroupByViewOperation {
232
+ if (this instanceof ASTGroupByViewOperation) return this;
233
+ throw new Error('Not an ASTGroupByViewOperation');
234
+ }
235
+
236
+ /**
237
+ * @hidden
238
+ */
239
+ findGroupByViewOperation(path: Path): ASTGroupByViewOperation {
240
+ return this.find(path).asGroupByViewOperation();
241
+ }
242
+
243
+ /**
244
+ * @hidden
245
+ */
246
+ asAggregateViewOperation(): ASTAggregateViewOperation {
247
+ if (this instanceof ASTAggregateViewOperation) return this;
248
+ throw new Error('Not an ASTAggregateViewOperation');
249
+ }
250
+
251
+ /**
252
+ * @hidden
253
+ */
254
+ findAggregateViewOperation(path: Path): ASTAggregateViewOperation {
255
+ return this.find(path).asAggregateViewOperation();
256
+ }
257
+
258
+ /**
259
+ * @hidden
260
+ */
261
+ asOrderByViewOperation(): ASTOrderByViewOperation {
262
+ if (this instanceof ASTOrderByViewOperation) return this;
263
+ throw new Error('Not an ASTOrderByViewOperation');
264
+ }
265
+
266
+ /**
267
+ * @hidden
268
+ */
269
+ findOrderByViewOperation(path: Path): ASTOrderByViewOperation {
270
+ return this.find(path).asOrderByViewOperation();
271
+ }
272
+
273
+ /**
274
+ * @hidden
275
+ */
276
+ asField(): ASTField {
277
+ if (this instanceof ASTField) return this;
278
+ throw new Error('Not an ASTField');
279
+ }
280
+
281
+ /**
282
+ * @hidden
283
+ */
284
+ findField(path: Path): ASTField {
285
+ return this.find(path).asField();
286
+ }
287
+
288
+ /**
289
+ * @hidden
290
+ */
291
+ asReferenceExpression(): ASTReferenceExpression {
292
+ if (this instanceof ASTReferenceExpression) return this;
293
+ throw new Error('Not an ASTReferenceExpression');
294
+ }
295
+
296
+ /**
297
+ * @hidden
298
+ */
299
+ findReferenceExpression(path: Path): ASTReferenceExpression {
300
+ return this.find(path).asReferenceExpression();
301
+ }
302
+
303
+ /**
304
+ * @hidden
305
+ */
306
+ asReferenceViewDefinition(): ASTReferenceViewDefinition {
307
+ if (this instanceof ASTReferenceViewDefinition) return this;
308
+ throw new Error('Not an ASTReferenceViewDefinition');
309
+ }
310
+
311
+ /**
312
+ * @hidden
313
+ */
314
+ findReferenceViewDefinition(path: Path): ASTReferenceViewDefinition {
315
+ return this.find(path).asReferenceViewDefinition();
316
+ }
317
+
318
+ /**
319
+ * @hidden
320
+ */
321
+ asArrowQueryDefinition(): ASTArrowQueryDefinition {
322
+ if (this instanceof ASTArrowQueryDefinition) return this;
323
+ throw new Error('Not an ASTArrowQueryDefinition');
324
+ }
325
+
326
+ /**
327
+ * @hidden
328
+ */
329
+ findArrowQueryDefinition(path: Path): ASTArrowQueryDefinition {
330
+ return this.find(path).asArrowQueryDefinition();
331
+ }
332
+
333
+ /**
334
+ * @hidden
335
+ */
336
+ asArrowViewDefinition(): ASTArrowViewDefinition {
337
+ if (this instanceof ASTArrowViewDefinition) return this;
338
+ throw new Error('Not an ASTArrowViewDefinition');
339
+ }
340
+
341
+ /**
342
+ * @hidden
343
+ */
344
+ findArrowViewDefinition(path: Path): ASTArrowViewDefinition {
345
+ return this.find(path).asArrowViewDefinition();
346
+ }
347
+
348
+ /**
349
+ * @hidden
350
+ */
351
+ asRefinementViewDefinition(): ASTRefinementViewDefinition {
352
+ if (this instanceof ASTRefinementViewDefinition) return this;
353
+ throw new Error('Not an ASTRefinementViewDefinition');
354
+ }
355
+
356
+ /**
357
+ * @hidden
358
+ */
359
+ findRefinementViewDefinition(path: Path): ASTRefinementViewDefinition {
360
+ return this.find(path).asRefinementViewDefinition();
361
+ }
362
+
363
+ /**
364
+ * @hidden
365
+ */
366
+ asTimeTruncationExpression(): ASTTimeTruncationExpression {
367
+ if (this instanceof ASTTimeTruncationExpression) return this;
368
+ throw new Error('Not an ASTTimeTruncationExpression');
369
+ }
370
+
371
+ /**
372
+ * @hidden
373
+ */
374
+ findTimeTruncationExpression(path: Path): ASTTimeTruncationExpression {
375
+ return this.find(path).asTimeTruncationExpression();
376
+ }
377
+
378
+ /**
379
+ * @hidden
380
+ */
381
+ asFilteredFieldExpression(): ASTFilteredFieldExpression {
382
+ if (this instanceof ASTFilteredFieldExpression) return this;
383
+ throw new Error('Not an ASTFilteredFieldExpression');
384
+ }
385
+
386
+ /**
387
+ * @hidden
388
+ */
389
+ findFilteredFieldExpression(path: Path): ASTFilteredFieldExpression {
390
+ return this.find(path).asFilteredFieldExpression();
391
+ }
392
+
393
+ /**
394
+ * @hidden
395
+ */
396
+ asNestViewOperation(): ASTNestViewOperation {
397
+ if (this instanceof ASTNestViewOperation) return this;
398
+ throw new Error('Not an ASTNestViewOperation');
399
+ }
400
+
401
+ /**
402
+ * @hidden
403
+ */
404
+ findNestViewOperation(path: Path): ASTNestViewOperation {
405
+ return this.find(path).asNestViewOperation();
406
+ }
407
+
408
+ /**
409
+ * @hidden
410
+ */
411
+ asView(): ASTView {
412
+ if (this instanceof ASTView) return this;
413
+ throw new Error('Not an ASTView');
414
+ }
415
+
416
+ /**
417
+ * @hidden
418
+ */
419
+ findView(path: Path): ASTView {
420
+ return this.find(path).asView();
421
+ }
422
+
423
+ /**
424
+ * @hidden
425
+ */
426
+ asSegmentViewDefinition(): ASTSegmentViewDefinition {
427
+ if (this instanceof ASTSegmentViewDefinition) return this;
428
+ throw new Error('Not an ASTSegmentViewDefinition');
429
+ }
430
+
431
+ /**
432
+ * @hidden
433
+ */
434
+ findSegmentViewDefinition(path: Path): ASTSegmentViewDefinition {
435
+ return this.find(path).asSegmentViewDefinition();
436
+ }
437
+
438
+ /**
439
+ * @hidden
440
+ */
441
+ asLimitViewOperation(): ASTLimitViewOperation {
442
+ if (this instanceof ASTLimitViewOperation) return this;
443
+ throw new Error('Not an ASTLimitViewOperation');
444
+ }
445
+
446
+ /**
447
+ * @hidden
448
+ */
449
+ findLimitViewOperation(path: Path): ASTLimitViewOperation {
450
+ return this.find(path).asLimitViewOperation();
451
+ }
452
+
453
+ /**
454
+ * @hidden
455
+ */
456
+ asAnnotationList(): ASTAnnotationList {
457
+ if (this instanceof ASTAnnotationList) return this;
458
+ throw new Error('Not an ASTAnnotationList');
459
+ }
460
+
461
+ /**
462
+ * @hidden
463
+ */
464
+ findAnnotationList(path: Path): ASTAnnotationList {
465
+ return this.find(path).asAnnotationList();
466
+ }
467
+
468
+ /**
469
+ * @hidden
470
+ */
471
+ asAnnotation(): ASTAnnotation {
472
+ if (this instanceof ASTAnnotation) return this;
473
+ throw new Error('Not an ASTAnnotation');
474
+ }
475
+
476
+ /**
477
+ * @hidden
478
+ */
479
+ findAnnotation(path: Path): ASTAnnotation {
480
+ return this.find(path).asAnnotation();
481
+ }
482
+
483
+ /**
484
+ * @internal
485
+ */
486
+ get parent() {
487
+ if (this._parent === undefined) {
488
+ throw new Error('This node does not have a parent');
489
+ }
490
+ return this._parent;
491
+ }
492
+
493
+ /**
494
+ * @internal
495
+ */
496
+ set parent(parent: ASTAny) {
497
+ this._parent = parent;
498
+ }
499
+
500
+ /**
501
+ * @internal
502
+ */
503
+ static schemaTryGet(
504
+ schema: Malloy.Schema,
505
+ name: string,
506
+ path: string[] | undefined
507
+ ) {
508
+ let current = schema;
509
+ for (const part of path ?? []) {
510
+ const field = current.fields.find(f => f.name === part);
511
+ if (field === undefined) {
512
+ throw new Error(`${part} not found`);
513
+ }
514
+ if (field.kind !== 'join') {
515
+ throw new Error(`${part} is not a join`);
516
+ }
517
+ current = field.schema;
518
+ }
519
+ const field = current.fields.find(f => f.name === name);
520
+ return field;
521
+ }
522
+
523
+ /**
524
+ * @internal
525
+ */
526
+ static schemaGet(
527
+ schema: Malloy.Schema,
528
+ name: string,
529
+ path: string[] | undefined
530
+ ) {
531
+ const field = ASTNode.schemaTryGet(schema, name, path);
532
+ if (field === undefined) {
533
+ throw new Error(`${name} not found`);
534
+ }
535
+ return field;
536
+ }
537
+
538
+ /**
539
+ * @internal
540
+ */
541
+ static schemaMerge(a: Malloy.Schema, b: Malloy.Schema): Malloy.Schema {
542
+ return {
543
+ fields: [...a.fields, ...b.fields],
544
+ };
545
+ }
546
+ }
547
+
548
+ function isBasic(
549
+ t: ASTAny | string | number | string[] | boolean
550
+ ): t is string | number | string[] | boolean {
551
+ return (
552
+ Array.isArray(t) ||
553
+ typeof t === 'string' ||
554
+ typeof t === 'number' ||
555
+ typeof t === 'boolean'
556
+ );
557
+ }
558
+
559
+ abstract class ASTListNode<
560
+ T,
561
+ N extends ASTNode<T> = ASTNode<T>,
562
+ > extends ASTNode<T[]> {
563
+ originalChildren: N[];
564
+ constructor(
565
+ protected node: T[],
566
+ protected children: N[]
567
+ ) {
568
+ super();
569
+ this.originalChildren = [...children];
570
+ children.forEach(c => (c.parent = this));
571
+ }
572
+
573
+ *iter(): Generator<N, void, unknown> {
574
+ for (let i = 0; i < this.length; i++) {
575
+ yield this.index(i);
576
+ }
577
+ }
578
+
579
+ get length() {
580
+ return this.children.length;
581
+ }
582
+
583
+ get last() {
584
+ return this.children[this.children.length - 1];
585
+ }
586
+
587
+ /**
588
+ * Get the `i`th element of this list
589
+ *
590
+ * @param i The index of the list to get
591
+ * @returns The item at index `i` in this list
592
+ */
593
+ index(i: number) {
594
+ return this.children[i];
595
+ }
596
+
597
+ /**
598
+ * @internal
599
+ */
600
+ insert(n: N, index: number) {
601
+ this.edit();
602
+ this.children.splice(index, 0, n);
603
+ n.parent = this;
604
+ }
605
+
606
+ /**
607
+ * @internal
608
+ */
609
+ add(n: N) {
610
+ this.edit();
611
+ this.children.push(n);
612
+ n.parent = this;
613
+ }
614
+
615
+ /**
616
+ * @internal
617
+ */
618
+ remove(n: N) {
619
+ const idx = this.children.findIndex(o => o === n);
620
+ if (idx === -1) return this;
621
+ this.edit();
622
+ this.children.splice(idx, 1);
623
+ }
624
+
625
+ /**
626
+ * @internal
627
+ */
628
+ build(): T[] {
629
+ if (!this.edited) return this.node;
630
+ const ret = this.children.map(c => c.build());
631
+ this.edited = false;
632
+ this.originalChildren = [...this.children];
633
+ this.node = ret;
634
+ return ret;
635
+ }
636
+
637
+ /**
638
+ * @internal
639
+ */
640
+ find(path: Path): ASTAny {
641
+ if (path.length === 0) {
642
+ return this;
643
+ }
644
+ const [head, ...rest] = path;
645
+ if (typeof head !== 'number') {
646
+ throw new Error(
647
+ `${this.constructor.name} is a ASTListNode and thus cannot contain a ${head}`
648
+ );
649
+ }
650
+ const child = this.children[head];
651
+ return child.find(rest);
652
+ }
653
+
654
+ /**
655
+ * @internal
656
+ */
657
+ findIndex(predicate: (n: N) => boolean): number {
658
+ return this.children.findIndex(predicate);
659
+ }
660
+
661
+ /**
662
+ * @internal
663
+ */
664
+ indexOf(n: N): number {
665
+ return this.children.indexOf(n);
666
+ }
667
+ }
668
+
669
+ abstract class ASTObjectNode<
670
+ T,
671
+ Children extends ASTChildren<T>,
672
+ > extends ASTNode<T> {
673
+ constructor(
674
+ protected node: T,
675
+ protected children: Children
676
+ ) {
677
+ super();
678
+ for (const key in children) {
679
+ const child = children[key];
680
+ if (child && !isBasic(child)) {
681
+ child.parent = this;
682
+ }
683
+ }
684
+ }
685
+
686
+ /**
687
+ * @internal
688
+ */
689
+ build(): T {
690
+ if (!this.edited) return this.node;
691
+ let ret = this.node;
692
+ for (const key in this.children) {
693
+ const child = this.children[key];
694
+ if (child === undefined) {
695
+ ret = {...ret, [key]: undefined};
696
+ } else if (isBasic(child)) {
697
+ if (this.edited) {
698
+ ret = {...ret, [key]: child};
699
+ }
700
+ } else {
701
+ ret = {...ret, [key]: child.build()};
702
+ }
703
+ }
704
+ this.node = ret;
705
+ this.edited = false;
706
+ return ret;
707
+ }
708
+
709
+ /**
710
+ * @internal
711
+ */
712
+ find(path: Path): ASTAny {
713
+ if (path.length === 0) {
714
+ return this;
715
+ }
716
+ const [head, ...rest] = path;
717
+ if (typeof head !== 'string') {
718
+ throw new Error(
719
+ `${this.constructor.name} is a ASTListNode and thus cannot contain a ${head}`
720
+ );
721
+ }
722
+ const child = this.children[head];
723
+ if (isBasic(child)) {
724
+ throw new Error(
725
+ `${this.constructor.name}.${head} refers to a basic type, not an ASTNode`
726
+ );
727
+ }
728
+ return child.find(rest);
729
+ }
730
+ }
731
+
732
+ /**
733
+ * AST Object to represent the whole query AST.
734
+ *
735
+ * ```ts
736
+ * const q = new ASTQuery({
737
+ * source: flightsSourceInfo,
738
+ * });
739
+ * const segment = q.getOrAddDefaultSegment();
740
+ * segment.addGroupBy("carrier");
741
+ * segment.addOrderBy("carrier", Malloy.OrderByDirection.DESC);
742
+ * const malloy = q.toMalloy();
743
+ * const query = q.build();
744
+ * ```
745
+ */
746
+ export class ASTQuery
747
+ extends ASTObjectNode<
748
+ Malloy.Query,
749
+ {
750
+ definition: ASTQueryDefinition;
751
+ annotations?: ASTAnnotationList;
752
+ }
753
+ >
754
+ implements IASTAnnotatable
755
+ {
756
+ model: Malloy.ModelInfo;
757
+
758
+ /**
759
+ * Create a new Query AST object against a given source.
760
+ *
761
+ * @param options.query Optional query to begin with; if none is provided, will
762
+ * use an empty query (`{pipeline: {stages: []}}`)
763
+ * @param options.source A source to base the query on. Will set the source name
764
+ * in the provided (or default) query to the name of this source.
765
+ */
766
+ constructor(options: {
767
+ query?: Malloy.Query;
768
+ source:
769
+ | Malloy.ModelEntryValueWithSource
770
+ | Malloy.ModelEntryValueWithQuery
771
+ | Malloy.SourceInfo
772
+ | Malloy.QueryInfo;
773
+ });
774
+ /**
775
+ * Create a new Query AST object against a given model.
776
+ *
777
+ * @param options.query Optional query to begin with; if none is provided, will
778
+ * use an empty query (`{pipeline: {stages: []}}`)
779
+ * @param options.model A model to use for building this query. Use {@link setSource}
780
+ * to configure the source, or set {@link setQueryHead} to run
781
+ * a top level query in the model.
782
+ */
783
+ constructor(options: {query?: Malloy.Query; model: Malloy.ModelInfo});
784
+ constructor(options: {
785
+ query?: Malloy.Query;
786
+ model?: Malloy.ModelInfo;
787
+ source?:
788
+ | Malloy.ModelEntryValueWithSource
789
+ | Malloy.ModelEntryValueWithQuery
790
+ | Malloy.SourceInfo
791
+ | Malloy.QueryInfo;
792
+ }) {
793
+ let chosenSource = options.source;
794
+ if (chosenSource === undefined) {
795
+ if (options.model === undefined) {
796
+ throw new Error('Need a model or source');
797
+ }
798
+ if (options.query) {
799
+ const definition = options.query.definition;
800
+ if (definition.kind === 'arrow') {
801
+ const name = definition.source_reference.name;
802
+ chosenSource = options.model.entries.find(e => e.name === name);
803
+ if (chosenSource === undefined) {
804
+ throw new Error(
805
+ `Model does not contain source or query named ${name}`
806
+ );
807
+ }
808
+ }
809
+ }
810
+ if (chosenSource === undefined) {
811
+ chosenSource = options.model.entries[0];
812
+ }
813
+ if (chosenSource === undefined) {
814
+ throw new Error('Model does not contain any sources or queries');
815
+ }
816
+ }
817
+ const source = sourceOrQueryToModelEntry(chosenSource);
818
+ const query = options.query ?? {
819
+ definition: {
820
+ kind: 'arrow',
821
+ source_reference: {
822
+ name: source.name,
823
+ },
824
+ view: {
825
+ kind: 'segment',
826
+ operations: [],
827
+ },
828
+ },
829
+ };
830
+ const model: Malloy.ModelInfo | undefined = options.model ?? {
831
+ entries: [source],
832
+ anonymous_queries: [],
833
+ };
834
+ super(query, {
835
+ definition: ASTQueryDefinition.from(query.definition),
836
+ annotations:
837
+ query.annotations && new ASTAnnotationList(query.annotations),
838
+ });
839
+ this.model = model;
840
+ if (options.source) {
841
+ this.setSource(options.source.name);
842
+ }
843
+ }
844
+
845
+ get definition() {
846
+ return this.children.definition;
847
+ }
848
+
849
+ set definition(definition: ASTQueryDefinition) {
850
+ this.edit();
851
+ this.children.definition = definition;
852
+ definition.parent = this;
853
+ }
854
+
855
+ get annotations() {
856
+ return this.children.annotations;
857
+ }
858
+
859
+ reorderFields(names: string[]) {
860
+ this.definition.reorderFields(names);
861
+ }
862
+
863
+ getOrAddAnnotations() {
864
+ if (this.annotations) return this.annotations;
865
+ this.edit();
866
+ const annotations = new ASTAnnotationList([]);
867
+ this.children.annotations = annotations;
868
+ return annotations;
869
+ }
870
+
871
+ getInheritedTag(prefix: RegExp | string = '# ') {
872
+ return tagFromAnnotations(prefix, this.getInheritedAnnotations());
873
+ }
874
+
875
+ getIntrinsicTag(prefix: RegExp | string = '# ') {
876
+ return this.annotations?.getIntrinsicTag(prefix) ?? new Tag();
877
+ }
878
+
879
+ getTag(prefix: RegExp | string = '# ') {
880
+ return this.annotations?.getTag(prefix) ?? this.getInheritedTag(prefix);
881
+ }
882
+
883
+ setTagProperty(path: Path, value: TagSetValue = null, prefix = '# ') {
884
+ this.getOrAddAnnotations().setTagProperty(path, value, prefix);
885
+ }
886
+
887
+ removeTagProperty(path: Path, prefix = '# ') {
888
+ if (!this.getTag().has(...path)) return;
889
+ this.getOrAddAnnotations().removeTagProperty(path, prefix);
890
+ }
891
+
892
+ setViewToEmptySegment() {
893
+ if (!(this.definition instanceof ASTArrowQueryDefinition)) {
894
+ throw new Error('Must set source before setting view');
895
+ }
896
+ this.definition = new ASTArrowQueryDefinition({
897
+ kind: 'arrow',
898
+ source_reference: this.definition.sourceReference.build(),
899
+ view: {
900
+ kind: 'segment',
901
+ operations: [],
902
+ },
903
+ });
904
+ return this.definition.view.asSegmentViewDefinition();
905
+ }
906
+
907
+ isRunnable() {
908
+ return this.definition.isRunnable();
909
+ }
910
+
911
+ /**
912
+ * Gets an {@link ASTSegmentViewDefinition} for the "default" place to add query
913
+ * operations, or creates one if it doesn't exist.
914
+ *
915
+ * ```
916
+ * run: flights ->
917
+ * ```
918
+ * ```ts
919
+ * q.getOrAddDefaultSegment();
920
+ * ```
921
+ * ```
922
+ * run: flights -> { }
923
+ * ```
924
+ *
925
+ * If there is a view at the head, it will refine it:
926
+ * ```
927
+ * run: flights -> by_carrier
928
+ * ```
929
+ * ```ts
930
+ * q.getOrAddDefaultSegment();
931
+ * ```
932
+ * ```
933
+ * run: flights -> by_carrier + { }
934
+ * ```
935
+ */
936
+ public getOrAddDefaultSegment(): ASTSegmentViewDefinition {
937
+ return this.definition.getOrAddDefaultSegment();
938
+ }
939
+
940
+ /**
941
+ * Sets the source of this query to be a reference to a source in the model.
942
+ *
943
+ * ```ts
944
+ * q.setSource('flights')
945
+ * ```
946
+ * ```
947
+ * run: flights -> { }
948
+ * ```
949
+ *
950
+ * @param name The name of the source in the model to reference.
951
+ */
952
+ public setSource(name: string) {
953
+ if (this.definition instanceof ASTArrowQueryDefinition) {
954
+ if (this.definition.sourceReference.name === name) {
955
+ return;
956
+ }
957
+ }
958
+ const source = this.model.entries.find(e => e.name === name);
959
+ if (source === undefined) {
960
+ throw new Error(`Source ${name} is not defined in model`);
961
+ }
962
+ this.definition = new ASTArrowQueryDefinition({
963
+ kind: 'arrow',
964
+ source_reference: {name},
965
+ view:
966
+ this.definition instanceof ASTArrowQueryDefinition
967
+ ? this.definition.view.build()
968
+ : {
969
+ kind: 'segment',
970
+ operations: [],
971
+ },
972
+ });
973
+ }
974
+
975
+ /**
976
+ * Sets the head of this query to be a reference to a top level query.
977
+ *
978
+ * ```ts
979
+ * q.setQueryHead('flights_by_carrier')
980
+ * ```
981
+ * ```
982
+ * run: flights_by_carrier
983
+ * ```
984
+ *
985
+ * @param name The name of the query in the model to reference.
986
+ */
987
+ public setQueryHead(name: string) {
988
+ const query = this.model.entries.find(e => e.name === name);
989
+ if (query === undefined || query.kind !== 'query') {
990
+ throw new Error(`No query named ${name} in the model`);
991
+ }
992
+ this.definition = new ASTReferenceQueryDefinition({
993
+ kind: 'query_reference',
994
+ name,
995
+ });
996
+ }
997
+
998
+ /**
999
+ * @internal
1000
+ */
1001
+ getQueryInfo(name: string): Malloy.QueryInfo {
1002
+ const query = this.model.entries.find(e => e.name === name);
1003
+ if (query === undefined) {
1004
+ throw new Error(`Query ${name} is not defined in model`);
1005
+ }
1006
+ if (query.kind !== 'query') {
1007
+ throw new Error(`Model entry ${name} is not a query`);
1008
+ }
1009
+ return query;
1010
+ }
1011
+
1012
+ /**
1013
+ * Emits the current query object as Malloy code
1014
+ *
1015
+ * ```ts
1016
+ * q.setSource('flights')
1017
+ * q.setView('by_carrier')
1018
+ * q.toMalloy();
1019
+ * ```
1020
+ * ```
1021
+ * run: flights -> by_carrier
1022
+ * ```
1023
+ *
1024
+ * @returns Malloy code for the query
1025
+ */
1026
+ toMalloy(): string {
1027
+ return Malloy.queryToMalloy(this.build());
1028
+ }
1029
+
1030
+ /**
1031
+ * Build the query into its `Malloy.Query` object form. New JS objects will be
1032
+ * created for any subtree which has edits.
1033
+ *
1034
+ * @returns A `Malloy.Query` representing the current query
1035
+ */
1036
+ build(): Malloy.Query {
1037
+ return super.build();
1038
+ }
1039
+
1040
+ /**
1041
+ * Set the view of this query; overwrites any other query operations.
1042
+ *
1043
+ * ```
1044
+ * run: flights ->
1045
+ * ```
1046
+ * ```ts
1047
+ * q.setView('by_carrier')
1048
+ * ```
1049
+ * ```
1050
+ * run: flights -> by_carrier
1051
+ * ```
1052
+ *
1053
+ * @param name The name of the view to set as the head of the query pipeline
1054
+ */
1055
+ setView(name: string): ASTReferenceViewDefinition {
1056
+ if (!(this.definition instanceof ASTArrowQueryDefinition)) {
1057
+ throw new Error('Must set source before setting view');
1058
+ }
1059
+ this.definition = new ASTArrowQueryDefinition({
1060
+ kind: 'arrow',
1061
+ source_reference: this.definition.sourceReference.build(),
1062
+ view: {
1063
+ kind: 'view_reference',
1064
+ name,
1065
+ },
1066
+ });
1067
+ return this.definition.view.asReferenceViewDefinition();
1068
+ }
1069
+
1070
+ getInheritedAnnotations(): Malloy.Annotation[] {
1071
+ if (this.definition instanceof ASTReferenceQueryDefinition) {
1072
+ const query = this.getQueryInfo(this.definition.name);
1073
+ return query.annotations ?? [];
1074
+ } else if (this.definition instanceof ASTRefinementQueryDefinition) {
1075
+ const query = this.getQueryInfo(this.definition.queryReference.name);
1076
+ return query.annotations ?? [];
1077
+ } else if (this.definition instanceof ASTArrowQueryDefinition) {
1078
+ return this.definition.view.getInheritedAnnotations();
1079
+ }
1080
+ return [];
1081
+ }
1082
+ }
1083
+
1084
+ export type RawLiteralValue =
1085
+ | number
1086
+ | string
1087
+ | {date: Date; granularity: Malloy.TimestampTimeframe}
1088
+ | boolean
1089
+ | null;
1090
+
1091
+ export interface IASTReference extends ASTAny {
1092
+ /**
1093
+ * Gets the parameter list for this reference, or creates it if it does not exist.
1094
+ *
1095
+ * @returns The parameter list `ASTParameterValueList`
1096
+ */
1097
+ getOrAddParameters(): ASTParameterValueList;
1098
+
1099
+ /**
1100
+ * Adds a parameter to this source with with the given name and value
1101
+ *
1102
+ * This will override an existing parameter with the same name.
1103
+ *
1104
+ * @param name The name of the parameter to set
1105
+ * @param value The value of the parameter to set
1106
+ */
1107
+ setParameter(name: string, value: RawLiteralValue): void;
1108
+
1109
+ tryGetParameter(name: string): ASTParameterValue | undefined;
1110
+
1111
+ parameters: ASTParameterValueList | undefined;
1112
+ name: string;
1113
+ path: string[] | undefined;
1114
+ }
1115
+
1116
+ export class ASTReference
1117
+ extends ASTObjectNode<
1118
+ Malloy.Reference,
1119
+ {
1120
+ name: string;
1121
+ path?: string[];
1122
+ parameters?: ASTParameterValueList;
1123
+ }
1124
+ >
1125
+ implements IASTReference
1126
+ {
1127
+ constructor(public reference: Malloy.Reference) {
1128
+ super(reference, {
1129
+ name: reference.name,
1130
+ path: reference.path,
1131
+ parameters:
1132
+ reference.parameters && new ASTParameterValueList(reference.parameters),
1133
+ });
1134
+ }
1135
+
1136
+ get name() {
1137
+ return this.children.name;
1138
+ }
1139
+
1140
+ set name(value: string) {
1141
+ this.edit();
1142
+ this.children.name = value;
1143
+ }
1144
+
1145
+ get parameters() {
1146
+ return this.children.parameters;
1147
+ }
1148
+
1149
+ set parameters(parameters: ASTParameterValueList | undefined) {
1150
+ this.edit();
1151
+ this.children.parameters = parameters;
1152
+ }
1153
+
1154
+ get path() {
1155
+ return this.children.path;
1156
+ }
1157
+
1158
+ /**
1159
+ * @internal
1160
+ */
1161
+ static getOrAddParameters(reference: IASTReference) {
1162
+ if (reference.parameters) {
1163
+ return reference.parameters;
1164
+ }
1165
+ reference.edit();
1166
+ const parameters = new ASTParameterValueList([]);
1167
+ reference.parameters = parameters;
1168
+ return parameters;
1169
+ }
1170
+
1171
+ /**
1172
+ * @internal
1173
+ */
1174
+ static setParameter(
1175
+ reference: IASTReference,
1176
+ name: string,
1177
+ value: RawLiteralValue
1178
+ ) {
1179
+ return reference.getOrAddParameters().addParameter(name, value);
1180
+ }
1181
+
1182
+ /**
1183
+ * @internal
1184
+ */
1185
+ static tryGetParameter(reference: IASTReference, name: string) {
1186
+ if (reference.parameters === undefined) return undefined;
1187
+ for (const parameter of reference.parameters.iter()) {
1188
+ if (parameter.name === name) {
1189
+ return parameter;
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ public getOrAddParameters() {
1195
+ return ASTReference.getOrAddParameters(this);
1196
+ }
1197
+
1198
+ public setParameter(name: string, value: RawLiteralValue) {
1199
+ return ASTReference.setParameter(this, name, value);
1200
+ }
1201
+
1202
+ public tryGetParameter(name: string): ASTParameterValue | undefined {
1203
+ return ASTReference.tryGetParameter(this, name);
1204
+ }
1205
+ }
1206
+
1207
+ type ASTFieldReferenceParent =
1208
+ | ASTFilterWithFilterString
1209
+ | ASTOrderByViewOperation
1210
+ | ASTTimeTruncationExpression
1211
+ | ASTFilteredFieldExpression;
1212
+
1213
+ export class ASTFieldReference extends ASTReference {
1214
+ /**
1215
+ * @internal
1216
+ */
1217
+ get segment(): ASTSegmentViewDefinition {
1218
+ const parent = this.parent as ASTFieldReferenceParent;
1219
+ if (
1220
+ parent instanceof ASTFilteredFieldExpression ||
1221
+ parent instanceof ASTTimeTruncationExpression
1222
+ ) {
1223
+ return parent.field.segment;
1224
+ } else if (parent instanceof ASTFilterWithFilterString) {
1225
+ const grand = parent.parent as ASTWhere | ASTWhereViewOperation;
1226
+ if (grand instanceof ASTWhere) {
1227
+ return grand.list.expression.field.segment;
1228
+ } else {
1229
+ return grand.list.segment;
1230
+ }
1231
+ } else {
1232
+ return parent.list.segment;
1233
+ }
1234
+ }
1235
+
1236
+ getFieldInfo() {
1237
+ const schema = this.segment.getInputSchema();
1238
+ return ASTNode.schemaGet(schema, this.name, this.path);
1239
+ }
1240
+ }
1241
+
1242
+ export class ASTSourceReference extends ASTReference {
1243
+ /**
1244
+ * @internal
1245
+ */
1246
+ get query(): ASTQuery {
1247
+ return this.parent.parent.asQuery();
1248
+ }
1249
+
1250
+ /**
1251
+ * Gets the `Malloy.SourceInfo` for the referenced source
1252
+ *
1253
+ * @returns The source information for the referenced source
1254
+ */
1255
+ public getSourceInfo(): Malloy.SourceInfo {
1256
+ const info = this.query.model.entries.find(e => e.name === this.name);
1257
+ if (info === undefined) {
1258
+ throw new Error('No source info found');
1259
+ }
1260
+ return info;
1261
+ }
1262
+
1263
+ public getSourceParameters(): Malloy.ParameterInfo[] {
1264
+ return this.getSourceInfo().parameters ?? [];
1265
+ }
1266
+
1267
+ areRequiredParametersSet() {
1268
+ const sourceParameters = this.getSourceParameters();
1269
+ for (const parameterInfo of sourceParameters) {
1270
+ if (parameterInfo.default_value !== undefined) continue;
1271
+ const parameter = this.tryGetParameter(parameterInfo.name);
1272
+ if (parameter === undefined) {
1273
+ return false;
1274
+ }
1275
+ }
1276
+ return true;
1277
+ }
1278
+ }
1279
+
1280
+ export class ASTParameterValueList extends ASTListNode<
1281
+ Malloy.ParameterValue,
1282
+ ASTParameterValue
1283
+ > {
1284
+ constructor(parameters: Malloy.ParameterValue[]) {
1285
+ super(
1286
+ parameters,
1287
+ parameters.map(p => new ASTParameterValue(p))
1288
+ );
1289
+ }
1290
+
1291
+ get parameters() {
1292
+ return this.children;
1293
+ }
1294
+
1295
+ addParameter(name: string, value: RawLiteralValue) {
1296
+ // TODO validate that the parameter is valid (name and type)
1297
+ this.add(
1298
+ new ASTParameterValue({
1299
+ name,
1300
+ value: LiteralValueAST.makeLiteral(value),
1301
+ })
1302
+ );
1303
+ }
1304
+ }
1305
+
1306
+ export class ASTParameterValue extends ASTObjectNode<
1307
+ Malloy.ParameterValue,
1308
+ {
1309
+ name: string;
1310
+ value: ASTLiteralValue;
1311
+ }
1312
+ > {
1313
+ constructor(public parameter: Malloy.ParameterValue) {
1314
+ super(parameter, {
1315
+ name: parameter.name,
1316
+ value: LiteralValueAST.from(parameter.value),
1317
+ });
1318
+ }
1319
+
1320
+ get name() {
1321
+ return this.children.name;
1322
+ }
1323
+ }
1324
+
1325
+ export type ASTLiteralValue =
1326
+ | ASTStringLiteralValue
1327
+ | ASTNumberLiteralValue
1328
+ | ASTBooleanLiteralValue
1329
+ | ASTDateLiteralValue
1330
+ | ASTTimestampLiteralValue
1331
+ | ASTNullLiteralValue;
1332
+ export const LiteralValueAST = {
1333
+ from(value: Malloy.LiteralValue) {
1334
+ switch (value.kind) {
1335
+ case 'string_literal':
1336
+ return new ASTStringLiteralValue(value);
1337
+ case 'number_literal':
1338
+ return new ASTNumberLiteralValue(value);
1339
+ case 'boolean_literal':
1340
+ return new ASTBooleanLiteralValue(value);
1341
+ case 'date_literal':
1342
+ return new ASTDateLiteralValue(value);
1343
+ case 'timestamp_literal':
1344
+ return new ASTTimestampLiteralValue(value);
1345
+ case 'null_literal':
1346
+ return new ASTNullLiteralValue(value);
1347
+ }
1348
+ },
1349
+ makeLiteral(value: RawLiteralValue): Malloy.LiteralValue {
1350
+ if (typeof value === 'string') {
1351
+ return {
1352
+ kind: 'string_literal',
1353
+ string_value: value,
1354
+ };
1355
+ } else if (typeof value === 'boolean') {
1356
+ return {
1357
+ kind: 'boolean_literal',
1358
+ boolean_value: value,
1359
+ };
1360
+ } else if (typeof value === 'number') {
1361
+ return {
1362
+ kind: 'number_literal',
1363
+ number_value: value,
1364
+ };
1365
+ } else if (value === null) {
1366
+ return {
1367
+ kind: 'null_literal',
1368
+ };
1369
+ } else if ('date' in value) {
1370
+ const granularity = value.granularity;
1371
+ const serialized = serializeDateAsLiteral(value.date);
1372
+ if (isDateTimeframe(granularity)) {
1373
+ return {
1374
+ kind: 'date_literal',
1375
+ date_value: serialized,
1376
+ granularity: granularity,
1377
+ };
1378
+ }
1379
+ return {
1380
+ kind: 'timestamp_literal',
1381
+ timestamp_value: serialized,
1382
+ granularity: granularity,
1383
+ };
1384
+ }
1385
+ throw new Error('Unknown literal type');
1386
+ },
1387
+ };
1388
+
1389
+ export class ASTStringLiteralValue extends ASTObjectNode<
1390
+ Malloy.LiteralValueWithStringLiteral,
1391
+ {
1392
+ kind: 'string_literal';
1393
+ string_value: string;
1394
+ }
1395
+ > {
1396
+ readonly kind: Malloy.LiteralValueType = 'string_literal';
1397
+
1398
+ constructor(public node: Malloy.LiteralValueWithStringLiteral) {
1399
+ super(node, {
1400
+ kind: node.kind,
1401
+ string_value: node.string_value,
1402
+ });
1403
+ }
1404
+ }
1405
+
1406
+ export class ASTNullLiteralValue extends ASTObjectNode<
1407
+ Malloy.LiteralValueWithNullLiteral,
1408
+ {
1409
+ kind: 'null_literal';
1410
+ }
1411
+ > {
1412
+ readonly kind: Malloy.LiteralValueType = 'null_literal';
1413
+
1414
+ constructor(public node: Malloy.LiteralValueWithNullLiteral) {
1415
+ super(node, {
1416
+ kind: node.kind,
1417
+ });
1418
+ }
1419
+ }
1420
+
1421
+ export class ASTNumberLiteralValue extends ASTObjectNode<
1422
+ Malloy.LiteralValueWithNumberLiteral,
1423
+ {
1424
+ kind: 'number_literal';
1425
+ number_value: number;
1426
+ }
1427
+ > {
1428
+ readonly kind: Malloy.LiteralValueType = 'number_literal';
1429
+
1430
+ constructor(public node: Malloy.LiteralValueWithNumberLiteral) {
1431
+ super(node, {
1432
+ kind: node.kind,
1433
+ number_value: node.number_value,
1434
+ });
1435
+ }
1436
+ }
1437
+
1438
+ export class ASTBooleanLiteralValue extends ASTObjectNode<
1439
+ Malloy.LiteralValueWithBooleanLiteral,
1440
+ {
1441
+ kind: 'boolean_literal';
1442
+ boolean_value: boolean;
1443
+ }
1444
+ > {
1445
+ readonly kind: Malloy.LiteralValueType = 'boolean_literal';
1446
+
1447
+ constructor(public node: Malloy.LiteralValueWithBooleanLiteral) {
1448
+ super(node, {
1449
+ kind: node.kind,
1450
+ boolean_value: node.boolean_value,
1451
+ });
1452
+ }
1453
+ }
1454
+
1455
+ export class ASTDateLiteralValue extends ASTObjectNode<
1456
+ Malloy.LiteralValueWithDateLiteral,
1457
+ {
1458
+ kind: 'date_literal';
1459
+ date_value: string;
1460
+ granularity?: Malloy.DateTimeframe;
1461
+ }
1462
+ > {
1463
+ readonly kind: Malloy.LiteralValueType = 'date_literal';
1464
+
1465
+ constructor(public node: Malloy.LiteralValueWithDateLiteral) {
1466
+ super(node, {
1467
+ kind: node.kind,
1468
+ date_value: node.date_value,
1469
+ granularity: node.granularity,
1470
+ });
1471
+ }
1472
+ }
1473
+
1474
+ export class ASTTimestampLiteralValue extends ASTObjectNode<
1475
+ Malloy.LiteralValueWithTimestampLiteral,
1476
+ {
1477
+ kind: 'timestamp_literal';
1478
+ timestamp_value: string;
1479
+ granularity?: Malloy.TimestampTimeframe;
1480
+ }
1481
+ > {
1482
+ readonly kind: Malloy.LiteralValueType = 'timestamp_literal';
1483
+
1484
+ constructor(public node: Malloy.LiteralValueWithTimestampLiteral) {
1485
+ super(node, {
1486
+ kind: node.kind,
1487
+ timestamp_value: node.timestamp_value,
1488
+ granularity: node.granularity,
1489
+ });
1490
+ }
1491
+ }
1492
+
1493
+ export class ASTUnimplemented<T> extends ASTNode<T> {
1494
+ constructor(private readonly node: T) {
1495
+ super();
1496
+ }
1497
+ get treeEdited(): boolean {
1498
+ return false;
1499
+ }
1500
+
1501
+ build(): T {
1502
+ return this.node;
1503
+ }
1504
+
1505
+ find(): never {
1506
+ throw new Error('Tried to find a node from an unimplemented node type');
1507
+ }
1508
+ }
1509
+
1510
+ export interface IASTQueryOrViewDefinition {
1511
+ /**
1512
+ * Upward propagation of field deletion/rename etc
1513
+ * @internal
1514
+ */
1515
+ propagateUp(f: PropagationFunction): void;
1516
+ /**
1517
+ * Downward propagation of field deletion/rename etc
1518
+ * @internal
1519
+ */
1520
+ propagateDown(f: PropagationFunction): void;
1521
+ }
1522
+
1523
+ type PropagationFunction = (propagatable: IASTQueryOrViewDefinition) => void;
1524
+
1525
+ export interface IASTQueryDefinition extends IASTQueryOrViewDefinition {
1526
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition;
1527
+ reorderFields(names: string[]): void;
1528
+ isRunnable(): boolean;
1529
+ }
1530
+
1531
+ export type ASTQueryDefinition =
1532
+ | ASTReferenceQueryDefinition
1533
+ | ASTArrowQueryDefinition
1534
+ | ASTRefinementQueryDefinition;
1535
+ export const ASTQueryDefinition = {
1536
+ from: (definition: Malloy.QueryDefinition) => {
1537
+ switch (definition.kind) {
1538
+ case 'arrow':
1539
+ return new ASTArrowQueryDefinition(definition);
1540
+ case 'query_reference':
1541
+ return new ASTReferenceQueryDefinition(definition);
1542
+ case 'refinement':
1543
+ return new ASTRefinementQueryDefinition(definition);
1544
+ }
1545
+ },
1546
+ };
1547
+
1548
+ export class ASTArrowQueryDefinition
1549
+ extends ASTObjectNode<
1550
+ Malloy.QueryDefinitionWithArrow,
1551
+ {
1552
+ kind: 'arrow';
1553
+ source_reference: ASTSourceReference;
1554
+ view: ASTViewDefinition;
1555
+ }
1556
+ >
1557
+ implements IASTQueryDefinition
1558
+ {
1559
+ constructor(public node: Malloy.QueryDefinitionWithArrow) {
1560
+ super(node, {
1561
+ kind: 'arrow',
1562
+ source_reference: new ASTSourceReference(node.source_reference),
1563
+ view: ASTViewDefinition.from(node.view),
1564
+ });
1565
+ }
1566
+
1567
+ get view() {
1568
+ return this.children.view;
1569
+ }
1570
+
1571
+ set view(view: ASTViewDefinition) {
1572
+ this.edit();
1573
+ this.children.view = view;
1574
+ view.parent = this;
1575
+ }
1576
+
1577
+ get sourceReference() {
1578
+ return this.children.source_reference;
1579
+ }
1580
+
1581
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition {
1582
+ return this.view.getOrAddDefaultSegment();
1583
+ }
1584
+
1585
+ getSourceInfo() {
1586
+ return this.sourceReference.getSourceInfo();
1587
+ }
1588
+
1589
+ getOutputSchema() {
1590
+ return this.view.getRefinementSchema();
1591
+ }
1592
+
1593
+ isRunnable(): boolean {
1594
+ return (
1595
+ this.view.isRunnable() && this.sourceReference.areRequiredParametersSet()
1596
+ );
1597
+ }
1598
+
1599
+ /**
1600
+ * @internal
1601
+ */
1602
+ get query() {
1603
+ return this.parent.asQuery();
1604
+ }
1605
+
1606
+ /**
1607
+ * @internal
1608
+ */
1609
+ propagateUp(f: PropagationFunction): void {
1610
+ this.propagateDown(f);
1611
+ }
1612
+
1613
+ /**
1614
+ * @internal
1615
+ */
1616
+ propagateDown(f: PropagationFunction): void {
1617
+ f(this.view);
1618
+ this.view.propagateDown(f);
1619
+ }
1620
+
1621
+ reorderFields(names: string[]): void {
1622
+ if (this.view instanceof ASTSegmentViewDefinition) {
1623
+ this.view.reorderFields(names);
1624
+ } else {
1625
+ this.query.getOrAddAnnotations().setTagProperty(['field_order'], names);
1626
+ }
1627
+ }
1628
+ }
1629
+
1630
+ export class ASTRefinementQueryDefinition
1631
+ extends ASTObjectNode<
1632
+ Malloy.QueryDefinitionWithRefinement,
1633
+ {
1634
+ kind: 'refinement';
1635
+ query_reference: ASTReference;
1636
+ refinement: ASTViewDefinition;
1637
+ }
1638
+ >
1639
+ implements IASTQueryDefinition
1640
+ {
1641
+ constructor(public node: Malloy.QueryDefinitionWithRefinement) {
1642
+ super(node, {
1643
+ kind: 'refinement',
1644
+ query_reference: new ASTReference(node.query_reference),
1645
+ refinement: ASTViewDefinition.from(node.refinement),
1646
+ });
1647
+ }
1648
+
1649
+ get queryReference() {
1650
+ return this.children.query_reference;
1651
+ }
1652
+
1653
+ get refinement() {
1654
+ return this.children.refinement;
1655
+ }
1656
+
1657
+ set refinement(refinement: ASTViewDefinition) {
1658
+ this.edit();
1659
+ this.children.refinement = refinement;
1660
+ refinement.parent = this;
1661
+ }
1662
+
1663
+ isRunnable(): boolean {
1664
+ return this.refinement.isRunnable();
1665
+ }
1666
+
1667
+ /**
1668
+ * @internal
1669
+ */
1670
+ get query() {
1671
+ return this.parent.asQuery();
1672
+ }
1673
+
1674
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition {
1675
+ return this.refinement.getOrAddDefaultSegment();
1676
+ }
1677
+
1678
+ getOutputSchema() {
1679
+ const model = this.query.model;
1680
+ const query = model.entries.find(e => e.name === this.queryReference.name);
1681
+ if (query === undefined) {
1682
+ throw new Error(`Query not found with name ${this.queryReference.name}`);
1683
+ }
1684
+ const base = query.schema;
1685
+ const refinement = this.refinement.getRefinementSchema();
1686
+ return ASTQuery.schemaMerge(base, refinement);
1687
+ }
1688
+
1689
+ /**
1690
+ * @internal
1691
+ */
1692
+ propagateUp(f: PropagationFunction): void {
1693
+ this.propagateDown(f);
1694
+ }
1695
+
1696
+ /**
1697
+ * @internal
1698
+ */
1699
+ propagateDown(f: PropagationFunction): void {
1700
+ f(this.refinement);
1701
+ this.refinement.propagateDown(f);
1702
+ }
1703
+
1704
+ reorderFields(names: string[]): void {
1705
+ this.query.getOrAddAnnotations().setTagProperty(['field_order'], names);
1706
+ }
1707
+ }
1708
+
1709
+ export class ASTReferenceQueryDefinition
1710
+ extends ASTObjectNode<
1711
+ Malloy.QueryDefinitionWithQueryReference,
1712
+ {
1713
+ kind: 'query_reference';
1714
+ name: string;
1715
+ path?: string[];
1716
+ parameters?: ASTParameterValueList;
1717
+ }
1718
+ >
1719
+ implements IASTQueryDefinition, IASTReference
1720
+ {
1721
+ constructor(public node: Malloy.QueryDefinitionWithQueryReference) {
1722
+ super(node, {
1723
+ kind: 'query_reference',
1724
+ name: node.name,
1725
+ path: node.path,
1726
+ parameters: node.parameters && new ASTParameterValueList(node.parameters),
1727
+ });
1728
+ }
1729
+
1730
+ isRunnable(): boolean {
1731
+ return true;
1732
+ }
1733
+
1734
+ get name() {
1735
+ return this.children.name;
1736
+ }
1737
+
1738
+ get query() {
1739
+ return this.parent.asQuery();
1740
+ }
1741
+
1742
+ get parameters() {
1743
+ return this.children.parameters;
1744
+ }
1745
+
1746
+ set parameters(parameters: ASTParameterValueList | undefined) {
1747
+ this.edit();
1748
+ this.children.parameters = parameters;
1749
+ }
1750
+
1751
+ get path() {
1752
+ return this.children.path;
1753
+ }
1754
+
1755
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition {
1756
+ const newQuery = new ASTRefinementQueryDefinition({
1757
+ kind: 'refinement',
1758
+ query_reference: {
1759
+ name: this.name,
1760
+ path: this.path,
1761
+ parameters: this.parameters?.build(),
1762
+ },
1763
+ refinement: {
1764
+ kind: 'segment',
1765
+ operations: [],
1766
+ },
1767
+ });
1768
+ this.query.definition = newQuery;
1769
+ return newQuery.refinement.asSegmentViewDefinition();
1770
+ }
1771
+
1772
+ /**
1773
+ * @internal
1774
+ */
1775
+ propagateUp(_f: PropagationFunction): void {
1776
+ return;
1777
+ }
1778
+
1779
+ /**
1780
+ * @internal
1781
+ */
1782
+ propagateDown(_f: PropagationFunction): void {
1783
+ return;
1784
+ }
1785
+
1786
+ reorderFields(names: string[]): void {
1787
+ this.query.getOrAddAnnotations().setTagProperty(['field_order'], names);
1788
+ }
1789
+
1790
+ public getOrAddParameters() {
1791
+ return ASTReference.getOrAddParameters(this);
1792
+ }
1793
+
1794
+ public setParameter(name: string, value: RawLiteralValue) {
1795
+ return ASTReference.setParameter(this, name, value);
1796
+ }
1797
+
1798
+ public tryGetParameter(name: string): ASTParameterValue | undefined {
1799
+ return ASTReference.tryGetParameter(this, name);
1800
+ }
1801
+ }
1802
+
1803
+ export interface IASTViewDefinition extends IASTQueryOrViewDefinition {
1804
+ isRunnable(): boolean;
1805
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition;
1806
+ getInputSchema(): Malloy.Schema;
1807
+ getOutputSchema(): Malloy.Schema;
1808
+ getImplicitName(): string | undefined;
1809
+ getRefinementSchema(): Malloy.Schema;
1810
+ addEmptyRefinement(): ASTSegmentViewDefinition;
1811
+ addViewRefinement(name: string, path?: string[]): ASTReferenceViewDefinition;
1812
+ isValidViewRefinement(
1813
+ name: string,
1814
+ path?: string[]
1815
+ ): {
1816
+ isValidViewRefinement: boolean;
1817
+ error?: string;
1818
+ };
1819
+ getInheritedAnnotations(): Malloy.Annotation[];
1820
+ }
1821
+
1822
+ export type ASTViewDefinition =
1823
+ | ASTArrowViewDefinition
1824
+ | ASTRefinementViewDefinition
1825
+ | ASTSegmentViewDefinition
1826
+ | ASTReferenceViewDefinition;
1827
+ const ASTViewDefinition = {
1828
+ from(definition: Malloy.ViewDefinition) {
1829
+ switch (definition.kind) {
1830
+ case 'arrow':
1831
+ return new ASTArrowViewDefinition(definition);
1832
+ case 'view_reference':
1833
+ return new ASTReferenceViewDefinition(definition);
1834
+ case 'segment':
1835
+ return new ASTSegmentViewDefinition(definition);
1836
+ case 'refinement':
1837
+ return new ASTRefinementViewDefinition(definition);
1838
+ }
1839
+ },
1840
+ };
1841
+
1842
+ function swapViewInParent(node: ASTViewDefinition, view: ASTViewDefinition) {
1843
+ const parent = node.parent as
1844
+ | ASTArrowQueryDefinition
1845
+ | ASTRefinementQueryDefinition
1846
+ | ASTView
1847
+ | ASTArrowViewDefinition
1848
+ | ASTRefinementViewDefinition;
1849
+ if (parent instanceof ASTArrowQueryDefinition) {
1850
+ parent.view = view;
1851
+ } else if (parent instanceof ASTRefinementQueryDefinition) {
1852
+ parent.refinement = view;
1853
+ } else if (parent instanceof ASTView) {
1854
+ parent.definition = view;
1855
+ } else if (parent instanceof ASTArrowViewDefinition) {
1856
+ if (parent.source === node) {
1857
+ parent.source = view;
1858
+ } else {
1859
+ parent.view = view;
1860
+ }
1861
+ } else {
1862
+ if (parent.base === node) {
1863
+ parent.base = view;
1864
+ } else {
1865
+ parent.refinement = view;
1866
+ }
1867
+ }
1868
+ }
1869
+
1870
+ function isValidViewRefinement(
1871
+ view: ASTViewDefinition,
1872
+ name: string,
1873
+ path: string[] = []
1874
+ ): {
1875
+ isValidViewRefinement: boolean;
1876
+ error?: string;
1877
+ } {
1878
+ const schema = view.getInputSchema();
1879
+ const field = ASTQuery.schemaGet(schema, name, path);
1880
+ if (field === undefined) {
1881
+ return {isValidViewRefinement: false, error: `${name} is not defined`};
1882
+ } else if (field.kind !== 'view') {
1883
+ // TODO scalar refinements
1884
+ return {isValidViewRefinement: false, error: `${name} is not a view`};
1885
+ }
1886
+ const prevOutput = view.getOutputSchema();
1887
+ for (const refinementField of field.schema.fields) {
1888
+ if (ASTQuery.schemaTryGet(prevOutput, refinementField.name, [])) {
1889
+ return {
1890
+ isValidViewRefinement: false,
1891
+ error: `Cannot refine with ${name} because stage already has an output field named ${refinementField.name}`,
1892
+ };
1893
+ }
1894
+ }
1895
+ return {isValidViewRefinement: true};
1896
+ }
1897
+
1898
+ export class ASTReferenceViewDefinition
1899
+ extends ASTObjectNode<
1900
+ Malloy.ViewDefinitionWithViewReference,
1901
+ {
1902
+ kind: 'view_reference';
1903
+ name: string;
1904
+ path?: string[];
1905
+ parameters?: ASTParameterValueList;
1906
+ }
1907
+ >
1908
+ implements IASTViewDefinition, IASTReference
1909
+ {
1910
+ constructor(public node: Malloy.ViewDefinitionWithViewReference) {
1911
+ super(node, {
1912
+ kind: 'view_reference',
1913
+ name: node.name,
1914
+ path: node.path,
1915
+ parameters: node.parameters && new ASTParameterValueList(node.parameters),
1916
+ });
1917
+ }
1918
+
1919
+ isRunnable(): boolean {
1920
+ return true;
1921
+ }
1922
+
1923
+ get name() {
1924
+ return this.children.name;
1925
+ }
1926
+
1927
+ get path() {
1928
+ return this.children.path;
1929
+ }
1930
+
1931
+ get parameters() {
1932
+ return this.children.parameters;
1933
+ }
1934
+
1935
+ set parameters(parameters: ASTParameterValueList | undefined) {
1936
+ this.edit();
1937
+ this.children.parameters = parameters;
1938
+ }
1939
+
1940
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition {
1941
+ return this.addEmptyRefinement();
1942
+ }
1943
+
1944
+ addEmptyRefinement(): ASTSegmentViewDefinition {
1945
+ const newView = ASTRefinementViewDefinition.segmentRefinementOf(
1946
+ this.build()
1947
+ );
1948
+ swapViewInParent(this, newView);
1949
+ return newView.refinement.asSegmentViewDefinition();
1950
+ }
1951
+
1952
+ addViewRefinement(name: string, path?: string[]): ASTReferenceViewDefinition {
1953
+ const {error} = this.isValidViewRefinement(name, path);
1954
+ if (error) {
1955
+ throw new Error(error);
1956
+ }
1957
+ const newView = ASTRefinementViewDefinition.viewRefinementOf(
1958
+ this.build(),
1959
+ name,
1960
+ path
1961
+ );
1962
+ swapViewInParent(this, newView);
1963
+ return newView.refinement.asReferenceViewDefinition();
1964
+ }
1965
+
1966
+ isValidViewRefinement(
1967
+ name: string,
1968
+ path?: string[]
1969
+ ): {
1970
+ isValidViewRefinement: boolean;
1971
+ error?: string;
1972
+ } {
1973
+ return isValidViewRefinement(this, name, path);
1974
+ }
1975
+
1976
+ getInputSchema(): Malloy.Schema {
1977
+ return getInputSchemaFromViewParent(this.parent as ViewParent);
1978
+ }
1979
+
1980
+ getOutputSchema(): Malloy.Schema {
1981
+ const parent = this.parent as ViewParent;
1982
+ return parent.getOutputSchema();
1983
+ }
1984
+
1985
+ getImplicitName(): string | undefined {
1986
+ return this.name;
1987
+ }
1988
+
1989
+ getViewInfo(): Malloy.FieldInfoWithView {
1990
+ const schema = this.getInputSchema();
1991
+ const view = ASTNode.schemaGet(schema, this.name, this.path);
1992
+ if (view.kind !== 'view') {
1993
+ throw new Error('Not a view');
1994
+ }
1995
+ return view;
1996
+ }
1997
+
1998
+ getRefinementSchema(): Malloy.Schema {
1999
+ const view = this.getViewInfo();
2000
+ return view.schema;
2001
+ }
2002
+
2003
+ /**
2004
+ * @internal
2005
+ */
2006
+ propagateUp(f: PropagationFunction): void {
2007
+ (this.parent as ViewParent).propagateUp(f);
2008
+ }
2009
+
2010
+ /**
2011
+ * @internal
2012
+ */
2013
+ propagateDown(_f: PropagationFunction): void {
2014
+ return;
2015
+ }
2016
+
2017
+ getInheritedAnnotations(): Malloy.Annotation[] {
2018
+ const view = this.getViewInfo();
2019
+ return view.annotations ?? [];
2020
+ }
2021
+
2022
+ public getOrAddParameters() {
2023
+ return ASTReference.getOrAddParameters(this);
2024
+ }
2025
+
2026
+ public setParameter(name: string, value: RawLiteralValue) {
2027
+ return ASTReference.setParameter(this, name, value);
2028
+ }
2029
+
2030
+ public tryGetParameter(name: string): ASTParameterValue | undefined {
2031
+ return ASTReference.tryGetParameter(this, name);
2032
+ }
2033
+ }
2034
+
2035
+ export class ASTArrowViewDefinition
2036
+ extends ASTObjectNode<
2037
+ Malloy.ViewDefinitionWithArrow,
2038
+ {
2039
+ kind: 'arrow';
2040
+ source: ASTViewDefinition;
2041
+ view: ASTViewDefinition;
2042
+ }
2043
+ >
2044
+ implements IASTViewDefinition
2045
+ {
2046
+ constructor(public node: Malloy.ViewDefinitionWithArrow) {
2047
+ super(node, {
2048
+ kind: 'arrow',
2049
+ source: ASTViewDefinition.from(node.source),
2050
+ view: ASTViewDefinition.from(node.view),
2051
+ });
2052
+ }
2053
+
2054
+ isRunnable(): boolean {
2055
+ return this.source.isRunnable() && this.view.isRunnable();
2056
+ }
2057
+
2058
+ get source() {
2059
+ return this.children.source;
2060
+ }
2061
+
2062
+ set source(source: ASTViewDefinition) {
2063
+ this.edit();
2064
+ this.children.source = source;
2065
+ }
2066
+
2067
+ get view() {
2068
+ return this.children.view;
2069
+ }
2070
+
2071
+ set view(view: ASTViewDefinition) {
2072
+ this.edit();
2073
+ this.children.view = view;
2074
+ view.parent = this;
2075
+ }
2076
+
2077
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition {
2078
+ return this.view.getOrAddDefaultSegment();
2079
+ }
2080
+
2081
+ addEmptyRefinement(): ASTSegmentViewDefinition {
2082
+ return this.view.addEmptyRefinement();
2083
+ }
2084
+
2085
+ addViewRefinement(name: string, path?: string[]): ASTReferenceViewDefinition {
2086
+ return this.view.addViewRefinement(name, path);
2087
+ }
2088
+
2089
+ getInputSchema(): Malloy.Schema {
2090
+ return this.source.getOutputSchema();
2091
+ }
2092
+
2093
+ getOutputSchema(): Malloy.Schema {
2094
+ return this.view.getRefinementSchema();
2095
+ }
2096
+
2097
+ getImplicitName(): string | undefined {
2098
+ return this.view.getImplicitName();
2099
+ }
2100
+
2101
+ getRefinementSchema(): Malloy.Schema {
2102
+ throw new Error('An arrow is not a valid refinement');
2103
+ }
2104
+
2105
+ isValidViewRefinement(
2106
+ name: string,
2107
+ path?: string[]
2108
+ ): {
2109
+ isValidViewRefinement: boolean;
2110
+ error?: string;
2111
+ } {
2112
+ return isValidViewRefinement(this, name, path);
2113
+ }
2114
+
2115
+ /**
2116
+ * @internal
2117
+ */
2118
+ propagateUp(f: PropagationFunction): void {
2119
+ this.propagateDown(f);
2120
+ }
2121
+
2122
+ /**
2123
+ * @internal
2124
+ */
2125
+ propagateDown(f: PropagationFunction): void {
2126
+ f(this.view);
2127
+ this.view.propagateDown(f);
2128
+ }
2129
+
2130
+ getInheritedAnnotations(): Malloy.Annotation[] {
2131
+ return [];
2132
+ }
2133
+ }
2134
+
2135
+ type ViewParent =
2136
+ | ASTArrowQueryDefinition
2137
+ | ASTRefinementQueryDefinition
2138
+ | ASTView
2139
+ | ASTArrowViewDefinition
2140
+ | ASTRefinementViewDefinition;
2141
+
2142
+ export class ASTRefinementViewDefinition
2143
+ extends ASTObjectNode<
2144
+ Malloy.ViewDefinitionWithRefinement,
2145
+ {
2146
+ kind: 'refinement';
2147
+ base: ASTViewDefinition;
2148
+ refinement: ASTViewDefinition;
2149
+ }
2150
+ >
2151
+ implements IASTViewDefinition
2152
+ {
2153
+ constructor(public node: Malloy.ViewDefinitionWithRefinement) {
2154
+ super(node, {
2155
+ kind: 'refinement',
2156
+ base: ASTViewDefinition.from(node.base),
2157
+ refinement: ASTViewDefinition.from(node.refinement),
2158
+ });
2159
+ }
2160
+
2161
+ isRunnable(): boolean {
2162
+ return this.base.isRunnable() && this.refinement.isRunnable();
2163
+ }
2164
+
2165
+ get refinement() {
2166
+ return this.children.refinement;
2167
+ }
2168
+
2169
+ set refinement(refinement: ASTViewDefinition) {
2170
+ this.edit();
2171
+ this.children.refinement = refinement;
2172
+ refinement.parent = this;
2173
+ }
2174
+
2175
+ get base() {
2176
+ return this.children.base;
2177
+ }
2178
+
2179
+ set base(base: ASTViewDefinition) {
2180
+ this.edit();
2181
+ this.children.base = base;
2182
+ }
2183
+
2184
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition {
2185
+ return this.refinement.getOrAddDefaultSegment();
2186
+ }
2187
+
2188
+ addEmptyRefinement(): ASTSegmentViewDefinition {
2189
+ return this.refinement.addEmptyRefinement();
2190
+ }
2191
+
2192
+ addViewRefinement(name: string, path?: string[]): ASTReferenceViewDefinition {
2193
+ return this.refinement.addViewRefinement(name, path);
2194
+ }
2195
+
2196
+ getInputSchema(): Malloy.Schema {
2197
+ return getInputSchemaFromViewParent(this.parent as ViewParent);
2198
+ }
2199
+
2200
+ getOutputSchema(): Malloy.Schema {
2201
+ const parent = this.parent as ViewParent;
2202
+ return parent.getOutputSchema();
2203
+ }
2204
+
2205
+ getRefinementSchema(): Malloy.Schema {
2206
+ return ASTNode.schemaMerge(
2207
+ this.base.getRefinementSchema(),
2208
+ this.refinement.getRefinementSchema()
2209
+ );
2210
+ }
2211
+
2212
+ getImplicitName(): string | undefined {
2213
+ return this.base.getImplicitName();
2214
+ }
2215
+
2216
+ isValidViewRefinement(
2217
+ name: string,
2218
+ path?: string[]
2219
+ ): {
2220
+ isValidViewRefinement: boolean;
2221
+ error?: string;
2222
+ } {
2223
+ return isValidViewRefinement(this, name, path);
2224
+ }
2225
+
2226
+ /**
2227
+ * @internal
2228
+ */
2229
+ propagateUp(f: PropagationFunction): void {
2230
+ (this.parent as ViewParent).propagateUp(f);
2231
+ }
2232
+
2233
+ /**
2234
+ * @internal
2235
+ */
2236
+ propagateDown(f: PropagationFunction): void {
2237
+ f(this.base);
2238
+ f(this.refinement);
2239
+ this.base.propagateDown(f);
2240
+ this.refinement.propagateDown(f);
2241
+ }
2242
+
2243
+ getInheritedAnnotations(): Malloy.Annotation[] {
2244
+ return this.base.getInheritedAnnotations();
2245
+ }
2246
+
2247
+ /**
2248
+ * @internal
2249
+ */
2250
+ static viewRefinementOf(
2251
+ view: Malloy.ViewDefinition,
2252
+ name: string,
2253
+ path?: string[]
2254
+ ) {
2255
+ return new ASTRefinementViewDefinition({
2256
+ kind: 'refinement',
2257
+ base: view,
2258
+ refinement: {
2259
+ kind: 'view_reference',
2260
+ name,
2261
+ path,
2262
+ },
2263
+ });
2264
+ }
2265
+
2266
+ /**
2267
+ * @internal
2268
+ */
2269
+ static segmentRefinementOf(view: Malloy.ViewDefinition) {
2270
+ return new ASTRefinementViewDefinition({
2271
+ kind: 'refinement',
2272
+ base: view,
2273
+ refinement: {
2274
+ kind: 'segment',
2275
+ operations: [],
2276
+ },
2277
+ });
2278
+ }
2279
+ }
2280
+
2281
+ export class ASTSegmentViewDefinition
2282
+ extends ASTObjectNode<
2283
+ Malloy.ViewDefinitionWithSegment,
2284
+ {
2285
+ kind: 'segment';
2286
+ operations: ASTViewOperationList;
2287
+ }
2288
+ >
2289
+ implements IASTViewDefinition
2290
+ {
2291
+ constructor(public node: Malloy.ViewDefinitionWithSegment) {
2292
+ super(node, {
2293
+ kind: 'segment',
2294
+ operations: new ASTViewOperationList(node.operations),
2295
+ });
2296
+ }
2297
+
2298
+ isRunnable(): boolean {
2299
+ for (const operation of this.operations.iter()) {
2300
+ if (
2301
+ operation instanceof ASTAggregateViewOperation ||
2302
+ operation instanceof ASTGroupByViewOperation ||
2303
+ operation instanceof ASTNestViewOperation
2304
+ ) {
2305
+ return true;
2306
+ }
2307
+ }
2308
+ return false;
2309
+ }
2310
+
2311
+ get operations() {
2312
+ return this.children.operations;
2313
+ }
2314
+
2315
+ /**
2316
+ * @internal
2317
+ */
2318
+ renameOrderBys(oldName: string, newName: string) {
2319
+ for (const operation of this.operations.iter()) {
2320
+ if (operation instanceof ASTOrderByViewOperation) {
2321
+ if (operation.name === oldName) {
2322
+ operation.setField(newName);
2323
+ }
2324
+ }
2325
+ }
2326
+ }
2327
+
2328
+ /**
2329
+ * @internal
2330
+ */
2331
+ propagateUp(f: PropagationFunction): void {
2332
+ (this.parent as ViewParent).propagateUp(f);
2333
+ }
2334
+
2335
+ /**
2336
+ * @internal
2337
+ */
2338
+ propagateDown(_f: PropagationFunction): void {
2339
+ return;
2340
+ }
2341
+
2342
+ /**
2343
+ * @internal
2344
+ */
2345
+ removeOrderBys(name: string): void {
2346
+ for (const operation of this.operations.iter()) {
2347
+ if (operation instanceof ASTOrderByViewOperation) {
2348
+ if (operation.name === name) {
2349
+ operation.delete();
2350
+ }
2351
+ }
2352
+ }
2353
+ }
2354
+
2355
+ reorderFields(names: string[]): void {
2356
+ const leadingOperations: ASTViewOperation[] = [];
2357
+ const trailingOperations: ASTViewOperation[] = [];
2358
+ const opsByName: {
2359
+ [name: string]:
2360
+ | ASTAggregateViewOperation
2361
+ | ASTGroupByViewOperation
2362
+ | ASTNestViewOperation;
2363
+ } = {};
2364
+ let seenAnyNamed = false;
2365
+ for (const operation of this.operations.iter()) {
2366
+ if (
2367
+ operation instanceof ASTAggregateViewOperation ||
2368
+ operation instanceof ASTGroupByViewOperation ||
2369
+ operation instanceof ASTNestViewOperation
2370
+ ) {
2371
+ if (names.includes(operation.name)) {
2372
+ opsByName[operation.name] = operation;
2373
+ seenAnyNamed = true;
2374
+ continue;
2375
+ }
2376
+ }
2377
+ if (seenAnyNamed) {
2378
+ trailingOperations.push(operation);
2379
+ } else {
2380
+ leadingOperations.push(operation);
2381
+ }
2382
+ }
2383
+ const middleOperations: ASTViewOperation[] = [];
2384
+ for (const name of names) {
2385
+ const operation = opsByName[name];
2386
+ if (operation === undefined) {
2387
+ throw new Error(`No field named ${name}`);
2388
+ }
2389
+ middleOperations.push(operation);
2390
+ }
2391
+ const operations = [
2392
+ ...leadingOperations,
2393
+ ...middleOperations,
2394
+ ...trailingOperations,
2395
+ ];
2396
+ this.operations.items = operations;
2397
+ }
2398
+
2399
+ public renameField(
2400
+ field:
2401
+ | ASTAggregateViewOperation
2402
+ | ASTGroupByViewOperation
2403
+ | ASTNestViewOperation,
2404
+ name: string
2405
+ ) {
2406
+ if (field.name === name) return;
2407
+ const output = this.getOutputSchema();
2408
+ if (ASTNode.schemaTryGet(output, name, [])) {
2409
+ throw new Error(`Output already has a field named ${name}`);
2410
+ }
2411
+ const oldName = field.name;
2412
+ field.name = name;
2413
+ this.propagateUp(v => {
2414
+ if (v instanceof ASTSegmentViewDefinition) {
2415
+ v.renameOrderBys(oldName, name);
2416
+ }
2417
+ });
2418
+ }
2419
+
2420
+ /**
2421
+ * Adds an order by to the segment. Will override the direction of an existing order by
2422
+ * if one is present for the same field.
2423
+ *
2424
+ * ```
2425
+ * run: flights -> { group_by: carrier }
2426
+ * ```
2427
+ * ```ts
2428
+ * q.getOrAddDefaultSegment().addOrderBy("carrier", Malloy.OrderByDirection.DESC);
2429
+ * ```
2430
+ * ```
2431
+ * run: flights -> {
2432
+ * group_by: carrier
2433
+ * order_by: carrier desc
2434
+ * }
2435
+ * ```
2436
+ *
2437
+ * The order by item is added after an existing order by operation if one is present,
2438
+ * or to a new order by operation at the end of the query.
2439
+ *
2440
+ * @param name The name of the field to order by.
2441
+ * @param direction The order by direction (ascending or descending).
2442
+ */
2443
+ public addOrderBy(name: string, direction?: Malloy.OrderByDirection) {
2444
+ // Ensure output schema has a field with this name
2445
+ const outputSchema = this.getOutputSchema();
2446
+ try {
2447
+ ASTNode.schemaGet(outputSchema, name, []);
2448
+ } catch {
2449
+ throw new Error(`No such field ${name} in stage output`);
2450
+ }
2451
+ // first see if there is already an order by for this field
2452
+ for (const operation of this.operations.iter()) {
2453
+ if (operation instanceof ASTOrderByViewOperation) {
2454
+ if (operation.name === name) {
2455
+ operation.direction = direction;
2456
+ return;
2457
+ }
2458
+ }
2459
+ }
2460
+ // add a new order by operation
2461
+ this.addOperation(
2462
+ new ASTOrderByViewOperation({
2463
+ kind: 'order_by',
2464
+ field_reference: {name},
2465
+ direction,
2466
+ })
2467
+ );
2468
+ }
2469
+
2470
+ /**
2471
+ * Adds an empty nest to this segment, with the given name.
2472
+ * @param name The name of the new nest.
2473
+ *
2474
+ * The new nest is always added to the end of the query in a new nest block.
2475
+ *
2476
+ * ```
2477
+ * run: flights -> { group_by: carrier }
2478
+ * ```
2479
+ * ```ts
2480
+ * q.getOrAddDefaultSegment().addEmptyNest("by_origin");
2481
+ * ```
2482
+ * ```
2483
+ * run: flights -> {
2484
+ * group_by: carrier
2485
+ * nest: by_origin is { }
2486
+ * }
2487
+ * ```
2488
+ *
2489
+ * @returns The {@link ASTNestViewOperation} that was created.
2490
+ *
2491
+ */
2492
+ public addEmptyNest(name: string): ASTNestViewOperation {
2493
+ const nest = new ASTNestViewOperation({
2494
+ kind: 'nest',
2495
+ name,
2496
+ view: {
2497
+ definition: {
2498
+ kind: 'segment',
2499
+ operations: [],
2500
+ },
2501
+ },
2502
+ });
2503
+ this.addOperation(nest);
2504
+ return nest;
2505
+ }
2506
+
2507
+ private firstIndexOfOperationType(type: Malloy.ViewOperationType) {
2508
+ return this.operations.findIndex(o => o.kind === type);
2509
+ }
2510
+
2511
+ private DEFAULT_INSERTION_ORDER: Malloy.ViewOperationType[] = [
2512
+ 'where',
2513
+ 'group_by',
2514
+ 'aggregate',
2515
+ 'nest',
2516
+ 'order_by',
2517
+ ];
2518
+
2519
+ private findInsertionPoint(kind: Malloy.ViewOperationType): number {
2520
+ const firstOfType = this.firstIndexOfOperationType(kind);
2521
+ if (firstOfType > -1) {
2522
+ let i = firstOfType;
2523
+ while (this.operations.index(i) && this.operations.index(i).kind === kind)
2524
+ i++;
2525
+ return i;
2526
+ }
2527
+ const indexInOrder = this.DEFAULT_INSERTION_ORDER.indexOf(kind);
2528
+ if (indexInOrder === -1) {
2529
+ throw new Error(
2530
+ `Operation ${kind} is not supported for \`findInsertionPoint\``
2531
+ );
2532
+ }
2533
+ const laterOperations = this.DEFAULT_INSERTION_ORDER.slice(
2534
+ indexInOrder + 1
2535
+ );
2536
+ for (const laterType of laterOperations) {
2537
+ const firstOfType = this.firstIndexOfOperationType(laterType);
2538
+ return firstOfType;
2539
+ }
2540
+ return this.operations.length;
2541
+ }
2542
+
2543
+ public getFieldNamed(
2544
+ name: string
2545
+ ):
2546
+ | ASTGroupByViewOperation
2547
+ | ASTAggregateViewOperation
2548
+ | ASTNestViewOperation
2549
+ | undefined {
2550
+ const field = this.tryGetFieldNamed(name);
2551
+ if (field === undefined) throw new Error('No such field');
2552
+ return field;
2553
+ }
2554
+
2555
+ public hasFieldNamed(name: string): boolean {
2556
+ return this.tryGetFieldNamed(name) !== undefined;
2557
+ }
2558
+
2559
+ public hasField(name: string, path?: string[]): boolean {
2560
+ return this.tryGetField(name, path) !== undefined;
2561
+ }
2562
+
2563
+ public getField(
2564
+ name: string,
2565
+ path?: string[]
2566
+ ):
2567
+ | ASTGroupByViewOperation
2568
+ | ASTAggregateViewOperation
2569
+ | ASTNestViewOperation {
2570
+ const field = this.tryGetField(name, path);
2571
+ if (field === undefined) {
2572
+ throw new Error('No such field');
2573
+ }
2574
+ return field;
2575
+ }
2576
+
2577
+ // TODO what constitutes "having a field" -- does "dep_time.month" count as dep_time?
2578
+ // does flight_count {where: carrier = 'CA' } count as flight_count?
2579
+ public tryGetField(
2580
+ name: string,
2581
+ path?: string[]
2582
+ ):
2583
+ | ASTGroupByViewOperation
2584
+ | ASTAggregateViewOperation
2585
+ | ASTNestViewOperation
2586
+ | undefined {
2587
+ for (const operation of this.operations.iter()) {
2588
+ if (
2589
+ operation instanceof ASTGroupByViewOperation ||
2590
+ operation instanceof ASTAggregateViewOperation
2591
+ ) {
2592
+ const reference = operation.field.getReference();
2593
+ if (reference.name === name && pathsMatch(reference.path, path)) {
2594
+ return operation;
2595
+ }
2596
+ } else if (operation instanceof ASTNestViewOperation) {
2597
+ if (operation.view instanceof ASTReferenceViewDefinition) {
2598
+ return operation;
2599
+ }
2600
+ }
2601
+ }
2602
+ return undefined;
2603
+ }
2604
+
2605
+ public tryGetFieldNamed(
2606
+ name: string
2607
+ ):
2608
+ | ASTGroupByViewOperation
2609
+ | ASTAggregateViewOperation
2610
+ | ASTNestViewOperation
2611
+ | undefined {
2612
+ for (const operation of this.operations.iter()) {
2613
+ if (
2614
+ operation instanceof ASTGroupByViewOperation ||
2615
+ operation instanceof ASTAggregateViewOperation ||
2616
+ operation instanceof ASTNestViewOperation
2617
+ ) {
2618
+ if (operation.name === name) {
2619
+ return operation;
2620
+ }
2621
+ }
2622
+ }
2623
+ return undefined;
2624
+ }
2625
+
2626
+ public getGroupBy(name: string) {
2627
+ for (const operation of this.operations.iter()) {
2628
+ if (operation instanceof ASTGroupByViewOperation) {
2629
+ if (operation.name === name) {
2630
+ return operation;
2631
+ }
2632
+ }
2633
+ }
2634
+ }
2635
+
2636
+ public removeGroupBy(name: string) {
2637
+ this.getGroupBy(name)?.delete();
2638
+ return this;
2639
+ }
2640
+
2641
+ /**
2642
+ * Adds a group by field with the given name to this segment.
2643
+ *
2644
+ * ```
2645
+ * run: flights -> { }
2646
+ * ```
2647
+ * ```ts
2648
+ * q.getOrAddDefaultSegment().addGroupBy("carrier");
2649
+ * ```
2650
+ * ```
2651
+ * run: flights -> { group_by: carrier }
2652
+ * ```
2653
+ *
2654
+ * If there is already a group by clause, the new field will be added
2655
+ * to that clause (or the first one if there are multiple).
2656
+ *
2657
+ * ```
2658
+ * run: flights -> { group_by: carrier }
2659
+ * ```
2660
+ * ```ts
2661
+ * q.getOrAddDefaultSegment().addGroupBy("origin_code");
2662
+ * ```
2663
+ * ```
2664
+ * run: flights -> {
2665
+ * group_by:
2666
+ * carrier
2667
+ * origin_code
2668
+ * }
2669
+ * ```
2670
+ *
2671
+ * If there is no group by clause, it will be added
2672
+ * 1) before the first aggregate clause if there is one, or
2673
+ * 2) before the first nest clause if there is one, or
2674
+ * 3) before the first order by clause if ther is one, or
2675
+ * 4) at the end of the query
2676
+ *
2677
+ * ```
2678
+ * run: flights -> {
2679
+ * order_by: flight_count
2680
+ * aggregate: flight_count
2681
+ * }
2682
+ * ```
2683
+ * ```ts
2684
+ * q.getOrAddDefaultSegment().addGroupBy("carrier");
2685
+ * ```
2686
+ * ```
2687
+ * run: flights -> {
2688
+ * order_by: flight_count
2689
+ * group_by: carrier
2690
+ * aggregate: flight_count
2691
+ * }
2692
+ * ```
2693
+ *
2694
+ * @param name The name of the dimension to group by.
2695
+ * @param path Join path for this dimension.
2696
+ */
2697
+ public addGroupBy(name: string, path: string[] = [], rename?: string) {
2698
+ const item = this.makeField(name, path, rename, 'dimension');
2699
+ this.addOperation(item);
2700
+ return item;
2701
+ }
2702
+
2703
+ public addWhere(name: string, filter: ParsedFilter): ASTWhereViewOperation;
2704
+ public addWhere(name: string, filterString: string): ASTWhereViewOperation;
2705
+ public addWhere(
2706
+ name: string,
2707
+ path: string[],
2708
+ filter: ParsedFilter
2709
+ ): ASTWhereViewOperation;
2710
+ public addWhere(
2711
+ name: string,
2712
+ path: string[],
2713
+ filterString: string
2714
+ ): ASTWhereViewOperation;
2715
+ public addWhere(
2716
+ name: string,
2717
+ arg2: string[] | string | ParsedFilter,
2718
+ arg3?: string | ParsedFilter
2719
+ ): ASTWhereViewOperation {
2720
+ const path = Array.isArray(arg2) ? arg2 : [];
2721
+ const filter = arg3 === undefined ? (arg2 as string | ParsedFilter) : arg3;
2722
+ const filterString =
2723
+ typeof filter === 'string' ? filter : serializeFilter(filter);
2724
+ const schema = this.getInputSchema();
2725
+ // Validate name
2726
+ const field = ASTQuery.schemaGet(schema, name, path);
2727
+ // Validate filter
2728
+ validateFilter(field, filter);
2729
+ const item = new ASTWhereViewOperation({
2730
+ kind: 'where',
2731
+ filter: {
2732
+ kind: 'filter_string',
2733
+ field_reference: {name},
2734
+ filter: filterString,
2735
+ },
2736
+ });
2737
+ this.addOperation(item);
2738
+ return item;
2739
+ }
2740
+
2741
+ private addTimeGroupBy(
2742
+ name: string,
2743
+ path: string[],
2744
+ timeframe: Malloy.TimestampTimeframe,
2745
+ type: 'date_type' | 'timestamp_type'
2746
+ ): ASTGroupByViewOperation {
2747
+ const schema = this.getInputSchema();
2748
+ const fieldInfo = ASTNode.schemaGet(schema, name, path);
2749
+ if (fieldInfo === undefined) {
2750
+ throw new Error(`No such field ${name}`);
2751
+ }
2752
+ if (fieldInfo.kind !== 'dimension') {
2753
+ throw new Error(`Cannot group by non-dimension ${name}`);
2754
+ }
2755
+ if (fieldInfo.type.kind !== type) {
2756
+ throw new Error(`${name} is not a ${type}`);
2757
+ }
2758
+ const item = new ASTGroupByViewOperation({
2759
+ kind: 'group_by',
2760
+ field: {
2761
+ expression: {
2762
+ kind: 'time_truncation',
2763
+ field_reference: {name, path},
2764
+ truncation: timeframe,
2765
+ },
2766
+ },
2767
+ });
2768
+ this.addOperation(item);
2769
+ return item;
2770
+ }
2771
+
2772
+ public addDateGroupBy(
2773
+ name: string,
2774
+ path: string[],
2775
+ timeframe: Malloy.DateTimeframe
2776
+ ): ASTGroupByViewOperation;
2777
+ public addDateGroupBy(
2778
+ name: string,
2779
+ timeframe: Malloy.DateTimeframe
2780
+ ): ASTGroupByViewOperation;
2781
+ public addDateGroupBy(
2782
+ name: string,
2783
+ arg2: string[] | Malloy.DateTimeframe,
2784
+ arg3?: Malloy.DateTimeframe
2785
+ ): ASTGroupByViewOperation {
2786
+ const timeframe =
2787
+ arg3 === undefined ? (arg2 as Malloy.DateTimeframe) : arg3;
2788
+ const path = arg3 === undefined ? [] : (arg2 as string[]);
2789
+ return this.addTimeGroupBy(name, path, timeframe, 'date_type');
2790
+ }
2791
+
2792
+ public addTimestampGroupBy(
2793
+ name: string,
2794
+ path: string[],
2795
+ timeframe: Malloy.TimestampTimeframe
2796
+ ): ASTGroupByViewOperation;
2797
+ public addTimestampGroupBy(
2798
+ name: string,
2799
+ timeframe: Malloy.TimestampTimeframe
2800
+ ): ASTGroupByViewOperation;
2801
+ public addTimestampGroupBy(
2802
+ name: string,
2803
+ arg2: string[] | Malloy.TimestampTimeframe,
2804
+ arg3?: Malloy.TimestampTimeframe
2805
+ ): ASTGroupByViewOperation {
2806
+ const timeframe =
2807
+ arg3 === undefined ? (arg2 as Malloy.TimestampTimeframe) : arg3;
2808
+ const path = arg3 === undefined ? [] : (arg2 as string[]);
2809
+ return this.addTimeGroupBy(name, path, timeframe, 'timestamp_type');
2810
+ }
2811
+
2812
+ /**
2813
+ * Adds an aggregate item with the given name to this segment.
2814
+ *
2815
+ * ```
2816
+ * run: flights -> { }
2817
+ * ```
2818
+ * ```ts
2819
+ * q.getOrAddDefaultSegment().addAggregate("flight_count");
2820
+ * ```
2821
+ * ```
2822
+ * run: flights -> { aggregate: flight_count }
2823
+ * ```
2824
+ *
2825
+ * Added
2826
+ * 1) at the end of an existing aggregate clause if ther is one, or
2827
+ * 2) before the first nest clause if there is one, or
2828
+ * 3) before the first order by clause if ther is one, or
2829
+ * 4) at the end of the query
2830
+ *
2831
+ * @param name The name of the measure to aggregate.
2832
+ * @param path The join path of the measure to aggregate.
2833
+ * @param rename A new name for this measure
2834
+ */
2835
+ public addAggregate(name: string, path: string[] = [], rename?: string) {
2836
+ const item = this.makeField(name, path, rename, 'measure');
2837
+ this.addOperation(item);
2838
+ return item;
2839
+ }
2840
+
2841
+ /**
2842
+ * Adds a nest item with the given name to this segment.
2843
+ *
2844
+ * ```
2845
+ * run: flights -> { }
2846
+ * ```
2847
+ * ```ts
2848
+ * q.getOrAddDefaultSegment().addNest("by_carrier");
2849
+ * ```
2850
+ * ```
2851
+ * run: flights -> { nest: by_carrier }
2852
+ * ```
2853
+ *
2854
+ * Added
2855
+ * 1) at the end of an existing nest clause if there is one, or
2856
+ * 2) before the first order by clause if ther is one, or
2857
+ * 3) at the end of the query
2858
+ *
2859
+ * @param name The name of the view to nest.
2860
+ * @param rename A new name for this view in the query
2861
+ */
2862
+ public addNest(name: string, rename?: string) {
2863
+ const item = this.makeField(name, [], rename, 'view');
2864
+ this.addOperation(item);
2865
+ return item;
2866
+ }
2867
+
2868
+ private makeField(
2869
+ name: string,
2870
+ path: string[],
2871
+ rename: string | undefined,
2872
+ type: 'dimension'
2873
+ ): ASTGroupByViewOperation;
2874
+ private makeField(
2875
+ name: string,
2876
+ path: string[],
2877
+ rename: string | undefined,
2878
+ type: 'measure'
2879
+ ): ASTAggregateViewOperation;
2880
+ private makeField(
2881
+ name: string,
2882
+ path: string[],
2883
+ rename: string | undefined,
2884
+ type: 'view'
2885
+ ): ASTNestViewOperation;
2886
+ private makeField(
2887
+ name: string,
2888
+ path: string[],
2889
+ rename: string | undefined,
2890
+ type: 'dimension' | 'measure' | 'view'
2891
+ ) {
2892
+ const schema = this.getInputSchema();
2893
+ const fieldInfo = ASTNode.schemaGet(schema, name, path);
2894
+ if (fieldInfo === undefined) {
2895
+ throw new Error(`No such field ${name}`);
2896
+ }
2897
+ if (fieldInfo.kind !== type) {
2898
+ const action = fieldTypeToAction(type);
2899
+ const typeName = fieldTypeName(type);
2900
+ throw new Error(`Cannot ${action} non-${typeName} ${name}`);
2901
+ }
2902
+ if (type === 'dimension') {
2903
+ return ASTGroupByViewOperation.fromReference(name, path, rename);
2904
+ } else if (type === 'measure') {
2905
+ return ASTAggregateViewOperation.fromReference(name, path, rename);
2906
+ } else {
2907
+ return ASTNestViewOperation.fromReference(name, path, rename);
2908
+ }
2909
+ }
2910
+
2911
+ private addOperation(
2912
+ item:
2913
+ | ASTGroupByViewOperation
2914
+ | ASTAggregateViewOperation
2915
+ | ASTNestViewOperation
2916
+ | ASTWhereViewOperation
2917
+ | ASTOrderByViewOperation
2918
+ ) {
2919
+ if (
2920
+ item instanceof ASTGroupByViewOperation ||
2921
+ item instanceof ASTAggregateViewOperation ||
2922
+ item instanceof ASTNestViewOperation
2923
+ ) {
2924
+ if (this.hasFieldNamed(item.name)) {
2925
+ throw new Error(`Query already has a field named ${item.name}`);
2926
+ }
2927
+ }
2928
+ const whereToInsert = this.findInsertionPoint(item.kind);
2929
+ this.operations.insert(item, whereToInsert);
2930
+ return item;
2931
+ }
2932
+
2933
+ /**
2934
+ * @internal
2935
+ */
2936
+ getRefinementSchema(): Malloy.Schema {
2937
+ const fields: Malloy.FieldInfo[] = [];
2938
+ for (const operation of this.operations.iter()) {
2939
+ if (
2940
+ operation instanceof ASTGroupByViewOperation ||
2941
+ operation instanceof ASTAggregateViewOperation ||
2942
+ operation instanceof ASTNestViewOperation
2943
+ ) {
2944
+ // TODO convert measures into dimensions for output
2945
+ fields.push(operation.getFieldInfo());
2946
+ }
2947
+ }
2948
+ return {fields};
2949
+ }
2950
+
2951
+ /**
2952
+ * Sets the limit for this segment. Overrides an existing limit.
2953
+ *
2954
+ * ```
2955
+ * run: flights -> { group_by: carrier }
2956
+ * ```
2957
+ * ```ts
2958
+ * q.getOrAddDefaultSegment().setLimit(10);
2959
+ * ```
2960
+ * ```
2961
+ * run: flights -> {
2962
+ * group_by: carrier
2963
+ * limit: 10
2964
+ * }
2965
+ * ```
2966
+ *
2967
+ * @param limit The limit to set. Must be an integer.
2968
+ */
2969
+ public setLimit(limit: number) {
2970
+ ASTLimitViewOperation.validateLimit(limit);
2971
+ const limitOp: ASTLimitViewOperation | undefined = [
2972
+ ...this.operations.iter(),
2973
+ ].find(ASTViewOperation.isLimit);
2974
+ if (limitOp) {
2975
+ limitOp.limit = limit;
2976
+ } else {
2977
+ this.operations.add(
2978
+ new ASTLimitViewOperation({
2979
+ kind: 'limit',
2980
+ limit,
2981
+ })
2982
+ );
2983
+ }
2984
+ }
2985
+
2986
+ getOrAddDefaultSegment(): ASTSegmentViewDefinition {
2987
+ return this;
2988
+ }
2989
+
2990
+ addEmptyRefinement(): ASTSegmentViewDefinition {
2991
+ const view = ASTRefinementViewDefinition.segmentRefinementOf(this.build());
2992
+ swapViewInParent(this, view);
2993
+ return view.refinement.asSegmentViewDefinition();
2994
+ }
2995
+
2996
+ addViewRefinement(name: string, path?: string[]): ASTReferenceViewDefinition {
2997
+ const {error} = this.isValidViewRefinement(name, path);
2998
+ if (error) {
2999
+ throw new Error(error);
3000
+ }
3001
+ const view = ASTRefinementViewDefinition.viewRefinementOf(
3002
+ this.build(),
3003
+ name,
3004
+ path
3005
+ );
3006
+ swapViewInParent(this, view);
3007
+ return view.refinement.asReferenceViewDefinition();
3008
+ }
3009
+
3010
+ getInputSchema(): Malloy.Schema {
3011
+ return getInputSchemaFromViewParent(this.parent as ViewParent);
3012
+ }
3013
+
3014
+ getOutputSchema(): Malloy.Schema {
3015
+ const parent = this.parent as ViewParent;
3016
+ return parent.getOutputSchema();
3017
+ }
3018
+
3019
+ getImplicitName(): string | undefined {
3020
+ return undefined;
3021
+ }
3022
+
3023
+ isValidViewRefinement(
3024
+ name: string,
3025
+ path?: string[]
3026
+ ): {
3027
+ isValidViewRefinement: boolean;
3028
+ error?: string;
3029
+ } {
3030
+ return isValidViewRefinement(this, name, path);
3031
+ }
3032
+
3033
+ getInheritedAnnotations(): Malloy.Annotation[] {
3034
+ return [];
3035
+ }
3036
+ }
3037
+
3038
+ export class ASTViewOperationList extends ASTListNode<
3039
+ Malloy.ViewOperation,
3040
+ ASTViewOperation
3041
+ > {
3042
+ constructor(operations: Malloy.ViewOperation[]) {
3043
+ super(
3044
+ operations,
3045
+ operations.map(p => ASTViewOperation.from(p))
3046
+ );
3047
+ }
3048
+
3049
+ get items() {
3050
+ return this.children;
3051
+ }
3052
+
3053
+ /**
3054
+ * @internal
3055
+ */
3056
+ set items(operations: ASTViewOperation[]) {
3057
+ this.edit();
3058
+ this.children = operations;
3059
+ }
3060
+
3061
+ /**
3062
+ * @internal
3063
+ */
3064
+ get segment() {
3065
+ return this.parent.asSegmentViewDefinition();
3066
+ }
3067
+ }
3068
+
3069
+ export type ASTViewOperation =
3070
+ | ASTGroupByViewOperation
3071
+ | ASTAggregateViewOperation
3072
+ | ASTOrderByViewOperation
3073
+ | ASTNestViewOperation
3074
+ | ASTLimitViewOperation
3075
+ | ASTWhereViewOperation;
3076
+ export const ASTViewOperation = {
3077
+ from(value: Malloy.ViewOperation): ASTViewOperation {
3078
+ switch (value.kind) {
3079
+ case 'group_by':
3080
+ return new ASTGroupByViewOperation(value);
3081
+ case 'aggregate':
3082
+ return new ASTAggregateViewOperation(value);
3083
+ case 'order_by':
3084
+ return new ASTOrderByViewOperation(value);
3085
+ case 'nest':
3086
+ return new ASTNestViewOperation(value);
3087
+ case 'limit':
3088
+ return new ASTLimitViewOperation(value);
3089
+ case 'where':
3090
+ return new ASTWhereViewOperation(value);
3091
+ }
3092
+ },
3093
+ isLimit(x: ASTViewOperation): x is ASTLimitViewOperation {
3094
+ return x instanceof ASTLimitViewOperation;
3095
+ },
3096
+ };
3097
+
3098
+ export interface IASTAnnotatable {
3099
+ getOrAddAnnotations(): ASTAnnotationList;
3100
+ getInheritedTag(prefix: RegExp | string): Tag;
3101
+ getIntrinsicTag(prefix: RegExp | string): Tag;
3102
+ getTag(prefix: RegExp | string): Tag;
3103
+ setTagProperty(path: Path, value: TagSetValue, prefix: string): void;
3104
+ removeTagProperty(path: Path, prefix: string): void;
3105
+ }
3106
+
3107
+ export class ASTOrderByViewOperation extends ASTObjectNode<
3108
+ Malloy.ViewOperationWithOrderBy,
3109
+ {
3110
+ kind: 'order_by';
3111
+ field_reference: ASTFieldReference;
3112
+ direction?: Malloy.OrderByDirection;
3113
+ }
3114
+ > {
3115
+ readonly kind: Malloy.ViewOperationType = 'order_by';
3116
+ constructor(public node: Malloy.ViewOperationWithOrderBy) {
3117
+ super(node, {
3118
+ kind: 'order_by',
3119
+ field_reference: new ASTFieldReference(node.field_reference),
3120
+ direction: node.direction,
3121
+ });
3122
+ }
3123
+
3124
+ get fieldReference() {
3125
+ return this.children.field_reference;
3126
+ }
3127
+
3128
+ get name() {
3129
+ return this.fieldReference.name;
3130
+ }
3131
+
3132
+ get direction() {
3133
+ return this.children.direction;
3134
+ }
3135
+
3136
+ set direction(direction: Malloy.OrderByDirection | undefined) {
3137
+ if (this.direction === direction) return;
3138
+ this.edit();
3139
+ this.children.direction = direction;
3140
+ }
3141
+
3142
+ get list() {
3143
+ return this.parent.asViewOperationList();
3144
+ }
3145
+
3146
+ delete() {
3147
+ const list = this.list;
3148
+ list.remove(this);
3149
+ }
3150
+
3151
+ setField(name: string) {
3152
+ const schema = this.list.segment.getOutputSchema();
3153
+ ASTNode.schemaGet(schema, name, []);
3154
+ this.edit();
3155
+ this.children.field_reference = new ASTFieldReference({name});
3156
+ }
3157
+
3158
+ setDirection(direction: Malloy.OrderByDirection | undefined) {
3159
+ this.direction = direction;
3160
+ }
3161
+ }
3162
+
3163
+ export class ASTGroupByViewOperation
3164
+ extends ASTObjectNode<
3165
+ Malloy.ViewOperationWithGroupBy,
3166
+ {
3167
+ kind: 'group_by';
3168
+ name?: string;
3169
+ field: ASTField;
3170
+ }
3171
+ >
3172
+ implements IASTAnnotatable
3173
+ {
3174
+ readonly kind: Malloy.ViewOperationType = 'group_by';
3175
+ constructor(public node: Malloy.ViewOperationWithGroupBy) {
3176
+ super(node, {
3177
+ kind: 'group_by',
3178
+ name: node.name,
3179
+ field: new ASTField(node.field),
3180
+ });
3181
+ }
3182
+
3183
+ get field() {
3184
+ return this.children.field;
3185
+ }
3186
+
3187
+ get name() {
3188
+ return this.children.name ?? this.field.name;
3189
+ }
3190
+
3191
+ set name(name: string) {
3192
+ if (this.name === name) return;
3193
+ this.edit();
3194
+ if (this.field.name === name) {
3195
+ this.children.name = undefined;
3196
+ } else {
3197
+ this.children.name = name;
3198
+ }
3199
+ }
3200
+
3201
+ /**
3202
+ * @internal
3203
+ */
3204
+ get list() {
3205
+ return this.parent.asViewOperationList();
3206
+ }
3207
+
3208
+ /**
3209
+ * Renames the group by item. If the field's name matches the given name,
3210
+ * removes the `name is` part.
3211
+ *
3212
+ * ```
3213
+ * run: flights -> { group_by: carrier }
3214
+ * ```
3215
+ * ```ts
3216
+ * groupBy.rename("carrier_2");
3217
+ * ```
3218
+ * ```
3219
+ * run: flights -> { group_by: carrier2 is carrier }
3220
+ * ```
3221
+ *
3222
+ * ```
3223
+ * run: flights -> { group_by: renamed is carrier }
3224
+ * ```
3225
+ * ```ts
3226
+ * groupBy.rename("carrier");
3227
+ * ```
3228
+ * ```
3229
+ * run: flights -> { group_by: carrier }
3230
+ * ```
3231
+ *
3232
+ *
3233
+ * @param name The new name
3234
+ */
3235
+ rename(name: string) {
3236
+ this.list.segment.renameField(this, name);
3237
+ }
3238
+
3239
+ /**
3240
+ * Delete this group by item.
3241
+ *
3242
+ * Possible side effects:
3243
+ * - If this was the last item in the group by operation, the whole
3244
+ * operation is removed.
3245
+ * - Any order by that references this group by item will be removed.
3246
+ *
3247
+ * ```
3248
+ * run: flights -> {
3249
+ * group_by: carrier
3250
+ * aggregate: flight_count
3251
+ * order by:
3252
+ * flight_count desc
3253
+ * carrier asc
3254
+ * }
3255
+ * ```
3256
+ * ```ts
3257
+ * groupBy.delete();
3258
+ * ```
3259
+ * ```
3260
+ * run: flights -> {
3261
+ * aggregate: flight_count
3262
+ * order by: flight_count desc
3263
+ * }
3264
+ * ```
3265
+ *
3266
+ */
3267
+ delete() {
3268
+ this.list.remove(this);
3269
+ this.list.segment.propagateUp(v => {
3270
+ if (v instanceof ASTSegmentViewDefinition) {
3271
+ v.removeOrderBys(this.name);
3272
+ }
3273
+ });
3274
+ }
3275
+
3276
+ getFieldInfo(): Malloy.FieldInfo {
3277
+ return {
3278
+ kind: 'dimension',
3279
+ name: this.name,
3280
+ type: this.field.type,
3281
+ };
3282
+ }
3283
+
3284
+ private get annotations() {
3285
+ return this.field.annotations;
3286
+ }
3287
+
3288
+ private set annotations(annotations: ASTAnnotationList | undefined) {
3289
+ this.edit();
3290
+ this.field.annotations = annotations;
3291
+ }
3292
+
3293
+ getOrAddAnnotations() {
3294
+ return this.field.getOrAddAnnotations();
3295
+ }
3296
+
3297
+ getInheritedTag(prefix: RegExp | string = '# ') {
3298
+ return tagFromAnnotations(prefix, this.field.getInheritedAnnotations());
3299
+ }
3300
+
3301
+ getIntrinsicTag(prefix: RegExp | string = '# ') {
3302
+ return this.annotations?.getIntrinsicTag(prefix) ?? new Tag();
3303
+ }
3304
+
3305
+ getTag(prefix: RegExp | string = '# ') {
3306
+ return this.annotations?.getTag(prefix) ?? this.getInheritedTag(prefix);
3307
+ }
3308
+
3309
+ setTagProperty(path: Path, value: TagSetValue = null, prefix = '# ') {
3310
+ this.getOrAddAnnotations().setTagProperty(path, value, prefix);
3311
+ }
3312
+
3313
+ removeTagProperty(path: Path, prefix = '# ') {
3314
+ if (!this.getTag().has(...path)) return;
3315
+ this.getOrAddAnnotations().removeTagProperty(path, prefix);
3316
+ }
3317
+
3318
+ /**
3319
+ * @internal
3320
+ */
3321
+ static fromReference(
3322
+ name: string,
3323
+ path: string[] | undefined,
3324
+ rename: string | undefined
3325
+ ) {
3326
+ return new ASTGroupByViewOperation({
3327
+ kind: 'group_by',
3328
+ name: rename,
3329
+ field: {
3330
+ expression: {
3331
+ kind: 'field_reference',
3332
+ name,
3333
+ path,
3334
+ },
3335
+ },
3336
+ });
3337
+ }
3338
+ }
3339
+
3340
+ export class ASTAggregateViewOperation
3341
+ extends ASTObjectNode<
3342
+ Malloy.ViewOperationWithAggregate,
3343
+ {
3344
+ kind: 'aggregate';
3345
+ name?: string;
3346
+ field: ASTField;
3347
+ }
3348
+ >
3349
+ implements IASTAnnotatable
3350
+ {
3351
+ readonly kind: Malloy.ViewOperationType = 'aggregate';
3352
+ constructor(public node: Malloy.ViewOperationWithAggregate) {
3353
+ super(node, {
3354
+ kind: 'aggregate',
3355
+ name: node.name,
3356
+ field: new ASTField(node.field),
3357
+ });
3358
+ }
3359
+
3360
+ get field() {
3361
+ return this.children.field;
3362
+ }
3363
+
3364
+ get name() {
3365
+ return this.children.name ?? this.field.name;
3366
+ }
3367
+
3368
+ set name(name: string) {
3369
+ if (this.name === name) return;
3370
+ this.edit();
3371
+ if (this.field.name === name) {
3372
+ this.children.name = undefined;
3373
+ } else {
3374
+ this.children.name = name;
3375
+ }
3376
+ }
3377
+
3378
+ get annotations() {
3379
+ return this.field.annotations;
3380
+ }
3381
+
3382
+ /**
3383
+ * Renames the aggregate item. If the field's name matches the given name,
3384
+ * removes the `name is` part.
3385
+ *
3386
+ * ```
3387
+ * run: flights -> { aggregate: flight_count }
3388
+ * ```
3389
+ * ```ts
3390
+ * aggregate.rename("flight_count_2");
3391
+ * ```
3392
+ * ```
3393
+ * run: flights -> { aggregate: flight_count2 is flight_count }
3394
+ * ```
3395
+ *
3396
+ * ```
3397
+ * run: flights -> { aggregate: renamed is flight_count }
3398
+ * ```
3399
+ * ```ts
3400
+ * aggregate.rename("flight_count");
3401
+ * ```
3402
+ * ```
3403
+ * run: flights -> { aggregate: flight_count }
3404
+ * ```
3405
+ *
3406
+ *
3407
+ * @param name The new name
3408
+ */
3409
+ rename(name: string) {
3410
+ this.list.segment.renameField(this, name);
3411
+ }
3412
+
3413
+ /**
3414
+ * @internal
3415
+ */
3416
+ get list() {
3417
+ return this.parent.asViewOperationList();
3418
+ }
3419
+
3420
+ delete() {
3421
+ this.list.remove(this);
3422
+ this.list.segment.propagateUp(v => {
3423
+ if (v instanceof ASTSegmentViewDefinition) {
3424
+ v.removeOrderBys(this.name);
3425
+ }
3426
+ });
3427
+ }
3428
+
3429
+ getFieldInfo(): Malloy.FieldInfo {
3430
+ return {
3431
+ kind: 'dimension',
3432
+ name: this.name,
3433
+ type: this.field.type,
3434
+ };
3435
+ }
3436
+
3437
+ getOrAddAnnotations() {
3438
+ return this.field.getOrAddAnnotations();
3439
+ }
3440
+
3441
+ getInheritedTag(prefix: RegExp | string = '# ') {
3442
+ return tagFromAnnotations(prefix, this.field.getInheritedAnnotations());
3443
+ }
3444
+
3445
+ getIntrinsicTag(prefix: RegExp | string = '# ') {
3446
+ return this.annotations?.getIntrinsicTag(prefix) ?? new Tag();
3447
+ }
3448
+
3449
+ getTag(prefix: RegExp | string = '# ') {
3450
+ return this.annotations?.getTag(prefix) ?? this.getInheritedTag(prefix);
3451
+ }
3452
+
3453
+ setTagProperty(path: Path, value: TagSetValue = null, prefix = '# ') {
3454
+ this.getOrAddAnnotations().setTagProperty(path, value, prefix);
3455
+ }
3456
+
3457
+ removeTagProperty(path: Path, prefix = '# ') {
3458
+ if (!this.getTag().has(...path)) return;
3459
+ this.getOrAddAnnotations().removeTagProperty(path, prefix);
3460
+ }
3461
+
3462
+ addWhere(
3463
+ name: string,
3464
+ path: string[],
3465
+ filterString: string
3466
+ ): ASTFilteredFieldExpression;
3467
+ addWhere(
3468
+ name: string,
3469
+ path: string[],
3470
+ filter: ParsedFilter
3471
+ ): ASTFilteredFieldExpression;
3472
+ addWhere(name: string, filterString: string): ASTFilteredFieldExpression;
3473
+ addWhere(name: string, filter: ParsedFilter): ASTFilteredFieldExpression;
3474
+ addWhere(
3475
+ name: string,
3476
+ arg2: string[] | string | ParsedFilter,
3477
+ arg3?: string | ParsedFilter
3478
+ ): ASTFilteredFieldExpression {
3479
+ const path = Array.isArray(arg2) ? arg2 : [];
3480
+ const filter = arg3 === undefined ? (arg2 as string | ParsedFilter) : arg3;
3481
+ const filterString =
3482
+ typeof filter === 'string' ? filter : serializeFilter(filter);
3483
+ const schema = this.list.segment.getInputSchema();
3484
+ const field = ASTQuery.schemaGet(schema, name, path);
3485
+ // Validate filter
3486
+ validateFilter(field, filter);
3487
+ const where: Malloy.Where = {
3488
+ filter: {
3489
+ kind: 'filter_string',
3490
+ field_reference: {name},
3491
+ filter: filterString,
3492
+ },
3493
+ };
3494
+ if (this.field.expression instanceof ASTFilteredFieldExpression) {
3495
+ this.field.expression.where.add(new ASTWhere(where));
3496
+ return this.field.expression;
3497
+ } else if (this.field.expression instanceof ASTReferenceExpression) {
3498
+ const existing = this.field.expression.build();
3499
+ this.field.expression = new ASTFilteredFieldExpression({
3500
+ kind: 'filtered_field',
3501
+ field_reference: {
3502
+ name: existing.name,
3503
+ path: existing.path,
3504
+ parameters: existing.parameters,
3505
+ },
3506
+ where: [where],
3507
+ });
3508
+ return this.field.expression;
3509
+ } else {
3510
+ throw new Error('This kind of expression does not support addWhere');
3511
+ }
3512
+ }
3513
+
3514
+ /**
3515
+ * @internal
3516
+ */
3517
+ static fromReference(
3518
+ name: string,
3519
+ path: string[] | undefined,
3520
+ rename: string | undefined
3521
+ ) {
3522
+ return new ASTAggregateViewOperation({
3523
+ kind: 'aggregate',
3524
+ name: rename,
3525
+ field: {
3526
+ expression: {
3527
+ kind: 'field_reference',
3528
+ name,
3529
+ path,
3530
+ },
3531
+ },
3532
+ });
3533
+ }
3534
+ }
3535
+
3536
+ export class ASTField
3537
+ extends ASTObjectNode<
3538
+ Malloy.Field,
3539
+ {
3540
+ expression: ASTExpression;
3541
+ annotations?: ASTAnnotationList;
3542
+ }
3543
+ >
3544
+ implements IASTAnnotatable
3545
+ {
3546
+ constructor(public node: Malloy.Field) {
3547
+ super(node, {
3548
+ expression: ASTExpression.from(node.expression),
3549
+ annotations: node.annotations && new ASTAnnotationList(node.annotations),
3550
+ });
3551
+ }
3552
+
3553
+ get expression() {
3554
+ return this.children.expression;
3555
+ }
3556
+
3557
+ set expression(expression: ASTExpression) {
3558
+ this.edit();
3559
+ this.children.expression = expression;
3560
+ expression.parent = this;
3561
+ }
3562
+
3563
+ get name() {
3564
+ return this.expression.name;
3565
+ }
3566
+
3567
+ get type() {
3568
+ return this.expression.fieldType;
3569
+ }
3570
+
3571
+ get annotations() {
3572
+ return this.children.annotations;
3573
+ }
3574
+
3575
+ set annotations(annotations: ASTAnnotationList | undefined) {
3576
+ this.edit();
3577
+ this.children.annotations = annotations;
3578
+ }
3579
+
3580
+ // Returns a Malloy reference that this field points to
3581
+ getReference() {
3582
+ return this.expression.getReference();
3583
+ }
3584
+
3585
+ getOrAddAnnotations() {
3586
+ if (this.annotations) return this.annotations;
3587
+ this.edit();
3588
+ const annotations = new ASTAnnotationList([]);
3589
+ this.children.annotations = annotations;
3590
+ return annotations;
3591
+ }
3592
+
3593
+ getInheritedTag(prefix: RegExp | string = '# ') {
3594
+ return tagFromAnnotations(prefix, this.getInheritedAnnotations());
3595
+ }
3596
+
3597
+ getIntrinsicTag(prefix: RegExp | string = '# ') {
3598
+ return this.annotations?.getIntrinsicTag(prefix) ?? new Tag();
3599
+ }
3600
+
3601
+ getTag(prefix: RegExp | string = '# ') {
3602
+ return this.annotations?.getTag(prefix) ?? this.getInheritedTag(prefix);
3603
+ }
3604
+
3605
+ setTagProperty(path: Path, value: TagSetValue = null, prefix = '# ') {
3606
+ this.getOrAddAnnotations().setTagProperty(path, value, prefix);
3607
+ }
3608
+
3609
+ removeTagProperty(path: Path, prefix = '# ') {
3610
+ if (!this.getTag().has(...path)) return;
3611
+ this.getOrAddAnnotations().removeTagProperty(path, prefix);
3612
+ }
3613
+
3614
+ /**
3615
+ * @internal
3616
+ */
3617
+ get segment() {
3618
+ const groupByOrAggregate = this.parent as
3619
+ | ASTGroupByViewOperation
3620
+ | ASTAggregateViewOperation;
3621
+ const operationList = groupByOrAggregate.list;
3622
+ return operationList.segment;
3623
+ }
3624
+
3625
+ getInheritedAnnotations(): Malloy.Annotation[] {
3626
+ return this.expression.getInheritedAnnotations();
3627
+ }
3628
+ }
3629
+
3630
+ export type ASTExpression =
3631
+ | ASTReferenceExpression
3632
+ | ASTFilteredFieldExpression
3633
+ | ASTTimeTruncationExpression;
3634
+ export const ASTExpression = {
3635
+ from(value: Malloy.Expression): ASTExpression {
3636
+ switch (value.kind) {
3637
+ case 'field_reference':
3638
+ return new ASTReferenceExpression(value);
3639
+ case 'filtered_field':
3640
+ return new ASTFilteredFieldExpression(value);
3641
+ case 'time_truncation':
3642
+ return new ASTTimeTruncationExpression(value);
3643
+ }
3644
+ },
3645
+ };
3646
+
3647
+ // TODO would be nice for this to extend ASTFieldReference?
3648
+ export class ASTReferenceExpression
3649
+ extends ASTObjectNode<
3650
+ Malloy.ExpressionWithFieldReference,
3651
+ {
3652
+ kind: 'field_reference';
3653
+ name: string;
3654
+ path?: string[];
3655
+ parameters?: ASTParameterValueList;
3656
+ }
3657
+ >
3658
+ implements IASTReference
3659
+ {
3660
+ readonly kind: Malloy.ExpressionType = 'field_reference';
3661
+
3662
+ constructor(public node: Malloy.ExpressionWithFieldReference) {
3663
+ super(node, {
3664
+ kind: node.kind,
3665
+ name: node.name,
3666
+ path: node.path,
3667
+ parameters: node.parameters && new ASTParameterValueList(node.parameters),
3668
+ });
3669
+ }
3670
+
3671
+ get name() {
3672
+ return this.children.name;
3673
+ }
3674
+
3675
+ get parameters() {
3676
+ return this.children.parameters;
3677
+ }
3678
+
3679
+ set parameters(parameters: ASTParameterValueList | undefined) {
3680
+ this.edit();
3681
+ this.children.parameters = parameters;
3682
+ }
3683
+
3684
+ /**
3685
+ * @internal
3686
+ */
3687
+ get field() {
3688
+ return this.parent.asField();
3689
+ }
3690
+
3691
+ get path() {
3692
+ return this.children.path;
3693
+ }
3694
+
3695
+ getReference() {
3696
+ return this.build();
3697
+ }
3698
+
3699
+ getFieldInfo(): Malloy.FieldInfoWithDimension | Malloy.FieldInfoWithMeasure {
3700
+ const schema = this.field.segment.getInputSchema();
3701
+ const def = ASTNode.schemaGet(schema, this.name, this.path);
3702
+ if (def.kind !== 'dimension' && def.kind !== 'measure') {
3703
+ throw new Error('Invalid field for ASTReferenceExpression');
3704
+ }
3705
+ return def;
3706
+ }
3707
+
3708
+ get fieldType() {
3709
+ return this.getFieldInfo().type;
3710
+ }
3711
+
3712
+ getInheritedAnnotations(): Malloy.Annotation[] {
3713
+ const field = this.getFieldInfo();
3714
+ return field.annotations ?? [];
3715
+ }
3716
+
3717
+ public getOrAddParameters() {
3718
+ return ASTReference.getOrAddParameters(this);
3719
+ }
3720
+
3721
+ public setParameter(name: string, value: RawLiteralValue) {
3722
+ return ASTReference.setParameter(this, name, value);
3723
+ }
3724
+
3725
+ public tryGetParameter(name: string): ASTParameterValue | undefined {
3726
+ return ASTReference.tryGetParameter(this, name);
3727
+ }
3728
+ }
3729
+
3730
+ export class ASTTimeTruncationExpression extends ASTObjectNode<
3731
+ Malloy.ExpressionWithTimeTruncation,
3732
+ {
3733
+ kind: 'time_truncation';
3734
+ field_reference: ASTFieldReference;
3735
+ truncation: Malloy.TimestampTimeframe;
3736
+ }
3737
+ > {
3738
+ readonly kind: Malloy.ExpressionType = 'time_truncation';
3739
+
3740
+ constructor(public node: Malloy.ExpressionWithTimeTruncation) {
3741
+ super(node, {
3742
+ kind: node.kind,
3743
+ field_reference: new ASTFieldReference(node.field_reference),
3744
+ truncation: node.truncation,
3745
+ });
3746
+ }
3747
+
3748
+ getReference() {
3749
+ return this.fieldReference.build();
3750
+ }
3751
+
3752
+ get fieldReference() {
3753
+ return this.children.field_reference;
3754
+ }
3755
+
3756
+ get truncation() {
3757
+ return this.children.truncation;
3758
+ }
3759
+
3760
+ get name() {
3761
+ return this.fieldReference.name;
3762
+ }
3763
+
3764
+ /**
3765
+ * @internal
3766
+ */
3767
+ get field() {
3768
+ return this.parent.asField();
3769
+ }
3770
+
3771
+ getFieldInfo(): Malloy.FieldInfoWithDimension | Malloy.FieldInfoWithMeasure {
3772
+ const schema = this.field.segment.getInputSchema();
3773
+ const def = ASTNode.schemaGet(schema, this.name, this.fieldReference.path);
3774
+ if (def.kind !== 'dimension' && def.kind !== 'measure') {
3775
+ throw new Error('Invalid field for ASTReferenceExpression');
3776
+ }
3777
+ return def;
3778
+ }
3779
+
3780
+ get fieldType() {
3781
+ const def = this.getFieldInfo();
3782
+ if (def.type.kind === 'date_type') {
3783
+ return {
3784
+ ...def.type,
3785
+ timeframe: timestampTimeframeToDateTimeframe(this.truncation),
3786
+ };
3787
+ } else if (def.type.kind === 'timestamp_type') {
3788
+ return {...def.type, timeframe: this.truncation};
3789
+ }
3790
+ throw new Error('This type of field cannot have a time truncation');
3791
+ }
3792
+
3793
+ getInheritedAnnotations(): Malloy.Annotation[] {
3794
+ const field = this.getFieldInfo();
3795
+ return field.annotations ?? [];
3796
+ }
3797
+ }
3798
+
3799
+ export class ASTWhere extends ASTObjectNode<Malloy.Where, {filter: ASTFilter}> {
3800
+ constructor(node: Malloy.Where) {
3801
+ super(node, {
3802
+ filter: ASTFilter.from(node.filter),
3803
+ });
3804
+ }
3805
+
3806
+ get filter() {
3807
+ return this.children.filter;
3808
+ }
3809
+
3810
+ get list() {
3811
+ return this.parent.asWhereList();
3812
+ }
3813
+
3814
+ delete() {
3815
+ this.list.remove(this);
3816
+ }
3817
+ }
3818
+
3819
+ export class ASTWhereList extends ASTListNode<Malloy.Where, ASTWhere> {
3820
+ constructor(wheres: Malloy.Where[]) {
3821
+ super(
3822
+ wheres,
3823
+ wheres.map(p => new ASTWhere(p))
3824
+ );
3825
+ }
3826
+
3827
+ get expression() {
3828
+ return this.parent.asFilteredFieldExpression();
3829
+ }
3830
+ }
3831
+
3832
+ export class ASTFilteredFieldExpression extends ASTObjectNode<
3833
+ Malloy.ExpressionWithFilteredField,
3834
+ {
3835
+ kind: 'filtered_field';
3836
+ field_reference: ASTFieldReference;
3837
+ where: ASTWhereList;
3838
+ }
3839
+ > {
3840
+ readonly kind: Malloy.ExpressionType = 'filtered_field';
3841
+
3842
+ constructor(public node: Malloy.ExpressionWithFilteredField) {
3843
+ super(node, {
3844
+ kind: node.kind,
3845
+ field_reference: new ASTFieldReference(node.field_reference),
3846
+ where: new ASTWhereList(node.where),
3847
+ });
3848
+ }
3849
+
3850
+ getReference() {
3851
+ return this.fieldReference.build();
3852
+ }
3853
+
3854
+ get fieldReference() {
3855
+ return this.children.field_reference;
3856
+ }
3857
+
3858
+ get name() {
3859
+ return this.fieldReference.name;
3860
+ }
3861
+
3862
+ get where() {
3863
+ return this.children.where;
3864
+ }
3865
+
3866
+ /**
3867
+ * @internal
3868
+ */
3869
+ get field() {
3870
+ return this.parent.asField();
3871
+ }
3872
+
3873
+ getFieldInfo(): Malloy.FieldInfoWithMeasure {
3874
+ const schema = this.field.segment.getInputSchema();
3875
+ const def = ASTNode.schemaGet(schema, this.name, this.fieldReference.path);
3876
+ if (def.kind !== 'measure') {
3877
+ throw new Error('Invalid field for ASTFilteredFieldExpression');
3878
+ }
3879
+ return def;
3880
+ }
3881
+
3882
+ get fieldType() {
3883
+ return this.getFieldInfo().type;
3884
+ }
3885
+
3886
+ getInheritedAnnotations(): Malloy.Annotation[] {
3887
+ const field = this.getFieldInfo();
3888
+ return field.annotations ?? [];
3889
+ }
3890
+ }
3891
+
3892
+ function timestampTimeframeToDateTimeframe(
3893
+ timeframe: Malloy.TimestampTimeframe
3894
+ ): Malloy.DateTimeframe {
3895
+ switch (timeframe) {
3896
+ case 'day':
3897
+ case 'week':
3898
+ case 'month':
3899
+ case 'year':
3900
+ case 'quarter':
3901
+ return timeframe;
3902
+ default:
3903
+ throw new Error('Invalid date timeframe');
3904
+ }
3905
+ }
3906
+
3907
+ export class ASTNestViewOperation
3908
+ extends ASTObjectNode<
3909
+ Malloy.ViewOperationWithNest,
3910
+ {
3911
+ kind: 'nest';
3912
+ name?: string;
3913
+ view: ASTView;
3914
+ }
3915
+ >
3916
+ implements IASTAnnotatable
3917
+ {
3918
+ readonly kind: Malloy.ViewOperationType = 'nest';
3919
+ constructor(public node: Malloy.ViewOperationWithNest) {
3920
+ super(node, {
3921
+ kind: 'nest',
3922
+ name: node.name,
3923
+ view: new ASTView(node.view),
3924
+ });
3925
+ }
3926
+
3927
+ get view() {
3928
+ return this.children.view;
3929
+ }
3930
+
3931
+ get annotations() {
3932
+ return this.view.annotations;
3933
+ }
3934
+
3935
+ get name() {
3936
+ const name = this.children.name ?? this.view.name;
3937
+ if (name === undefined) {
3938
+ throw new Error('Nest does not have a name');
3939
+ }
3940
+ return name;
3941
+ }
3942
+
3943
+ set name(name: string) {
3944
+ if (this.name === name) return;
3945
+ this.edit();
3946
+ if (this.view.name === name) {
3947
+ this.children.name = undefined;
3948
+ } else {
3949
+ this.children.name = name;
3950
+ }
3951
+ }
3952
+
3953
+ /**
3954
+ * @internal
3955
+ */
3956
+ get list() {
3957
+ return this.parent.asViewOperationList();
3958
+ }
3959
+
3960
+ getOrAddAnnotations() {
3961
+ return this.view.getOrAddAnnotations();
3962
+ }
3963
+
3964
+ getInheritedTag(prefix: RegExp | string = '# ') {
3965
+ return tagFromAnnotations(prefix, this.view.getInheritedAnnotations());
3966
+ }
3967
+
3968
+ getIntrinsicTag(prefix: RegExp | string = '# ') {
3969
+ return this.annotations?.getIntrinsicTag(prefix) ?? new Tag();
3970
+ }
3971
+
3972
+ getTag(prefix: RegExp | string = '# ') {
3973
+ return this.annotations?.getTag(prefix) ?? this.getInheritedTag(prefix);
3974
+ }
3975
+
3976
+ setTagProperty(path: Path, value: TagSetValue = null, prefix = '# ') {
3977
+ this.getOrAddAnnotations().setTagProperty(path, value, prefix);
3978
+ }
3979
+
3980
+ removeTagProperty(path: Path, prefix = '# ') {
3981
+ if (!this.getTag().has(...path)) return;
3982
+ this.getOrAddAnnotations().removeTagProperty(path, prefix);
3983
+ }
3984
+
3985
+ delete() {
3986
+ this.list.remove(this);
3987
+ }
3988
+
3989
+ /**
3990
+ * Renames the nest item. If the view's name matches the given name,
3991
+ * removes the `name is` part.
3992
+ *
3993
+ * ```
3994
+ * run: flights -> { nest: by_carrier }
3995
+ * ```
3996
+ * ```ts
3997
+ * nest.rename("by_carrier_2");
3998
+ * ```
3999
+ * ```
4000
+ * run: flights -> { nest: by_carrier_2 is by_carrier }
4001
+ * ```
4002
+ *
4003
+ * ```
4004
+ * run: flights -> { nest: by_carrier_2 is by_carrier }
4005
+ * ```
4006
+ * ```ts
4007
+ * nest.rename("by_carrier");
4008
+ * ```
4009
+ * ```
4010
+ * run: flights -> { nest: by_carrier }
4011
+ * ```
4012
+ *
4013
+ * ```
4014
+ * run: flights -> {
4015
+ * nest: by_carrier is {
4016
+ * group_by: carrier
4017
+ * }
4018
+ * }
4019
+ * ```
4020
+ * ```ts
4021
+ * nest.rename("by_carrier_2");
4022
+ * ```
4023
+ * ```
4024
+ * run: flights -> {
4025
+ * nest: by_carrier_2 is {
4026
+ * group_by: carrier
4027
+ * }
4028
+ * }
4029
+ * ```
4030
+ *
4031
+ * @param name The new name
4032
+ */
4033
+ rename(name: string) {
4034
+ this.list.segment.renameField(this, name);
4035
+ }
4036
+
4037
+ getFieldInfo(): Malloy.FieldInfo {
4038
+ return {
4039
+ kind: 'view',
4040
+ name: this.name,
4041
+ definition: this.view.build(),
4042
+ schema: this.view.getOutputSchema(),
4043
+ };
4044
+ }
4045
+
4046
+ /**
4047
+ * @internal
4048
+ */
4049
+ static fromReference(
4050
+ name: string,
4051
+ path: string[] | undefined,
4052
+ rename: string | undefined
4053
+ ) {
4054
+ return new ASTNestViewOperation({
4055
+ kind: 'nest',
4056
+ name: rename,
4057
+ view: {
4058
+ definition: {
4059
+ kind: 'view_reference',
4060
+ name,
4061
+ path,
4062
+ },
4063
+ },
4064
+ });
4065
+ }
4066
+ }
4067
+
4068
+ export class ASTWhereViewOperation extends ASTObjectNode<
4069
+ Malloy.ViewOperationWithWhere,
4070
+ {
4071
+ kind: 'where';
4072
+ filter: ASTFilter;
4073
+ }
4074
+ > {
4075
+ readonly kind: Malloy.ViewOperationType = 'nest';
4076
+ constructor(public node: Malloy.ViewOperationWithWhere) {
4077
+ super(node, {
4078
+ kind: 'where',
4079
+ filter: ASTFilter.from(node.filter),
4080
+ });
4081
+ }
4082
+
4083
+ get filter() {
4084
+ return this.children.filter;
4085
+ }
4086
+
4087
+ /**
4088
+ * @internal
4089
+ */
4090
+ get list() {
4091
+ return this.parent.asViewOperationList();
4092
+ }
4093
+
4094
+ delete() {
4095
+ this.list.remove(this);
4096
+ }
4097
+ }
4098
+
4099
+ export type ASTFilter = ASTFilterWithFilterString;
4100
+ export const ASTFilter = {
4101
+ from(filter: Malloy.Filter) {
4102
+ return new ASTFilterWithFilterString(filter);
4103
+ },
4104
+ };
4105
+
4106
+ export class ASTFilterWithFilterString extends ASTObjectNode<
4107
+ Malloy.FilterWithFilterString,
4108
+ {
4109
+ kind: 'filter_string';
4110
+ field_reference: ASTFieldReference;
4111
+ filter: string;
4112
+ }
4113
+ > {
4114
+ readonly kind: Malloy.FilterType = 'filter_string';
4115
+ constructor(public node: Malloy.FilterWithFilterString) {
4116
+ super(node, {
4117
+ kind: 'filter_string',
4118
+ field_reference: new ASTFieldReference(node.field_reference),
4119
+ filter: node.filter,
4120
+ });
4121
+ }
4122
+
4123
+ get fieldReference() {
4124
+ return this.children.field_reference;
4125
+ }
4126
+
4127
+ get filterString() {
4128
+ return this.children.filter;
4129
+ }
4130
+
4131
+ set filterString(filter: string) {
4132
+ this.edit();
4133
+ this.children.filter = filter;
4134
+ }
4135
+
4136
+ setFilterString(filterString: string) {
4137
+ const kind = this.getFilterType();
4138
+ parseFilter(this.filterString, kind);
4139
+ this.filterString = filterString;
4140
+ }
4141
+
4142
+ getFieldInfo() {
4143
+ const field = this.fieldReference.getFieldInfo();
4144
+ if (field.kind !== 'dimension' && field.kind !== 'measure') {
4145
+ throw new Error('Invalid field type for filter with filter string');
4146
+ }
4147
+ return field;
4148
+ }
4149
+
4150
+ getFilterType(): 'string' | 'boolean' | 'number' | 'date' | 'other' {
4151
+ const fieldInfo = this.getFieldInfo();
4152
+ return getFilterType(fieldInfo);
4153
+ }
4154
+
4155
+ setFilter(filter: ParsedFilter) {
4156
+ const kind = this.getFilterType();
4157
+ if (kind !== filter.kind) {
4158
+ throw new Error(
4159
+ `Invalid filter: expected type ${kind}, got ${filter.kind}`
4160
+ );
4161
+ }
4162
+ this.filterString = serializeFilter(filter);
4163
+ }
4164
+
4165
+ getFilter(): ParsedFilter {
4166
+ const kind = this.getFilterType();
4167
+ return parseFilter(this.filterString, kind);
4168
+ }
4169
+ }
4170
+
4171
+ export class ASTView
4172
+ extends ASTObjectNode<
4173
+ Malloy.View,
4174
+ {
4175
+ definition: ASTViewDefinition;
4176
+ annotations?: ASTAnnotationList;
4177
+ }
4178
+ >
4179
+ implements IASTAnnotatable
4180
+ {
4181
+ constructor(public node: Malloy.View) {
4182
+ super(node, {
4183
+ definition: ASTViewDefinition.from(node.definition),
4184
+ annotations: node.annotations && new ASTAnnotationList(node.annotations),
4185
+ });
4186
+ }
4187
+
4188
+ get definition() {
4189
+ return this.children.definition;
4190
+ }
4191
+
4192
+ set definition(definition: ASTViewDefinition) {
4193
+ this.edit();
4194
+ this.children.definition = definition;
4195
+ definition.parent = this;
4196
+ }
4197
+
4198
+ get name() {
4199
+ return this.definition.getImplicitName();
4200
+ }
4201
+
4202
+ public getOrAddDefaultSegment(): ASTSegmentViewDefinition {
4203
+ return this.definition.getOrAddDefaultSegment();
4204
+ }
4205
+
4206
+ /**
4207
+ * @internal
4208
+ */
4209
+ get nest() {
4210
+ return this.parent.asNestViewOperation();
4211
+ }
4212
+
4213
+ getInputSchema() {
4214
+ return this.nest.list.segment.getInputSchema();
4215
+ }
4216
+
4217
+ getOutputSchema() {
4218
+ return this.definition.getOutputSchema();
4219
+ }
4220
+
4221
+ /**
4222
+ * @internal
4223
+ */
4224
+ propagateUp(f: PropagationFunction): void {
4225
+ this.propagateDown(f);
4226
+ }
4227
+
4228
+ /**
4229
+ * @internal
4230
+ */
4231
+ propagateDown(f: PropagationFunction): void {
4232
+ f(this.definition);
4233
+ this.definition.propagateDown(f);
4234
+ }
4235
+
4236
+ reorderFields(names: string[]): void {
4237
+ if (this.definition instanceof ASTSegmentViewDefinition) {
4238
+ this.definition.reorderFields(names);
4239
+ } else {
4240
+ this.getOrAddAnnotations().setTagProperty(['field_order'], names);
4241
+ }
4242
+ }
4243
+
4244
+ get annotations() {
4245
+ return this.children.annotations;
4246
+ }
4247
+
4248
+ getOrAddAnnotations() {
4249
+ if (this.annotations) return this.annotations;
4250
+ this.edit();
4251
+ const annotations = new ASTAnnotationList([]);
4252
+ this.children.annotations = annotations;
4253
+ return annotations;
4254
+ }
4255
+
4256
+ getInheritedAnnotations(): Malloy.Annotation[] {
4257
+ return this.definition.getInheritedAnnotations();
4258
+ }
4259
+
4260
+ getInheritedTag(prefix: RegExp | string = '# ') {
4261
+ return tagFromAnnotations(prefix, this.getInheritedAnnotations());
4262
+ }
4263
+
4264
+ getIntrinsicTag(prefix: RegExp | string = '# ') {
4265
+ return this.annotations?.getIntrinsicTag(prefix) ?? new Tag();
4266
+ }
4267
+
4268
+ getTag(prefix: RegExp | string = '# ') {
4269
+ return this.annotations?.getTag(prefix) ?? this.getInheritedTag(prefix);
4270
+ }
4271
+
4272
+ setTagProperty(path: Path, value: TagSetValue = null, prefix = '# ') {
4273
+ this.getOrAddAnnotations().setTagProperty(path, value, prefix);
4274
+ }
4275
+
4276
+ removeTagProperty(path: Path, prefix = '# ') {
4277
+ if (!this.getTag().has(...path)) return;
4278
+ this.getOrAddAnnotations().removeTagProperty(path, prefix);
4279
+ }
4280
+ }
4281
+
4282
+ export class ASTLimitViewOperation extends ASTObjectNode<
4283
+ Malloy.ViewOperationWithLimit,
4284
+ {
4285
+ kind: 'limit';
4286
+ limit: number;
4287
+ }
4288
+ > {
4289
+ readonly kind: Malloy.ViewOperationType = 'limit';
4290
+
4291
+ get limit() {
4292
+ return this.children.limit;
4293
+ }
4294
+
4295
+ set limit(limit: number) {
4296
+ ASTLimitViewOperation.validateLimit(limit);
4297
+ this.edit();
4298
+ this.children.limit = limit;
4299
+ }
4300
+
4301
+ constructor(public node: Malloy.ViewOperationWithLimit) {
4302
+ super(node, {
4303
+ kind: node.kind,
4304
+ limit: node.limit,
4305
+ });
4306
+ }
4307
+
4308
+ /**
4309
+ * @internal
4310
+ */
4311
+ get list() {
4312
+ return this.parent.asViewOperationList();
4313
+ }
4314
+
4315
+ delete() {
4316
+ this.list.remove(this);
4317
+ }
4318
+
4319
+ /**
4320
+ * @internal
4321
+ */
4322
+ static validateLimit(limit: number) {
4323
+ if (!Number.isInteger(limit)) {
4324
+ throw new Error('Limit must be an integer');
4325
+ }
4326
+ }
4327
+ }
4328
+
4329
+ function fieldTypeToAction(type: Malloy.FieldInfoType): string {
4330
+ switch (type) {
4331
+ case 'dimension':
4332
+ return 'group by';
4333
+ case 'measure':
4334
+ return 'aggregate';
4335
+ case 'view':
4336
+ return 'nest';
4337
+ case 'join':
4338
+ return 'join';
4339
+ }
4340
+ }
4341
+
4342
+ function fieldTypeName(type: Malloy.FieldInfoType): string {
4343
+ return type;
4344
+ }
4345
+
4346
+ type ASTAnnotationListParent = ASTQuery | ASTField | ASTView;
4347
+
4348
+ export class ASTAnnotationList extends ASTListNode<
4349
+ Malloy.Annotation,
4350
+ ASTAnnotation
4351
+ > {
4352
+ constructor(annotations: Malloy.Annotation[]) {
4353
+ super(
4354
+ annotations,
4355
+ annotations.map(p => new ASTAnnotation(p))
4356
+ );
4357
+ }
4358
+
4359
+ get items() {
4360
+ return this.children;
4361
+ }
4362
+
4363
+ getInheritedAnnotations(): Malloy.Annotation[] {
4364
+ const parent = this.parent as ASTAnnotationListParent;
4365
+ return parent.getInheritedAnnotations();
4366
+ }
4367
+
4368
+ getIntrinsicTag(prefix: RegExp | string = '# '): Tag {
4369
+ return tagFromAnnotations(prefix, this.items);
4370
+ }
4371
+
4372
+ getTag(prefix: RegExp | string = '# '): Tag {
4373
+ const extending = Tag.fromTagLines(
4374
+ this.getInheritedAnnotations().map(a => a.value)
4375
+ ).tag;
4376
+ return tagFromAnnotations(prefix, this.items, extending);
4377
+ }
4378
+
4379
+ get annotations() {
4380
+ return this.children.map(astAnnotation => astAnnotation.node);
4381
+ }
4382
+
4383
+ setTagProperty(path: Path, value: TagSetValue = null, prefix = '# ') {
4384
+ let firstMatch: ASTAnnotation | undefined = undefined;
4385
+ for (let i = this.items.length - 1; i >= 0; i--) {
4386
+ const annotation = this.index(i);
4387
+ if (!annotation.hasPrefix(prefix)) continue;
4388
+ firstMatch = annotation;
4389
+ if (annotation.hasIntrinsicTagProperty(path)) {
4390
+ annotation.setTagProperty(path, value);
4391
+ return;
4392
+ }
4393
+ }
4394
+ if (firstMatch) {
4395
+ firstMatch.setTagProperty(path, value);
4396
+ } else {
4397
+ this.add(
4398
+ new ASTAnnotation({
4399
+ value: new Tag({prefix}).set(path, value).toString(),
4400
+ })
4401
+ );
4402
+ }
4403
+ }
4404
+
4405
+ removeTagProperty(path: Path, prefix = '# ') {
4406
+ let firstMatch: ASTAnnotation | undefined = undefined;
4407
+ for (let i = this.items.length - 1; i >= 0; i--) {
4408
+ const annotation = this.index(i);
4409
+ if (!annotation.hasPrefix(prefix)) continue;
4410
+ firstMatch = annotation;
4411
+ if (annotation.hasIntrinsicTagProperty(path)) {
4412
+ annotation.removeTagProperty(path);
4413
+ return;
4414
+ }
4415
+ }
4416
+ if (firstMatch) {
4417
+ firstMatch.removeTagProperty(path);
4418
+ } else {
4419
+ this.add(
4420
+ new ASTAnnotation({value: new Tag({prefix}).unset(...path).toString()})
4421
+ );
4422
+ }
4423
+ }
4424
+ }
4425
+
4426
+ export class ASTAnnotation extends ASTObjectNode<
4427
+ Malloy.Annotation,
4428
+ {
4429
+ value: string;
4430
+ }
4431
+ > {
4432
+ readonly kind: Malloy.ViewOperationType = 'limit';
4433
+
4434
+ get value() {
4435
+ return this.children.value;
4436
+ }
4437
+
4438
+ set value(value: string) {
4439
+ this.edit();
4440
+ this.children.value = value;
4441
+ }
4442
+
4443
+ constructor(public node: Malloy.Annotation) {
4444
+ super(node, {
4445
+ value: node.value,
4446
+ });
4447
+ }
4448
+
4449
+ /**
4450
+ * @internal
4451
+ */
4452
+ get list() {
4453
+ return this.parent.asAnnotationList();
4454
+ }
4455
+
4456
+ get index() {
4457
+ return this.list.indexOf(this);
4458
+ }
4459
+
4460
+ delete() {
4461
+ this.list.remove(this);
4462
+ }
4463
+
4464
+ getIntrinsicTag(): Tag {
4465
+ return Tag.fromTagLines([this.value]).tag ?? new Tag();
4466
+ }
4467
+
4468
+ getTag(): Tag {
4469
+ const extending =
4470
+ this.index === 0
4471
+ ? Tag.fromTagLines(
4472
+ this.list.getInheritedAnnotations().map(a => a.value)
4473
+ ).tag ?? new Tag()
4474
+ : this.list.index(this.index - 1).getTag();
4475
+ return Tag.fromTagLines([this.value], extending).tag ?? new Tag();
4476
+ }
4477
+
4478
+ hasPrefix(prefix: string) {
4479
+ return this.value.startsWith(prefix);
4480
+ }
4481
+
4482
+ hasIntrinsicTagProperty(path: Path) {
4483
+ return this.getIntrinsicTag().has(...path);
4484
+ }
4485
+
4486
+ setTagProperty(path: Path, value: TagSetValue) {
4487
+ this.value = this.getTag().set(path, value).toString();
4488
+ }
4489
+
4490
+ removeTagProperty(path: Path) {
4491
+ this.value = this.getTag()
4492
+ .unset(...path)
4493
+ .toString();
4494
+ }
4495
+ }
4496
+
4497
+ function tagFromAnnotations(
4498
+ prefix: string | RegExp,
4499
+ annotations: Malloy.Annotation[] = [],
4500
+ inherited?: Tag
4501
+ ) {
4502
+ const lines = annotations.map(a => a.value);
4503
+ const filteredLines = lines.filter(l =>
4504
+ typeof prefix === 'string' ? l.startsWith(prefix) : l.match(prefix)
4505
+ );
4506
+ return Tag.fromTagLines(filteredLines, inherited).tag ?? new Tag();
4507
+ }
4508
+
4509
+ function serializeFilter(filter: ParsedFilter) {
4510
+ switch (filter.kind) {
4511
+ case 'string':
4512
+ return new Filter.StringSerializer(filter.clauses).serialize();
4513
+ case 'number':
4514
+ return new Filter.NumberSerializer(filter.clauses).serialize();
4515
+ case 'boolean':
4516
+ return new Filter.BooleanSerializer(filter.clauses).serialize();
4517
+ case 'date':
4518
+ return new Filter.DateSerializer(filter.clauses).serialize();
4519
+ }
4520
+ }
4521
+
4522
+ function parseFilter(
4523
+ filterString: string,
4524
+ kind: 'string' | 'number' | 'boolean' | 'date' | 'other'
4525
+ ) {
4526
+ function handleError(logs: Filter.FilterLog[]) {
4527
+ const errors = logs.filter(l => l.severity === 'error');
4528
+ if (errors.length === 0) return;
4529
+ throw new Error(`Invalid Malloy filter string: ${errors[0].message}`);
4530
+ }
4531
+ switch (kind) {
4532
+ case 'string': {
4533
+ const result = new Filter.StringParser(filterString).parse();
4534
+ handleError(result.logs);
4535
+ return {kind, clauses: result.clauses};
4536
+ }
4537
+ case 'number': {
4538
+ const result = new Filter.NumberParser(filterString).parse();
4539
+ handleError(result.logs);
4540
+ return {kind, clauses: result.clauses};
4541
+ }
4542
+ case 'boolean': {
4543
+ const result = new Filter.BooleanParser(filterString).parse();
4544
+ handleError(result.logs);
4545
+ return {kind, clauses: result.clauses};
4546
+ }
4547
+ case 'date': {
4548
+ const result = new Filter.DateParser(filterString).parse();
4549
+ handleError(result.logs);
4550
+ return {kind, clauses: result.clauses};
4551
+ }
4552
+ case 'other':
4553
+ throw new Error('Not implemented');
4554
+ }
4555
+ }
4556
+
4557
+ function sourceOrQueryToModelEntry(
4558
+ entry:
4559
+ | Malloy.ModelEntryValueWithSource
4560
+ | Malloy.ModelEntryValueWithQuery
4561
+ | Malloy.SourceInfo
4562
+ | Malloy.QueryInfo
4563
+ ): Malloy.ModelEntryValueWithSource | Malloy.ModelEntryValueWithQuery {
4564
+ if ('kind' in entry) {
4565
+ return entry;
4566
+ } else {
4567
+ return {...entry, kind: 'source'};
4568
+ }
4569
+ }
4570
+
4571
+ function isDateTimeframe(
4572
+ timeframe: Malloy.TimestampTimeframe
4573
+ ): timeframe is Malloy.DateTimeframe {
4574
+ switch (timeframe) {
4575
+ case 'year':
4576
+ case 'quarter':
4577
+ case 'month':
4578
+ case 'week':
4579
+ case 'day':
4580
+ return true;
4581
+ default:
4582
+ return false;
4583
+ }
4584
+ }
4585
+
4586
+ function digits(value: number, digits: number) {
4587
+ return value.toString().padStart(digits, '0');
4588
+ }
4589
+
4590
+ function serializeDateAsLiteral(date: Date): string {
4591
+ const year = digits(date.getUTCFullYear(), 2);
4592
+ const month = digits(date.getUTCMonth() + 1, 2);
4593
+ const day = digits(date.getUTCDate(), 2);
4594
+ const hour = digits(date.getUTCHours(), 2);
4595
+ const minute = digits(date.getUTCMinutes(), 2);
4596
+ const second = digits(date.getUTCSeconds(), 2);
4597
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
4598
+ }
4599
+
4600
+ function pathsMatch(a: string[] | undefined, b: string[] | undefined): boolean {
4601
+ const aOrEmpty = a ?? [];
4602
+ const bOrEmpty = b ?? [];
4603
+ return (
4604
+ aOrEmpty.length === bOrEmpty.length &&
4605
+ aOrEmpty.every((s, i) => s === bOrEmpty[i])
4606
+ );
4607
+ }
4608
+
4609
+ function getFilterType(
4610
+ fieldInfo: Malloy.FieldInfoWithDimension | Malloy.FieldInfoWithMeasure
4611
+ ): 'string' | 'boolean' | 'number' | 'date' | 'other' {
4612
+ switch (fieldInfo.type.kind) {
4613
+ case 'string_type':
4614
+ return 'string';
4615
+ case 'boolean_type':
4616
+ return 'boolean';
4617
+ case 'number_type':
4618
+ return 'number';
4619
+ case 'date_type':
4620
+ case 'timestamp_type':
4621
+ return 'date';
4622
+ default:
4623
+ return 'other';
4624
+ }
4625
+ }
4626
+
4627
+ function validateFilter(
4628
+ field: Malloy.FieldInfo,
4629
+ filter: string | ParsedFilter
4630
+ ) {
4631
+ if (field.kind !== 'dimension' && field.kind !== 'measure') {
4632
+ throw new Error(`Cannot filter by field of type ${field.kind}`);
4633
+ }
4634
+ const type = getFilterType(field);
4635
+ if (typeof filter === 'string') {
4636
+ parseFilter(filter, type);
4637
+ } else {
4638
+ if (filter.kind !== type) {
4639
+ throw new Error(
4640
+ `Invalid filter for field ${field.name}; expected type ${type}, but got ${filter.kind}`
4641
+ );
4642
+ }
4643
+ }
4644
+ }
4645
+
4646
+ function getInputSchemaFromViewParent(parent: ViewParent) {
4647
+ if (parent instanceof ASTArrowQueryDefinition) {
4648
+ return parent.getSourceInfo().schema;
4649
+ } else if (parent instanceof ASTRefinementQueryDefinition) {
4650
+ throw new Error('unimplemented');
4651
+ } else {
4652
+ return parent.getInputSchema();
4653
+ }
4654
+ }