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