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