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