xref: /aosp_15_r20/external/perfetto/ui/src/core/event_set.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2023 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {arrayEquals, isArrayOf} from '../base/array_utils';
16import {isString} from '../base/object_utils';
17import {intersect} from '../base/set_utils';
18
19// Contents:
20// CORE_TYPES - The main types for using EventSet.
21// EVENT_SET_IMPLS - Impl of {Concreate, Empty, Sql, Naive{...}}EventSet
22// EXPR - Expression logic which can be lowered to either JS or SQL
23// STUPID_TYPE_MAGIC
24// HELPERS - Random helpers.
25
26// CORE_TYPES =========================================================
27
28// A single value. These are often retrieved from trace_processor so
29// need to map to the related sqlite type:
30// null = NULL, string = TEXT, number = INTEGER/REAL,
31// boolean = INTEGER, bigint = INTEGER
32export type Primitive = null | string | boolean | number | bigint;
33
34export const Null = 'null' as const;
35export const Num = 'num' as const;
36export const BigInt = 'bigint' as const;
37export const Str = 'str' as const;
38export const Id = 'id' as const;
39export const Bool = 'bool' as const;
40
41// Values may be of any of the above types:
42export type KeyType =
43  | typeof Num
44  | typeof Str
45  | typeof Null
46  | typeof Id
47  | typeof Bool
48  | typeof BigInt;
49
50// KeySet is a specification for the key/value pairs on an Event.
51// - Every event must have a string ID.
52// - In addition Events may have 1 or more key/value pairs.
53// The *specification* for the key/value pair has to be *precisely* one
54// of the KeySet constants above. So:
55// const thisTypeChecks: KeySet = { foo: Str };
56// const thisDoesNot: KeySet = { foo: "bar" };
57// Since although 'bar' is a string it's not a KeyType.
58export type KeySet = {
59  readonly [key: string]: KeyType;
60};
61
62// The empty keyset. Events from this KeySet will only have ids.
63export interface EmptyKeySet extends KeySet {}
64
65export type UntypedKeySet = KeySet;
66
67// A single trace Event.
68// Events have:
69// - A globally unique identifier `id`.
70// - Zero or more key/value pairs.
71// Note: Events do *not* have to have all possible keys/value pairs for
72// the given id. It is expected that users will only materialise the
73// key/value pairs relevant to the specific use case at hand.
74export type WritableUntypedEvent = {
75  id: string;
76  [key: string]: Primitive;
77};
78
79export type UntypedEvent = Readonly<WritableUntypedEvent>;
80
81export type Event<K extends KeySet> = {
82  readonly [Property in Exclude<keyof K, 'id'>]: ConformingValue<K[Property]>;
83} & {
84  readonly id: string;
85};
86
87// An EventSet is a:
88// - ordered
89// - immutable
90// - subset
91// of events in the trace.
92export interface EventSet<P extends KeySet> {
93  // All possible keys for Events in this EventSet.
94  readonly keys: P;
95
96  // Methods for refining the set.
97  // Note: these are all synchronous - we expect the cost (and hence
98  // any asynchronous queries) to be deferred to analysis time.
99  filter(...filters: Filter[]): EventSet<P>;
100  sort(...sorts: Sort[]): EventSet<P>;
101  union<Q extends KeySet>(other: EventSet<Q>): Merged<P, Q>;
102  intersect<Q extends KeySet>(other: EventSet<Q>): Merged<P, Q>;
103
104  // Methods for analysing the set.
105  // Note: these are all asynchronous - it's expected that these will
106  // often have to do queries.
107  count(): Promise<number>;
108  isEmpty(): Promise<boolean>;
109  materialise<T extends KeySet>(
110    keys: T,
111    offset?: number,
112    limit?: number,
113  ): Promise<Materialised<T, P>>;
114}
115
116interface UnionEventSet<T extends KeySet> extends EventSet<T> {
117  readonly parents: EventSet<T>[];
118  readonly isUnion: true;
119  create(...events: EventSet<KeySet>[]): UnionEventSet<T>;
120}
121
122interface IntersectionEventSet<T extends KeySet> extends EventSet<T> {
123  readonly parents: EventSet<T>[];
124  readonly isIntersection: true;
125  create(...events: EventSet<KeySet>[]): IntersectionEventSet<T>;
126}
127
128interface FilterEventSet<T extends KeySet> extends EventSet<T> {
129  readonly parent: EventSet<T>;
130  readonly filters: Filter[];
131  readonly isFilter: true;
132}
133
134interface SortEventSet<T extends KeySet> extends EventSet<T> {
135  readonly parent: EventSet<T>;
136  readonly sorts: Sort[];
137  readonly isSort: true;
138}
139
140// eslint-disable-next-line @typescript-eslint/no-explicit-any
141export type UntypedEventSet = EventSet<any>;
142
143// An expression that operates on an Event and produces a Primitive as
144// output. Expressions have to work both in JavaScript and in SQL.
145// In SQL users can use buildQueryFragment to convert the expression
146// into a snippet of SQL. For JavaScript they call execute(). In both
147// cases you need to know which keys the expression uses, for this call
148// `freeVariables`.
149// TODO(hjd): These should also be paramatised by KeySet and final
150// type.
151export interface Expr {
152  // Return a fragment of SQL that can be used to evaluate the
153  // expression. `binding` maps key names to column names in the
154  // resulting SQL. The caller must ensure that binding includes at
155  // least all the keys from `freeVariables`.
156  buildQueryFragment(binding: Map<string, string>): string;
157
158  // Run the expression on an Event. The caller must ensure that event
159  // has all the keys from `freeVariables` materialised.
160  execute(event: UntypedEvent): Primitive;
161
162  // Returns the set of keys used in this expression.
163  // For example in an expression representing `(foo + 4) * bar`
164  // freeVariables would return the set {foo: Num, bar: Num}.
165  freeVariables(): KeySet;
166}
167
168// A filter is a (normally boolean) expression.
169export type Filter = Expr;
170
171// Sorting direction.
172export enum Direction {
173  ASC,
174  DESC,
175}
176
177// A sort is an expression combined with a direction:
178export interface Sort {
179  direction: Direction;
180  expression: Expr;
181}
182
183// EVENT_SET_IMPLS ====================================================
184
185// OptimisingEventSet is what makes it a) tractable to write EventSet
186// implementations and b) have those implementations be fast.
187// The EventSet interface has two kinds of methods:
188// 1. Synchronous refinement methods which produce an EventSet and
189//    often take a second EventSet as an argument
190// 2. Asynchronous 'analysis' methods
191//
192// Together this means in the minimal case subclasses only *have* to
193// implement the single abstract method: materialise(). Everything else
194// is handled for you.
195export abstract class OptimisingEventSet<P extends KeySet>
196  implements EventSet<P>
197{
198  abstract readonly keys: P;
199
200  // OptimisingEventSet provides the synchronous refinement methods.
201  // The basic pattern is to construct a 'NaiveFoo' EventSet which will
202  // do the given operation (filter, sort, union, intersection) in
203  // JavaScript then call optimise(). Optimse then tries to improve the
204  // EventSet tree - and avoid having to use the fallback naive
205  // implementaion.
206  // Optimise does 'tree rewriting' of the EventSet tree. For example
207  // considering a tree: 'union(A, 0)' where 0 is the empty set and
208  // A is some arbitrary EventSet, optimise(union(A, 0)) returns A.
209  // For more detail see optimise() below.
210
211  filter(...filters: Filter[]): EventSet<P> {
212    const result = new NaiveFilterEventSet(this, filters);
213    const optimised = optimise(result);
214    return optimised;
215  }
216
217  sort(...sorts: Sort[]): EventSet<P> {
218    const result = new NaiveSortEventSet(this, sorts);
219    const optimised = optimise(result);
220    return optimised;
221  }
222
223  union<Q extends KeySet>(other: EventSet<Q>): Merged<P, Q> {
224    const merged = mergeKeys(this.keys, other.keys);
225    const result = new NaiveUnionEventSet<MergedKeys<P, Q>>(
226      merged,
227      this as UntypedEventSet,
228      other as UntypedEventSet,
229    );
230    const optimised = optimise(result);
231    return optimised;
232  }
233
234  intersect<Q extends KeySet>(other: EventSet<Q>): Merged<P, Q> {
235    const merged = mergeKeys(this.keys, other.keys);
236    const result = new NaiveIntersectionEventSet<MergedKeys<P, Q>>(
237      merged,
238      this as UntypedEventSet,
239      other as UntypedEventSet,
240    );
241    const optimised = optimise(result);
242    return optimised;
243  }
244
245  // Analysis methods should be implemented by the subclass.
246  // Materialise is abstract and must be implemented by the subclass.
247  abstract materialise<Q extends KeySet>(
248    keys: Q,
249    offset?: number,
250    limit?: number,
251  ): Promise<Materialised<Q, P>>;
252
253  // We provide a default implementation of count() on top of
254  // materialise(). It's likely the subclass can provide a more
255  // performant implementation.
256  async count(): Promise<number> {
257    const materialised = await this.materialise({});
258    return materialised.events.length;
259  }
260
261  // We provide a default implementation of empty() on top of
262  // materialise(). It's likely the subclass can provide a more
263  // performant implementation.
264  async isEmpty(): Promise<boolean> {
265    const materialised = await this.materialise(
266      {},
267      0 /* offset */,
268      1 /* limit */,
269    );
270    return materialised.events.length === 0;
271  }
272}
273
274class NaiveFilterEventSet<P extends KeySet>
275  extends OptimisingEventSet<P>
276  implements FilterEventSet<P>
277{
278  readonly isFilter = true;
279  readonly parent: EventSet<P>;
280  readonly filters: Filter[];
281  readonly keys: P;
282
283  constructor(parent: EventSet<P>, filters: Filter[]) {
284    super();
285    this.parent = parent;
286    this.keys = this.parent.keys;
287    this.filters = filters;
288  }
289
290  async count(): Promise<number> {
291    const keys = freeVariablesFromFilters(this.filters);
292    const concreteParent = await this.parent.materialise(keys);
293    const events = concreteParent.events;
294    let total = 0;
295    for (const e of events) {
296      if (this.filters.every((f) => f.execute(e))) {
297        total += 1;
298      }
299    }
300    return total;
301  }
302
303  async isEmpty(): Promise<boolean> {
304    const keys = freeVariablesFromFilters(this.filters);
305    const concreateParent = await this.parent.materialise(keys);
306    const events = concreateParent.events;
307    for (const e of events) {
308      if (this.filters.every((f) => f.execute(e))) {
309        return false;
310      }
311    }
312    return true;
313  }
314
315  async materialise<Q extends KeySet>(
316    keys: Q,
317    offset?: number,
318    limit?: number,
319  ): Promise<Materialised<Q, P>> {
320    const combined = freeVariablesFromFilters(this.filters, keys);
321    const concreateParent = await this.parent.materialise(combined);
322    let events = concreateParent.events;
323    for (const filter of this.filters) {
324      events = events.filter((e) => filter.execute(e));
325    }
326    return new ConcreteEventSet(combined, events).materialise(
327      keys,
328      offset,
329      limit,
330    );
331  }
332}
333
334class NaiveSortEventSet<P extends KeySet>
335  extends OptimisingEventSet<P>
336  implements SortEventSet<P>
337{
338  readonly isSort = true;
339  readonly parent: EventSet<P>;
340  readonly sorts: Sort[];
341  readonly keys: P;
342
343  constructor(parent: EventSet<P>, sorts: Sort[]) {
344    super();
345    this.parent = parent;
346    this.keys = this.parent.keys;
347    this.sorts = sorts;
348  }
349
350  async count(): Promise<number> {
351    return this.parent.count();
352  }
353
354  async isEmpty(): Promise<boolean> {
355    return this.parent.isEmpty();
356  }
357
358  async materialise<Q extends KeySet>(
359    keys: Q,
360    offset?: number,
361    limit?: number,
362  ): Promise<Materialised<Q, P>> {
363    const combined = freeVariablesFromSorts(this.sorts, keys);
364    const concreateParent = await this.parent.materialise(combined);
365    let events = concreateParent.events;
366    for (const sort of this.sorts) {
367      events = events.sort(cmpFromSort(sort));
368    }
369    return new ConcreteEventSet(combined, events).materialise(
370      keys,
371      offset,
372      limit,
373    );
374  }
375}
376
377export class NaiveUnionEventSet<T extends KeySet>
378  extends OptimisingEventSet<T>
379  implements UnionEventSet<T>
380{
381  readonly isUnion = true;
382  readonly parents: EventSet<T>[];
383  readonly keys: T;
384
385  constructor(keys: T, ...parents: EventSet<T>[]) {
386    super();
387    this.keys = keys;
388    this.parents = parents;
389  }
390
391  create(...parents: EventSet<T>[]): NaiveUnionEventSet<T> {
392    return new NaiveUnionEventSet(this.keys, ...parents);
393  }
394
395  // TODO(hjd): We could implement a more efficient dedicated count().
396  // TODO(hjd): We could implement a more efficient dedicated isEmpty().
397
398  async materialise<Q extends KeySet>(
399    keys: Q,
400    offset?: number,
401    limit?: number,
402  ): Promise<Materialised<Q, T>> {
403    const promises = this.parents.map((p) => p.materialise(keys));
404    const materialisedParents = (await Promise.all(
405      promises,
406    )) as ConcreteEventSet<Q>[];
407    const seen = new Set<string>();
408    let events = [];
409
410    // TODO(hjd): There are various options for doing this in faster
411    // way and we should do one of them.
412    for (const parent of materialisedParents) {
413      for (const e of parent.events) {
414        if (!seen.has(e.id)) {
415          events.push(e);
416          seen.add(e.id);
417        }
418      }
419    }
420
421    events = applyLimitOffset(events, limit, offset);
422    return ConcreteEventSet.from(keys, events) as unknown as Materialised<Q, T>;
423  }
424}
425
426export class NaiveIntersectionEventSet<T extends KeySet>
427  extends OptimisingEventSet<T>
428  implements IntersectionEventSet<T>
429{
430  readonly isIntersection = true;
431  readonly parents: EventSet<T>[];
432  readonly keys: T;
433
434  constructor(keys: T, ...parents: EventSet<T>[]) {
435    super();
436    this.keys = keys;
437    this.parents = parents;
438  }
439
440  create(...parents: EventSet<T>[]): NaiveIntersectionEventSet<T> {
441    return new NaiveIntersectionEventSet(this.keys, ...parents);
442  }
443
444  // TODO(hjd): We could implement a more efficient dedicated count().
445  // TODO(hjd): We could implement a more efficient dedicated isEmpty().
446
447  async materialise<Q extends KeySet>(
448    keys: Q,
449    offset?: number,
450    limit?: number,
451  ): Promise<Materialised<Q, T>> {
452    if (this.parents.length === 0) {
453      return ConcreteEventSet.from(keys, []) as Materialised<Q, T>;
454    }
455
456    const parents = this.parents.slice();
457    const firstParent = parents.pop()!;
458
459    const promises = parents.map((p) => p.materialise({}));
460    const firstPromise = firstParent.materialise(
461      keys,
462    ) as unknown as ConcreteEventSet<Q>;
463
464    const materialised = await Promise.all(promises);
465    const firstMaterialised = await firstPromise;
466
467    let ids = new Set<string>();
468    for (const e of firstMaterialised.events) {
469      ids.add(e.id);
470    }
471    for (const m of materialised) {
472      const newIds = new Set<string>();
473      for (const e of m.events) {
474        newIds.add(e.id);
475      }
476      ids = intersect(ids, newIds);
477    }
478
479    let events = firstMaterialised.events.filter((e) => ids.has(e.id));
480    events = applyLimitOffset(events, limit, offset);
481    return ConcreteEventSet.from(keys, events) as unknown as Materialised<Q, T>;
482  }
483}
484
485// A completely empty EventSet.
486export class EmptyEventSet<T extends KeySet>
487  extends OptimisingEventSet<T>
488  implements EventSet<T>
489{
490  readonly isEmptyEventSet = true;
491  readonly keys: T;
492
493  constructor(keys: T) {
494    super();
495    this.keys = keys;
496  }
497
498  static get(): EmptyEventSet<EmptyKeySet> {
499    return new EmptyEventSet<EmptyKeySet>({});
500  }
501
502  count(): Promise<number> {
503    return Promise.resolve(0);
504  }
505
506  isEmpty(): Promise<boolean> {
507    return Promise.resolve(true);
508  }
509
510  async materialise<Q extends KeySet>(
511    keys: Q,
512    _offset?: number,
513    _limit?: number,
514  ): Promise<Materialised<Q, T>> {
515    return Promise.resolve(
516      new ConcreteEventSet<Q>(keys, []) as unknown as Materialised<Q, T>,
517    );
518  }
519}
520
521export class ConcreteEventSet<P extends KeySet>
522  extends OptimisingEventSet<P>
523  implements EventSet<P>
524{
525  readonly isConcreteEventSet = true;
526  readonly events: Event<P>[];
527  readonly keys: P;
528
529  static from<Q extends KeySet>(
530    keys: Q,
531    events: Event<Q>[],
532  ): ConcreteEventSet<Q> {
533    return new ConcreteEventSet<Q>(keys, events);
534  }
535
536  constructor(keys: P, events: Event<P>[]) {
537    super();
538    // TODO(hjd): Add some paranoid mode where we crash here if
539    // `events` and `keys` mismatch?
540    this.events = events;
541    this.keys = keys;
542  }
543
544  count(): Promise<number> {
545    return Promise.resolve(this.events.length);
546  }
547
548  isEmpty(): Promise<boolean> {
549    return Promise.resolve(this.events.length === 0);
550  }
551
552  materialise<Q extends KeySet>(
553    keys: Q,
554    offset?: number,
555    limit?: number,
556  ): Promise<Materialised<Q, P>> {
557    const actualOffset = offset === undefined ? 0 : offset;
558    const actualEnd =
559      limit === undefined ? this.events.length : actualOffset + limit;
560
561    const shouldFilter = !isEqualKeySet(keys, this.keys);
562    const shouldSlice = actualOffset !== 0 || actualEnd !== this.events.length;
563
564    if (!shouldFilter && !shouldSlice) {
565      return Promise.resolve(this as unknown as Materialised<Q, P>);
566    }
567
568    let events = this.events as Event<Q>[];
569
570    if (shouldFilter) {
571      events = events.map((e) => {
572        const result: WritableUntypedEvent = {
573          id: e.id,
574        };
575        for (const [k, v] of Object.entries(keys)) {
576          // While the static typing prevents folks from hitting
577          // this in the common case people can still on purpose pass
578          // keysets and lie about the types.
579          result[k] = (e as UntypedEvent)[k] ?? getKeyDefault(k, v);
580        }
581        return result as Event<Q>;
582      });
583    }
584
585    if (shouldSlice) {
586      events = events.slice(actualOffset, actualEnd);
587    }
588
589    return Promise.resolve(
590      new ConcreteEventSet<Q>(keys, events) as unknown as Materialised<Q, P>,
591    );
592  }
593}
594
595// Optimse:
596// We have a couple major kinds of optimisation:
597// 1. Pushing down filters.
598// 2. Set optimisations (e.g union(empty, A) == A)
599// 3. Merging EventSets of the same kind
600//
601// In more detail:
602// 1. Pushing down filters. For example:
603//    filter(union(A, B), pred) ==
604//      union(filter(A, pred), filter(B, pred))
605//    This is more useful than it seems since if we manage to push down
606//    filters all the may to SQL they can be implemented very
607//    efficiently in C++.
608// 2. Classic set optimisations. e.g.
609//      union(A, empty) == A
610//      union(A, A) == A
611//      intersect(A, empty) == empty
612//      etc
613// 3. Merging EventSets of the same type. For example:
614//    union(concrete(a, b), concrete(b, c)) == concrete(a, b, c)
615//    Similarly the combinations of two SQL EventSets can be converted
616//    into a single SQL EventSet with a more complicated query -
617//    avoiding doing the processing in TypeScript.
618//
619// A critical pre-condition of this function is that EventSets are
620// immutable - this allows us to reuse parts of the input event set tree
621// in the output.
622export function optimise<T extends KeySet>(eventSet: EventSet<T>): EventSet<T> {
623  // Empty EventSet can't be futher optimised.
624  if (isEmptyEventSet(eventSet)) {
625    return eventSet;
626  }
627
628  if (isConcreteEventSet(eventSet)) {
629    // A concrete events with zero elements is the empty events.
630    if (eventSet.events.length === 0) {
631      return new EmptyEventSet(eventSet.keys);
632    }
633    // ...but otherwise can not be optimised further.
634    return eventSet;
635  }
636
637  if (isUnionEventSet(eventSet)) {
638    const keys = eventSet.keys;
639
640    let newParents: EventSet<T>[] = eventSet.parents.slice();
641
642    // Empty sets don't contribute to the union.
643    newParents = newParents.filter((p) => !isEmptyEventSet(p));
644
645    // union([]) is empty.
646    if (newParents.length === 0) {
647      return new EmptyEventSet(keys);
648    }
649
650    if (newParents.length === 1) {
651      return newParents[0];
652    }
653
654    // The union of concrete EventSets is a concrete EventSets with all
655    // the events in.
656    if (
657      isArrayOf<ConcreteEventSet<T>, EventSet<T>>(
658        isConcreteEventSet,
659        newParents,
660      )
661    ) {
662      const seen = new Set<string>();
663      const events = [];
664      for (const p of newParents) {
665        for (const e of p.events) {
666          if (!seen.has(e.id)) {
667            events.push(e);
668            seen.add(e.id);
669          }
670        }
671      }
672      return ConcreteEventSet.from(eventSet.keys, events);
673    }
674
675    if (arrayEquals(newParents, eventSet.parents)) {
676      return eventSet;
677    } else {
678      return eventSet.create(...newParents);
679    }
680  }
681
682  if (isIntersectionEventSet(eventSet)) {
683    // For any x: intersect([x, 0]) is 0
684    for (const parent of eventSet.parents) {
685      if (isEmptyEventSet(parent)) {
686        return parent;
687      }
688    }
689    return eventSet;
690  }
691
692  if (isFilterEventSet(eventSet)) {
693    const parent = eventSet.parent;
694
695    if (isEmptyEventSet(parent)) {
696      return parent;
697    }
698
699    return eventSet;
700  }
701
702  if (isSortEventSet(eventSet)) {
703    const parent = eventSet.parent;
704
705    if (isEmptyEventSet(parent)) {
706      return parent;
707    }
708
709    return eventSet;
710  }
711
712  // TODO(hjd): Re-add the optimisations from the prototype.
713  // TODO(hjd): Union([a, a]) === a but maybe not worth optimising.
714
715  return eventSet;
716}
717
718// EXPR ===============================================================
719
720abstract class BinOp implements Expr {
721  readonly left: Expr;
722  readonly right: Expr;
723
724  constructor(left: Expr, right: Expr) {
725    this.left = left;
726    this.right = right;
727  }
728
729  buildQueryFragment(binding: Map<string, string>): string {
730    const a = this.left.buildQueryFragment(binding);
731    const b = this.right.buildQueryFragment(binding);
732    const op = this.sqlOp();
733    return `(${a} ${op} ${b})`;
734  }
735
736  execute(event: UntypedEvent): Primitive {
737    const a = this.left.execute(event);
738    const b = this.right.execute(event);
739    return this.evaluate(a, b);
740  }
741
742  freeVariables(): KeySet {
743    const a = this.left.freeVariables();
744    const b = this.right.freeVariables();
745    return mergeKeys(a, b);
746  }
747
748  abstract sqlOp(): string;
749  abstract evaluate(lhs: Primitive, rhs: Primitive): Primitive;
750}
751
752class Le extends BinOp implements Expr {
753  sqlOp(): string {
754    return '<=';
755  }
756
757  evaluate(lhs: Primitive, rhs: Primitive): Primitive {
758    return lhs! <= rhs!;
759  }
760}
761
762class Lt extends BinOp implements Expr {
763  sqlOp(): string {
764    return '<';
765  }
766
767  evaluate(lhs: Primitive, rhs: Primitive): Primitive {
768    return lhs! < rhs!;
769  }
770}
771
772class Ge extends BinOp implements Expr {
773  sqlOp(): string {
774    return '>=';
775  }
776
777  evaluate(lhs: Primitive, rhs: Primitive): Primitive {
778    return lhs! >= rhs!;
779  }
780}
781
782class Gt extends BinOp implements Expr {
783  sqlOp(): string {
784    return '>';
785  }
786
787  evaluate(lhs: Primitive, rhs: Primitive): Primitive {
788    return lhs! > rhs!;
789  }
790}
791
792class Eq extends BinOp implements Expr {
793  sqlOp(): string {
794    return '=';
795  }
796
797  evaluate(lhs: Primitive, rhs: Primitive): Primitive {
798    return lhs === rhs;
799  }
800}
801
802class And extends BinOp implements Expr {
803  sqlOp(): string {
804    return 'AND';
805  }
806
807  evaluate(lhs: Primitive, rhs: Primitive): Primitive {
808    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
809    return lhs && rhs;
810  }
811}
812
813class Or extends BinOp implements Expr {
814  sqlOp(): string {
815    return 'OR';
816  }
817
818  evaluate(lhs: Primitive, rhs: Primitive): Primitive {
819    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
820    return lhs || rhs;
821  }
822}
823
824class Ne extends BinOp implements Expr {
825  sqlOp(): string {
826    return '!=';
827  }
828
829  evaluate(lhs: Primitive, rhs: Primitive): Primitive {
830    return lhs !== rhs;
831  }
832}
833
834class Var implements Expr {
835  readonly name: string;
836
837  constructor(name: string) {
838    this.name = name;
839  }
840
841  buildQueryFragment(binding: Map<string, string>): string {
842    // TODO(hjd): wrap in try catch?
843    return binding.get(this.name)!;
844  }
845
846  execute(event: UntypedEvent): Primitive {
847    return event[this.name]!;
848  }
849
850  freeVariables(): KeySet {
851    return {
852      [this.name]: Null,
853    };
854  }
855}
856
857class Constant implements Expr {
858  readonly value: Primitive;
859
860  constructor(value: Primitive) {
861    this.value = value;
862  }
863
864  buildQueryFragment(_: Map<string, string>): string {
865    const value = this.value;
866    if (value === null) {
867      return 'NULL';
868    } else if (isString(value)) {
869      return `'${value}'`;
870    } else if (typeof value === 'boolean') {
871      return value ? 'TRUE' : 'FALSE';
872    } else {
873      return `${value}`;
874    }
875  }
876
877  execute(_: UntypedEvent): Primitive {
878    return this.value;
879  }
880
881  freeVariables(): EmptyKeySet {
882    return {};
883  }
884}
885
886export function eq(left: Expr, right: Expr): Eq {
887  return new Eq(left, right);
888}
889export function ne(left: Expr, right: Expr): Ne {
890  return new Ne(left, right);
891}
892
893export function gt(left: Expr, right: Expr): Gt {
894  return new Gt(left, right);
895}
896
897export function ge(left: Expr, right: Expr): Ge {
898  return new Ge(left, right);
899}
900
901export function lt(left: Expr, right: Expr): Lt {
902  return new Lt(left, right);
903}
904
905export function le(left: Expr, right: Expr): Le {
906  return new Le(left, right);
907}
908
909export function and(left: Expr, right: Expr): And {
910  return new And(left, right);
911}
912
913export function or(left: Expr, right: Expr): Or {
914  return new Or(left, right);
915}
916
917export function c(value: Primitive): Constant {
918  return new Constant(value);
919}
920
921export function v(name: string): Var {
922  return new Var(name);
923}
924
925// Type guards:
926export function isEmptyEventSet<T extends KeySet>(
927  s: EventSet<T> | EmptyEventSet<T>,
928): s is EmptyEventSet<T> {
929  return !!(s as EmptyEventSet<T>).isEmptyEventSet;
930}
931
932export function isConcreteEventSet<T extends KeySet>(
933  s: EventSet<T> | ConcreteEventSet<T>,
934): s is ConcreteEventSet<T> {
935  return !!(s as ConcreteEventSet<T>).isConcreteEventSet;
936}
937
938export function isUnionEventSet<T extends KeySet>(
939  s: EventSet<T> | UnionEventSet<T>,
940): s is UnionEventSet<T> {
941  return (
942    (s as UnionEventSet<T>).isUnion &&
943    Array.isArray((s as UnionEventSet<T>).parents)
944  );
945}
946
947export function isIntersectionEventSet<T extends KeySet>(
948  s: EventSet<T> | IntersectionEventSet<T>,
949): s is IntersectionEventSet<T> {
950  return (
951    (s as IntersectionEventSet<T>).isIntersection &&
952    Array.isArray((s as IntersectionEventSet<T>).parents)
953  );
954}
955
956export function isFilterEventSet<T extends KeySet>(
957  s: EventSet<T> | FilterEventSet<T>,
958): s is FilterEventSet<T> {
959  return (
960    (s as FilterEventSet<T>).isFilter &&
961    Array.isArray((s as FilterEventSet<T>).filters)
962  );
963}
964
965export function isSortEventSet<T extends KeySet>(
966  s: EventSet<T> | SortEventSet<T>,
967): s is SortEventSet<T> {
968  return (
969    (s as SortEventSet<T>).isSort && Array.isArray((s as SortEventSet<T>).sorts)
970  );
971}
972
973// STUPID_TYPE_MAGIC ==================================================
974type ErrorBrand<T extends string> = {
975  [k in T]: void;
976};
977
978// A particular key/value pair on an Event matches the relevant entry
979// on the KeySet if the KeyType and the value type 'match':
980// Id => string
981// Str => string
982// Bool => boolean
983// Null => null
984// Num => number
985type KeyToType = {
986  num: number;
987  str: string;
988  bool: boolean;
989  null: null;
990  bigint: bigint;
991  id: string;
992};
993
994type ConformingValue<T> = T extends keyof KeyToType ? KeyToType[T] : void;
995
996type Materialised<
997  Concrete extends KeySet,
998  Parent extends KeySet,
999> = Parent extends Concrete
1000  ? ConcreteEventSet<Concrete>
1001  : ErrorBrand<`Very bad!`>;
1002
1003type MergedKeys<Left extends KeySet, Right extends KeySet> = Left & Right;
1004
1005type Merged<Left extends KeySet, Right extends KeySet> = EventSet<
1006  MergedKeys<Left, Right>
1007>;
1008
1009// HELPERS ============================================================
1010function applyLimitOffset<T>(arr: T[], limit?: number, offset?: number): T[] {
1011  const actualOffset = offset === undefined ? 0 : offset;
1012  const actualEnd = limit === undefined ? arr.length : actualOffset + limit;
1013  const shouldSlice = actualOffset !== 0 || actualEnd !== arr.length;
1014  return shouldSlice ? arr.slice(actualOffset, actualEnd) : arr;
1015}
1016
1017function mergeKeys<P extends KeySet, Q extends KeySet>(
1018  left: P,
1019  right: Q,
1020): MergedKeys<P, Q> {
1021  return Object.assign({}, left, right);
1022}
1023
1024function getKeyDefault(keyName: string, keyType: KeyType): Primitive {
1025  switch (keyType) {
1026    case Id:
1027      throw new Error(
1028        `Can't create default for key '${keyName}' with type '${keyType}'`,
1029      );
1030    case Num:
1031      return 0;
1032    case Null:
1033      return null;
1034    case Str:
1035      return '';
1036    case Bool:
1037      return false;
1038    case BigInt:
1039      return 0n;
1040    default:
1041      const _exhaustiveCheck: never = keyType;
1042      return _exhaustiveCheck;
1043  }
1044}
1045
1046function isEqualKeySet(a: UntypedKeySet, b: UntypedKeySet): boolean {
1047  for (const k in a) {
1048    if (a[k] !== b[k]) {
1049      return false;
1050    }
1051  }
1052  for (const k in b) {
1053    if (b[k] !== a[k]) {
1054      return false;
1055    }
1056  }
1057  return true;
1058}
1059
1060function freeVariablesFromFilters(
1061  filters: Filter[],
1062  initialKeySet?: KeySet,
1063): KeySet {
1064  let result = {};
1065
1066  if (initialKeySet !== undefined) {
1067    result = mergeKeys(result, initialKeySet);
1068  }
1069
1070  for (const filter of filters) {
1071    result = mergeKeys(result, filter.freeVariables());
1072  }
1073
1074  return result;
1075}
1076
1077function freeVariablesFromSorts(sorts: Sort[], initialKeySet?: KeySet): KeySet {
1078  let result = {};
1079
1080  if (initialKeySet !== undefined) {
1081    result = mergeKeys(result, initialKeySet);
1082  }
1083
1084  for (const sort of sorts) {
1085    result = mergeKeys(result, sort.expression.freeVariables());
1086  }
1087
1088  return result;
1089}
1090
1091function primativeToRank(p: Primitive) {
1092  if (p === null) {
1093    return 0;
1094  } else if (isString(p)) {
1095    return 2;
1096  } else {
1097    return 1;
1098  }
1099}
1100
1101// TODO(hjd): test for bignums
1102// Convert an expression into a sort style comparison function.
1103// Exported for testing.
1104export function cmpFromExpr<T extends KeySet>(
1105  expr: Expr,
1106): (l: Event<T>, r: Event<T>) => number {
1107  return (l: Event<T>, r: Event<T>) => {
1108    const lhs = expr.execute(l);
1109    const rhs = expr.execute(r);
1110    const lhsRank = primativeToRank(lhs);
1111    const rhsRank = primativeToRank(rhs);
1112    if (lhsRank < rhsRank) {
1113      return -1;
1114    } else if (lhsRank > rhsRank) {
1115      return 1;
1116    } else {
1117      // Double equals on purpose so 0 == false and 1 == true are true
1118      if (lhs == rhs) {
1119        return 0;
1120      } else if (lhs! < rhs!) {
1121        return -1;
1122      } else {
1123        return 1;
1124      }
1125    }
1126  };
1127}
1128
1129// Convert a 'sort' into a sort() style comparison function.
1130// Exported for testing.
1131export function cmpFromSort<T extends KeySet>(
1132  sort: Sort,
1133): (l: Event<T>, r: Event<T>) => number {
1134  const cmp = cmpFromExpr<T>(sort.expression);
1135  if (sort.direction === Direction.ASC) {
1136    return cmp;
1137  } else {
1138    // cmp(r, l) is better than -cmp(l, r) since JS distinguishes
1139    // between -0 and 0.
1140    return (l: Event<T>, r: Event<T>) => cmp(r, l);
1141  }
1142}
1143