xref: /aosp_15_r20/external/perfetto/ui/src/components/widgets/sql/table/column.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 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 m from 'mithril';
16import {SqlValue} from '../../../../trace_processor/query_result';
17import {SortDirection} from '../../../../base/comparison_utils';
18import {arrayEquals} from '../../../../base/array_utils';
19import {Trace} from '../../../../public/trace';
20
21// We are dealing with two types of columns here:
22// - Column, which is shown to a user in table (high-level, ColumnTable).
23// - Column in the underlying SQL data (low-level, SqlColumn).
24// They are related, but somewhat separate due to the fact that some table columns need to work with multiple SQL values to display it properly.
25// For example, a "time range" column would need both timestamp and duration to display interactive experience (e.g. highlight the time range on hover).
26// Each TableColumn has a primary SqlColumn, as well as optional dependent columns.
27
28// A source table for a SQL column, representing the joined table and the join constraints.
29export type SourceTable = {
30  table: string;
31  joinOn: {[key: string]: SqlColumn};
32  // Whether more performant 'INNER JOIN' can be used instead of 'LEFT JOIN'.
33  // Special care should be taken to ensure that a) all rows exist in a target table, and b) the source is not null, otherwise the rows will be filtered out.
34  // false by default.
35  innerJoin?: boolean;
36};
37
38// A column in the SQL query. It can be either a column from a base table or a "lookup" column from a joined table.
39export type SqlColumn =
40  | string
41  | {
42      column: string;
43      source: SourceTable;
44    };
45
46// List of columns of args, corresponding to arg values, which cause a short-form of the ID to be generated.
47// (e.g. arg_set_id[foo].int instead of args[arg_set_id,key=foo].int_value).
48const ARG_COLUMN_TO_SUFFIX: {[key: string]: string} = {
49  display_value: '',
50  int_value: '.int',
51  string_value: '.str',
52  real_value: '.real',
53};
54
55// A unique identifier for the SQL column.
56export function sqlColumnId(column: SqlColumn): string {
57  if (typeof column === 'string') {
58    return column;
59  }
60  // Special case: If the join is performed on a single column `id`, we can use a simpler representation (i.e. `table[id].column`).
61  if (arrayEquals(Object.keys(column.source.joinOn), ['id'])) {
62    return `${column.source.table}[${sqlColumnId(Object.values(column.source.joinOn)[0])}].${column.column}`;
63  }
64  // Special case: args lookup. For it, we can use a simpler representation (i.e. `arg_set_id[key]`).
65  if (
66    column.column in ARG_COLUMN_TO_SUFFIX &&
67    column.source.table === 'args' &&
68    arrayEquals(Object.keys(column.source.joinOn).sort(), ['arg_set_id', 'key'])
69  ) {
70    const key = column.source.joinOn['key'];
71    const argSetId = column.source.joinOn['arg_set_id'];
72    return `${sqlColumnId(argSetId)}[${sqlColumnId(key)}]${ARG_COLUMN_TO_SUFFIX[column.column]}`;
73  }
74  // Otherwise, we need to list all the join constraints.
75  const lookup = Object.entries(column.source.joinOn)
76    .map(([key, value]): string => {
77      const valueStr = sqlColumnId(value);
78      if (key === valueStr) return key;
79      return `${key}=${sqlColumnId(value)}`;
80    })
81    .join(', ');
82  return `${column.source.table}[${lookup}].${column.column}`;
83}
84
85export function isSqlColumnEqual(a: SqlColumn, b: SqlColumn): boolean {
86  return sqlColumnId(a) === sqlColumnId(b);
87}
88
89function sqlColumnName(column: SqlColumn): string {
90  if (typeof column === 'string') {
91    return column;
92  }
93  return column.column;
94}
95
96// Interface which allows TableColumn and TableColumnSet to interact with the table (e.g. add filters, or run the query).
97export interface TableManager {
98  addFilter(filter: Filter): void;
99
100  trace: Trace;
101  getSqlQuery(data: {[key: string]: SqlColumn}): string;
102}
103
104export interface TableColumnParams {
105  // See TableColumn.tag.
106  tag?: string;
107  // See TableColumn.alias.
108  alias?: string;
109  // See TableColumn.startsHidden.
110  startsHidden?: boolean;
111}
112
113export interface AggregationConfig {
114  dataType?: 'nominal' | 'quantitative';
115}
116
117// Class which represents a column in a table, which can be displayed to the user.
118// It is based on the primary SQL column, but also contains additional information needed for displaying it as a part of a table.
119export abstract class TableColumn {
120  constructor(params?: TableColumnParams) {
121    this.tag = params?.tag;
122    this.alias = params?.alias;
123    this.startsHidden = params?.startsHidden ?? false;
124  }
125
126  // Column title to be displayed.
127  // If not set, then `alias` will be used if it's unique.
128  // If `alias` is not set as well, then `sqlColumnId(primaryColumn())` will be used.
129  // TODO(altimin): This should return m.Children, but a bunch of things, including low-level widgets (Button, MenuItem, Anchor) need to be fixed first.
130  getTitle?(): string | undefined;
131
132  // Some SQL columns can map to multiple table columns. For example, a "utid" can be displayed as an integer column, or as a "thread" column, which displays "$thread_name [$tid]".
133  // Each column should have a unique id, so in these cases `tag` is appended to the primary column id to guarantee uniqueness.
134  readonly tag?: string;
135
136  // Preferred alias to be used in the SQL query. If omitted, column name will be used instead, including postfixing it with an integer if necessary.
137  // However, e.g. explicit aliases like `process_name` and `thread_name` are typically preferred to `name_1`, `name_2`, hence the need for explicit aliasing.
138  readonly alias?: string;
139
140  // Whether the column should be hidden by default.
141  readonly startsHidden: boolean;
142
143  // The SQL column this data corresponds to. Will be also used for sorting and aggregation purposes.
144  abstract primaryColumn(): SqlColumn;
145
146  // Sometimes to display an interactive cell more than a single value is needed (e.g. "time range" corresponds to (ts, dur) pair. While we want to show the duration, we would want to highlight the interval on hover, for which both timestamp and duration are needed.
147  dependentColumns?(): {[key: string]: SqlColumn};
148
149  // The set of underlying sql columns that should be sorted when this column is sorted.
150  sortColumns?(): SqlColumn[];
151
152  // Render a table cell. `value` corresponds to the fetched SQL value for the primary column, `dependentColumns` are the fetched values for the dependent columns.
153  abstract renderCell(
154    value: SqlValue,
155    tableManager: TableManager,
156    dependentColumns: {[key: string]: SqlValue},
157  ): m.Children;
158
159  // Specifies how this column should be aggregated. If not set, then all
160  // numeric columns will be treated as quantitative, and all other columns as
161  // nominal.
162  aggregation?(): AggregationConfig;
163}
164
165// Returns a unique identifier for the table column.
166export function tableColumnId(column: TableColumn): string {
167  const primaryColumnName = sqlColumnId(column.primaryColumn());
168  if (column.tag) {
169    return `${primaryColumnName}#${column.tag}`;
170  }
171  return primaryColumnName;
172}
173
174export function tableColumnAlias(column: TableColumn): string {
175  return column.alias ?? sqlColumnName(column.primaryColumn());
176}
177
178// This class represents a set of columns, from which the user can choose which columns to display. It is typically impossible or impractical to list all possible columns, so this class allows to discover them dynamically.
179// Two examples of canonical TableColumnSet usage are:
180// - Argument sets, where the set of arguments can be arbitrary large (and can change when the user changes filters on the table).
181// - Dependent columns, where the id.
182export abstract class TableColumnSet {
183  // TODO(altimin): This should return m.Children, same comment as in TableColumn.getTitle applies here.
184  abstract getTitle(): string;
185
186  // Returns a list of columns from this TableColumnSet which should be displayed by default.
187  initialColumns?(): TableColumn[];
188
189  // Returns a list of columns which can be added to the table from the current TableColumnSet.
190  abstract discover(manager: TableManager): Promise<
191    {
192      key: string;
193      column: TableColumn | TableColumnSet;
194    }[]
195  >;
196}
197
198// A filter which can be applied to the table.
199export interface Filter {
200  // Operation: it takes a list of column names and should return a valid SQL expression for this filter.
201  op: (cols: string[]) => string;
202  // Columns that the `op` should reference. The number of columns should match the number of interpolations in `op`.
203  columns: SqlColumn[];
204  // Returns a human-readable title for the filter. If not set, `op` will be used.
205  // TODO(altimin): This probably should return m.Children, but currently Button expects its label to be string.
206  getTitle?(): string;
207}
208
209// Returns a default string representation of the filter.
210export function formatFilter(filter: Filter): string {
211  return filter.op(filter.columns.map((c) => sqlColumnId(c)));
212}
213
214// Returns a human-readable title for the filter.
215export function filterTitle(filter: Filter): string {
216  if (filter.getTitle !== undefined) {
217    return filter.getTitle();
218  }
219  return formatFilter(filter);
220}
221
222// A column order clause, which specifies the column and the direction in which it should be sorted.
223export interface ColumnOrderClause {
224  column: SqlColumn;
225  direction: SortDirection;
226}
227