xref: /aosp_15_r20/external/perfetto/ui/src/trace_processor/sql_utils.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 {SortDirection} from '../base/comparison_utils';
16import {isString} from '../base/object_utils';
17import {sqliteString} from '../base/string_utils';
18import {Engine} from './engine';
19import {NUM, SqlValue} from './query_result';
20
21export interface OrderClause {
22  fieldName: string;
23  direction?: SortDirection;
24}
25
26export type CommonTableExpressions = {
27  [key: string]: string | undefined;
28};
29
30// Interface for defining constraints which can be passed to a SQL query.
31export interface SQLConstraints {
32  commonTableExpressions?: CommonTableExpressions;
33  filters?: (undefined | string)[];
34  joins?: (undefined | string)[];
35  orderBy?: (undefined | string | OrderClause)[];
36  groupBy?: (undefined | string)[];
37  limit?: number;
38}
39
40function isDefined<T>(t: T | undefined): t is T {
41  return t !== undefined;
42}
43
44export function constraintsToQueryPrefix(c: SQLConstraints): string {
45  const ctes = Object.entries(c.commonTableExpressions ?? {}).filter(
46    ([_, value]) => isDefined(value),
47  );
48  if (ctes.length === 0) return '';
49  const cteStatements = ctes.map(([name, query]) => `${name} AS (${query})`);
50  return `WITH ${cteStatements.join(',\n')}`;
51}
52
53// Formatting given constraints into a string which can be injected into
54// SQL query.
55export function constraintsToQuerySuffix(c: SQLConstraints): string {
56  const result: string[] = [];
57
58  const joins = (c.joins ?? []).filter(isDefined);
59  if (joins.length > 0) {
60    result.push(...joins);
61  }
62  const filters = (c.filters ?? []).filter(isDefined);
63  if (filters.length > 0) {
64    result.push(`WHERE ${filters.join(' and ')}`);
65  }
66  const groupBy = (c.groupBy ?? []).filter(isDefined);
67  if (groupBy.length > 0) {
68    const groups = groupBy.join(', ');
69    result.push(`GROUP BY ${groups}`);
70  }
71  const orderBy = (c.orderBy ?? []).filter(isDefined);
72  if (orderBy.length > 0) {
73    const orderBys = orderBy.map((clause) => {
74      if (isString(clause)) {
75        return clause;
76      } else {
77        const direction = clause.direction ? ` ${clause.direction}` : '';
78        return `${clause.fieldName}${direction}`;
79      }
80    });
81    result.push(`ORDER BY ${orderBys.join(', ')}`);
82  }
83  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
84  if (c.limit) {
85    result.push(`LIMIT ${c.limit}`);
86  }
87  return result.join('\n');
88}
89
90// Trace Processor returns number | null for NUM_NULL, while most of the UI
91// code uses number | undefined. This functions provides a short-hand
92// conversion.
93// TODO(altimin): Support NUM_UNDEFINED as a first-class citizen.
94export function fromNumNull(n: number | null): number | undefined {
95  if (n === null) {
96    return undefined;
97  }
98  return n;
99}
100
101// Given a SqlValue, return a string representation of it to display to the
102// user.
103export function sqlValueToReadableString(val: SqlValue): string;
104export function sqlValueToReadableString(val?: SqlValue): string | undefined;
105export function sqlValueToReadableString(val?: SqlValue): string | undefined {
106  if (val === undefined) return undefined;
107  if (val instanceof Uint8Array) {
108    return `<blob length=${val.length}>`;
109  }
110  if (val === null) {
111    return 'NULL';
112  }
113  return val.toString();
114}
115
116// Given a SqlValue, return a string representation (properly escaped, if
117// necessary) of it to be used in a SQL query.
118export function sqlValueToSqliteString(val: SqlValue): string {
119  if (val instanceof Uint8Array) {
120    throw new Error("Can't pass blob back to trace processor as value");
121  }
122  if (val === null) {
123    return 'NULL';
124  }
125  if (typeof val === 'string') {
126    return sqliteString(val);
127  }
128  return `${val}`;
129}
130
131// Return a SQL predicate that can be used to compare with the given `value`,
132// correctly handling NULLs.
133export function matchesSqlValue(value: SqlValue): string {
134  if (value === null) {
135    return 'IS NULL';
136  }
137  return `= ${sqlValueToSqliteString(value)}`;
138}
139
140export async function getTableRowCount(
141  engine: Engine,
142  tableName: string,
143): Promise<number | undefined> {
144  const result = await engine.query(
145    `SELECT COUNT() as count FROM ${tableName}`,
146  );
147  if (result.numRows() === 0) {
148    return undefined;
149  }
150  return result.firstRow({
151    count: NUM,
152  }).count;
153}
154
155export {SqlValue};
156
157/**
158 * Asynchronously creates a 'perfetto' table using the given engine and returns
159 * an disposable object to handle its cleanup.
160 *
161 * @param engine - The database engine to execute the query.
162 * @param tableName - The name of the table to be created.
163 * @param expression - The SQL expression to define the table.
164 * @returns An AsyncDisposable which drops the created table when disposed.
165 *
166 * @example
167 * const engine = new Engine();
168 * const tableName = 'my_perfetto_table';
169 * const expression = 'SELECT * FROM source_table';
170 *
171 * const table = await createPerfettoTable(engine, tableName, expression);
172 *
173 * // Use the table...
174 *
175 * // Cleanup the table when done
176 * await table[Symbol.asyncDispose]();
177 */
178export async function createPerfettoTable(
179  engine: Engine,
180  tableName: string,
181  expression: string,
182): Promise<AsyncDisposable> {
183  await engine.query(`CREATE PERFETTO TABLE ${tableName} AS ${expression}`);
184  return {
185    [Symbol.asyncDispose]: async () => {
186      await engine.tryQuery(`DROP TABLE IF EXISTS ${tableName}`);
187    },
188  };
189}
190
191/**
192 * Asynchronously creates a SQL view using the given engine and returns an
193 * disposable object to handle its cleanup.
194 *
195 * @param engine - The database engine to execute the query.
196 * @param viewName - The name of the view to be created.
197 * @param as - The SQL expression to define the table.
198 * @returns An AsyncDisposable which drops the created table when disposed.
199 *
200 * @example
201 * const engine = new Engine();
202 * const viewName = 'my_view';
203 * const expression = 'SELECT * FROM source_table';
204 *
205 * const view = await createView(engine, viewName, expression);
206 *
207 * // Use the view...
208 *
209 * // Cleanup the view when done
210 * await view[Symbol.asyncDispose]();
211 */
212export async function createView(
213  engine: Engine,
214  viewName: string,
215  as: string,
216): Promise<AsyncDisposable> {
217  await engine.query(`CREATE VIEW ${viewName} AS ${as}`);
218  return {
219    [Symbol.asyncDispose]: async () => {
220      await engine.tryQuery(`DROP VIEW IF EXISTS ${viewName}`);
221    },
222  };
223}
224
225export async function createVirtualTable(
226  engine: Engine,
227  tableName: string,
228  using: string,
229): Promise<AsyncDisposable> {
230  await engine.query(`CREATE VIRTUAL TABLE ${tableName} USING ${using}`);
231  return {
232    [Symbol.asyncDispose]: async () => {
233      await engine.tryQuery(`DROP TABLE IF EXISTS ${tableName}`);
234    },
235  };
236}
237
238/**
239 * Asynchronously creates a 'perfetto' index using the given engine and returns
240 * an disposable object to handle its cleanup.
241 *
242 * @param engine - The database engine to execute the query.
243 * @param indexName - The name of the index to be created.
244 * @param expression - The SQL expression containing the table and columns.
245 * @returns An AsyncDisposable which drops the created table when disposed.
246 *
247 * @example
248 * const engine = new Engine();
249 * const indexName = 'my_perfetto_index';
250 * const expression = 'my_perfetto_table(foo)';
251 *
252 * const index = await createPerfettoIndex(engine, indexName, expression);
253 *
254 * // Use the index...
255 *
256 * // Cleanup the index when done
257 * await index[Symbol.asyncDispose]();
258 */
259export async function createPerfettoIndex(
260  engine: Engine,
261  indexName: string,
262  expression: string,
263): Promise<AsyncDisposable> {
264  await engine.query(`create perfetto index ${indexName} on ${expression}`);
265  return {
266    [Symbol.asyncDispose]: async () => {
267      await engine.tryQuery(`drop perfetto index ${indexName}`);
268    },
269  };
270}
271