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