• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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(
119  val: SqlValue | ReadonlyArray<SqlValue>,
120): string {
121  if (Array.isArray(val)) {
122    return val.map((v) => sqlValueToSqliteString(v)).join(',');
123  }
124  if (val instanceof Uint8Array) {
125    throw new Error("Can't pass blob back to trace processor as value");
126  }
127  if (val === null) {
128    return 'NULL';
129  }
130  if (typeof val === 'string') {
131    return sqliteString(val);
132  }
133  return `${val}`;
134}
135
136// Return a SQL predicate that can be used to compare with the given `value`,
137// correctly handling NULLs.
138export function matchesSqlValue(value: SqlValue): string {
139  if (value === null) {
140    return 'IS NULL';
141  }
142  return `= ${sqlValueToSqliteString(value)}`;
143}
144
145export async function getTableRowCount(
146  engine: Engine,
147  tableName: string,
148): Promise<number | undefined> {
149  const result = await engine.query(
150    `SELECT COUNT() as count FROM ${tableName}`,
151  );
152  if (result.numRows() === 0) {
153    return undefined;
154  }
155  return result.firstRow({
156    count: NUM,
157  }).count;
158}
159
160export {SqlValue};
161
162/**
163 * Asynchronously creates a 'perfetto' table using the given engine and returns
164 * an disposable object to handle its cleanup.
165 *
166 * @param engine - The database engine to execute the query.
167 * @param tableName - The name of the table to be created.
168 * @param expression - The SQL expression to define the table.
169 * @returns An AsyncDisposable which drops the created table when disposed.
170 *
171 * @example
172 * const engine = new Engine();
173 * const tableName = 'my_perfetto_table';
174 * const expression = 'SELECT * FROM source_table';
175 *
176 * const table = await createPerfettoTable(engine, tableName, expression);
177 *
178 * // Use the table...
179 *
180 * // Cleanup the table when done
181 * await table[Symbol.asyncDispose]();
182 */
183export async function createPerfettoTable(
184  engine: Engine,
185  tableName: string,
186  expression: string,
187): Promise<AsyncDisposable> {
188  await engine.query(`CREATE PERFETTO TABLE ${tableName} AS ${expression}`);
189  return {
190    [Symbol.asyncDispose]: async () => {
191      await engine.tryQuery(`DROP TABLE IF EXISTS ${tableName}`);
192    },
193  };
194}
195
196/**
197 * Asynchronously creates a SQL view using the given engine and returns an
198 * disposable object to handle its cleanup.
199 *
200 * @param engine - The database engine to execute the query.
201 * @param viewName - The name of the view to be created.
202 * @param as - The SQL expression to define the table.
203 * @returns An AsyncDisposable which drops the created table when disposed.
204 *
205 * @example
206 * const engine = new Engine();
207 * const viewName = 'my_view';
208 * const expression = 'SELECT * FROM source_table';
209 *
210 * const view = await createView(engine, viewName, expression);
211 *
212 * // Use the view...
213 *
214 * // Cleanup the view when done
215 * await view[Symbol.asyncDispose]();
216 */
217export async function createView(
218  engine: Engine,
219  viewName: string,
220  as: string,
221): Promise<AsyncDisposable> {
222  await engine.query(`CREATE VIEW ${viewName} AS ${as}`);
223  return {
224    [Symbol.asyncDispose]: async () => {
225      await engine.tryQuery(`DROP VIEW IF EXISTS ${viewName}`);
226    },
227  };
228}
229
230export async function createVirtualTable(
231  engine: Engine,
232  tableName: string,
233  using: string,
234): Promise<AsyncDisposable> {
235  await engine.query(`CREATE VIRTUAL TABLE ${tableName} USING ${using}`);
236  return {
237    [Symbol.asyncDispose]: async () => {
238      await engine.tryQuery(`DROP TABLE IF EXISTS ${tableName}`);
239    },
240  };
241}
242
243/**
244 * Asynchronously creates a 'perfetto' index using the given engine and returns
245 * an disposable object to handle its cleanup.
246 *
247 * @param engine - The database engine to execute the query.
248 * @param indexName - The name of the index to be created.
249 * @param expression - The SQL expression containing the table and columns.
250 * @returns An AsyncDisposable which drops the created table when disposed.
251 *
252 * @example
253 * const engine = new Engine();
254 * const indexName = 'my_perfetto_index';
255 * const expression = 'my_perfetto_table(foo)';
256 *
257 * const index = await createPerfettoIndex(engine, indexName, expression);
258 *
259 * // Use the index...
260 *
261 * // Cleanup the index when done
262 * await index[Symbol.asyncDispose]();
263 */
264export async function createPerfettoIndex(
265  engine: Engine,
266  indexName: string,
267  expression: string,
268): Promise<AsyncDisposable> {
269  await engine.query(`create perfetto index ${indexName} on ${expression}`);
270  return {
271    [Symbol.asyncDispose]: async () => {
272      await engine.tryQuery(`drop perfetto index ${indexName}`);
273    },
274  };
275}
276