|
| 1 | +private import qtil.parameterization.SignatureTypes |
| 2 | +private import qtil.parameterization.SignaturePredicates |
| 3 | +private import qtil.tuple.StringTuple as CustomStringTuple |
| 4 | +private import qtil.strings.Chars |
| 5 | +private import qtil.inheritance.Instance |
| 6 | +private import codeql.util.Boolean |
| 7 | + |
| 8 | +class StringTuple = CustomStringTuple::StringTuple<Chars::comma/0>::Tuple; |
| 9 | + |
| 10 | +/** |
| 11 | + * A module that allows multiple values to be aggregated at the same time, where each value |
| 12 | + * (including the aggregated value) acts like a tuple. |
| 13 | + * |
| 14 | + * The tuple may contain any number of the following types of columns: |
| 15 | + * - `string` columns, which are concatenated with a separator |
| 16 | + * - `int` columns, which are summed |
| 17 | + * |
| 18 | + * Additionally, the unique values of each column can be counted, and the total number of unique |
| 19 | + * aggregated tuples can be counted. |
| 20 | + * |
| 21 | + * This can be useful for writing generic code where a module may wish to perform an unknown number |
| 22 | + * of aggregations in a context where it cannot perform the aggregation for itself. |
| 23 | + * |
| 24 | + * Each value to be aggregated should be of type `AggregableTuple::Piece`, and pieces should be |
| 25 | + * aggregated with `concat(Piece p | p, ",")`, as the underlying representation is a comma |
| 26 | + * -separated string (a `StringTuple`). |
| 27 | + * |
| 28 | + * After aggregation, the result should be cast to a `AggregableTuple::Sum` to access the |
| 29 | + * aggregated values of each column. |
| 30 | + * |
| 31 | + * Note: This will not be as performant as individual aggregations, and should only be used in cases |
| 32 | + * where a single aggregation is not practical. |
| 33 | + * |
| 34 | + * Example usage: |
| 35 | + * ```ql |
| 36 | + * // What values a "person" may aggregate over defined here: |
| 37 | + * AggregableTuple::Piece personAggregant(Person p) { |
| 38 | + * result = AggregableTuple::initString(p.name) |
| 39 | + * .appendInt(p.age) |
| 40 | + * } |
| 41 | + * |
| 42 | + * // A usage of that aggregation can be defined separately: |
| 43 | + * predicate useAggregation(AggregableTuple::Sum<two/0>::Sum aggregated) { |
| 44 | + * exists(int counted, string names, int totalAge | |
| 45 | + * counted = aggregated.getCountTotal() and |
| 46 | + * names = aggregated.getAsJoinedString(0, ",") and |
| 47 | + * totalAge = aggregated.getAsSummedInt(1) and |
| 48 | + * // Use `counted`, `names`, and `totalAge` as needed |
| 49 | + * ) |
| 50 | + * } |
| 51 | + * ``` |
| 52 | + */ |
| 53 | +module AggregableTuple { |
| 54 | + |
| 55 | + /** |
| 56 | + * Begin the construction of a new piece of an aggregable tuple with a `string` column. |
| 57 | + * |
| 58 | + * Sets the first column of this tuple to be the given `string` value. The `Piece` |
| 59 | + * returned by this predicate can have additional columns appended to it of any type. |
| 60 | + */ |
| 61 | + bindingset[s] |
| 62 | + Piece initString(string s) { result = s } |
| 63 | + |
| 64 | + /** |
| 65 | + * Begin the construction of a new piece of an aggregable tuple with an `int` column. |
| 66 | + * |
| 67 | + * Sets the first column of this tuple to be the given `int` value. The `Piece` |
| 68 | + * returned by this predicate can have additional columns appended to it of any type. |
| 69 | + */ |
| 70 | + bindingset[i] |
| 71 | + Piece initInt(int i) { result = i.toString() } |
| 72 | + |
| 73 | + /** |
| 74 | + * A piece of an aggregable tuple, which can be used to aggregate multiple values at the same |
| 75 | + * time. |
| 76 | + * |
| 77 | + * This class can be built up one column at a time, beginning with one of the predicates `asInc`, |
| 78 | + * `asString`, or `asInt`. Additional columns can be appended to the piece using the `appendInc`, |
| 79 | + * `appendString`, or `appendInt` predicates. |
| 80 | + * |
| 81 | + * After all of the columns have been appended, the piece can be aggregated with |
| 82 | + * `concat(Piece p | p, ",")`. Then the result can be cast to `AggregableTuple::Sum` to access the |
| 83 | + * aggregated values of each column. |
| 84 | + */ |
| 85 | + bindingset[this] |
| 86 | + class Piece extends InfInstance<StringTuple>::Type { |
| 87 | + bindingset[this, s] |
| 88 | + Piece appendString(string s) { result = inst().append(s) } |
| 89 | + |
| 90 | + bindingset[this, i] |
| 91 | + Piece appendInt(int i) { result = inst().append(i.toString()) } |
| 92 | + } |
| 93 | + |
| 94 | + module Sum<Nullary::Ret<int>::pred/0 columns> { |
| 95 | + bindingset[this] |
| 96 | + class Sum extends InfInstance<StringTuple>::Type { |
| 97 | + |
| 98 | + bindingset[this] |
| 99 | + int getCountTotal() { |
| 100 | + result = (inst().size()) / columns() |
| 101 | + } |
| 102 | + |
| 103 | + /** |
| 104 | + * Since the underlying representation is a comma-separated string, the ith value of |
| 105 | + * the nth column can be found at the index `i * columns() + n`. |
| 106 | + * |
| 107 | + * This predicate returns all such indexes for the nth column. |
| 108 | + */ |
| 109 | + bindingset[this] |
| 110 | + int getARawColumnValueIndex(int colIdx) { |
| 111 | + colIdx in [0 .. columns()] and |
| 112 | + exists(int rowIdx | |
| 113 | + rowIdx = [0..getCountTotal() - 1] and |
| 114 | + result = rowIdx * columns() + colIdx |
| 115 | + ) |
| 116 | + } |
| 117 | + |
| 118 | + /** |
| 119 | + * Get all of the raw string values for the nth column of aggregated tuples. |
| 120 | + */ |
| 121 | + bindingset[this] |
| 122 | + string getARawColumn(int colIdx) { |
| 123 | + colIdx in [0 .. columns()] and |
| 124 | + result = inst().get(getARawColumnValueIndex(colIdx)) |
| 125 | + } |
| 126 | + |
| 127 | + bindingset[this] |
| 128 | + int countColumn(int colIdx) { |
| 129 | + colIdx in [0 .. columns()] and |
| 130 | + result = count(string item | item = getARawColumn(colIdx)) |
| 131 | + } |
| 132 | + |
| 133 | + /** |
| 134 | + * Get the nth column of aggregated tuples, treated as strings and joined with the given |
| 135 | + * separator. |
| 136 | + */ |
| 137 | + bindingset[this, sep] |
| 138 | + string getAsJoinedString(int colIdx, string sep) { |
| 139 | + colIdx in [0 .. columns()] and |
| 140 | + result = concat(string item | item = getARawColumn(colIdx) | item, sep) |
| 141 | + } |
| 142 | + |
| 143 | + /** |
| 144 | + * Get the nth column of aggregated tuples, treated as integers and summed. |
| 145 | + */ |
| 146 | + bindingset[this] |
| 147 | + int getAsSummedInt(int colIdx) { |
| 148 | + colIdx in [0 .. columns()] and |
| 149 | + result = sum(int item | item = getARawColumn(colIdx).toInt()) |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | +} |
0 commit comments