| // Copyright (C) 2024 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {z} from 'zod'; |
| import { |
| SqlModules, |
| SqlColumn, |
| SqlFunction, |
| SqlArgument, |
| SqlMacro, |
| SqlModule, |
| SqlPackage, |
| SqlTable, |
| SqlTableFunction, |
| } from './sql_modules'; |
| import {SqlTableDefinition} from '../../components/widgets/sql/table/table_description'; |
| import {TableColumn} from '../../components/widgets/sql/table/table_column'; |
| import {Trace} from '../../public/trace'; |
| import { |
| parsePerfettoSqlTypeFromString, |
| PerfettoSqlType, |
| } from '../../trace_processor/perfetto_sql_type'; |
| import {unwrapResult} from '../../base/result'; |
| import {createTableColumn} from '../../components/widgets/sql/table/columns'; |
| |
| // Runs a data availability check query (wrapped by check_to_query in Python). |
| // The query is `SELECT EXISTS(...) AS has_data` which always returns one row. |
| // Returns true if data is present, false if not or if the query fails. |
| async function runDataCheck(trace: Trace, sql: string): Promise<boolean> { |
| try { |
| const result = await trace.engine.query(sql); |
| // Use iter() to avoid type checking issues with VARINT |
| const iter = result.iter({}); |
| iter.next(); |
| const hasDataValue = iter.get('has_data'); |
| return typeof hasDataValue === 'bigint' |
| ? hasDataValue !== 0n |
| : Number(hasDataValue) !== 0; |
| } catch (_e) { |
| // If query fails, assume no data |
| return false; |
| } |
| } |
| |
| export class SqlModulesImpl implements SqlModules { |
| readonly packages: SqlPackage[]; |
| private disabledModules: Set<string> = new Set(); |
| private disabledTables: Set<string> = new Set(); |
| private tablesWithChecks: Set<string> = new Set(); |
| private initPromise: Promise<void> | undefined; |
| private readonly startInit: () => Promise<void>; |
| |
| constructor(trace: Trace, docs: SqlModulesDocsSchema) { |
| this.packages = docs.map((json) => new StdlibPackageImpl(trace, json)); |
| // Capture initialization logic in a closure to avoid storing trace/docs |
| this.startInit = () => this.computeDisabledModules(trace, docs); |
| } |
| |
| ensureInitialized(): Promise<void> { |
| if (this.initPromise === undefined) { |
| this.initPromise = this.startInit(); |
| } |
| return this.initPromise; |
| } |
| |
| async waitForInit(): Promise<void> { |
| await this.ensureInitialized(); |
| } |
| |
| private async computeDisabledModules( |
| trace: Trace, |
| docs: SqlModulesDocsSchema, |
| ): Promise<void> { |
| // Build dependency graph: module -> modules that include it |
| const dependents = new Map<string, Set<string>>(); |
| const modulesWithChecks = new Map<string, string>(); |
| |
| for (const pkg of docs) { |
| for (const mod of pkg.modules) { |
| const moduleName = mod.module_name; |
| |
| // Store data check SQL if present |
| if (mod.data_check_sql) { |
| modulesWithChecks.set(moduleName, mod.data_check_sql); |
| } |
| |
| // Build reverse dependency graph |
| if (mod.includes) { |
| for (const includedModule of mod.includes) { |
| if (!dependents.has(includedModule)) { |
| dependents.set(includedModule, new Set()); |
| } |
| dependents.get(includedModule)!.add(moduleName); |
| } |
| } |
| } |
| } |
| |
| // Check data availability for modules with checks |
| const missingDataModules = new Set<string>(); |
| // Track modules whose trace checks explicitly passed (have data) |
| const hasDataModules = new Set<string>(); |
| for (const [moduleName, checkSql] of modulesWithChecks) { |
| const hasData = await runDataCheck(trace, checkSql); |
| if (hasData) { |
| hasDataModules.add(moduleName); |
| } else { |
| missingDataModules.add(moduleName); |
| } |
| } |
| |
| // Compute "protected" modules: modules with passing checks AND all modules |
| // that depend on them (transitively). These should not be disabled by |
| // dependency propagation since they have access to data through at least |
| // one dependency with a passing trace check. |
| const protectedModules = new Set(hasDataModules); |
| const protectedQueue = Array.from(hasDataModules); |
| |
| while (protectedQueue.length > 0) { |
| const current = protectedQueue.shift()!; |
| const deps = dependents.get(current); |
| |
| if (deps) { |
| for (const dependent of deps) { |
| if (!protectedModules.has(dependent)) { |
| protectedModules.add(dependent); |
| protectedQueue.push(dependent); |
| } |
| } |
| } |
| } |
| |
| // BFS to find all transitive dependents of modules with missing data |
| // Skip disabling protected modules (those with passing checks or that |
| // depend on modules with passing checks) |
| const queue = Array.from(missingDataModules); |
| const disabled = new Set(missingDataModules); |
| |
| while (queue.length > 0) { |
| const current = queue.shift()!; |
| const deps = dependents.get(current); |
| |
| if (deps) { |
| for (const dependent of deps) { |
| if (!disabled.has(dependent) && !protectedModules.has(dependent)) { |
| disabled.add(dependent); |
| queue.push(dependent); |
| } |
| } |
| } |
| } |
| |
| this.disabledModules = disabled; |
| |
| // Run per-table data checks for tables that have their own check SQL. |
| // This provides finer granularity than module-level checks. |
| const tableChecks: Array<{name: string; sql: string}> = []; |
| for (const pkg of this.packages) { |
| for (const mod of pkg.modules) { |
| for (const table of mod.tables) { |
| if (table.dataCheckSql) { |
| this.tablesWithChecks.add(table.name); |
| tableChecks.push({name: table.name, sql: table.dataCheckSql}); |
| } |
| } |
| } |
| } |
| |
| for (const {name, sql} of tableChecks) { |
| const hasData = await runDataCheck(trace, sql); |
| if (!hasData) { |
| this.disabledTables.add(name); |
| } |
| } |
| } |
| |
| isModuleDisabled(moduleName: string): boolean { |
| return this.disabledModules.has(moduleName); |
| } |
| |
| tablePassedDataCheck(tableName: string): boolean | undefined { |
| if (!this.tablesWithChecks.has(tableName)) { |
| return undefined; |
| } |
| return !this.disabledTables.has(tableName); |
| } |
| |
| getDisabledModules(): ReadonlySet<string> { |
| return this.disabledModules; |
| } |
| |
| getTable(tableName: string): SqlTable | undefined { |
| for (const p of this.packages) { |
| const t = p.getTable(tableName); |
| if (t !== undefined) { |
| return t; |
| } |
| } |
| return; |
| } |
| |
| listTables(): SqlTable[] { |
| return this.packages.flatMap((p) => p.listTables()); |
| } |
| |
| listTablesNames(): string[] { |
| return this.packages.flatMap((p) => p.listTablesNames()); |
| } |
| |
| getModuleForTable(tableName: string): SqlModule | undefined { |
| for (const stdlibPackage of this.packages) { |
| const maybeTable = stdlibPackage.getModuleForTable(tableName); |
| if (maybeTable) { |
| return maybeTable; |
| } |
| } |
| return undefined; |
| } |
| |
| listModules(): SqlModule[] { |
| return this.packages.flatMap((p) => p.modules); |
| } |
| } |
| |
| export class StdlibPackageImpl implements SqlPackage { |
| readonly name: string; |
| readonly modules: SqlModule[]; |
| |
| constructor(trace: Trace, docs: DocsPackageSchemaType) { |
| this.name = docs.name; |
| this.modules = []; |
| for (const moduleJson of docs.modules) { |
| this.modules.push(new StdlibModuleImpl(trace, moduleJson)); |
| } |
| } |
| |
| getTable(tableName: string): SqlTable | undefined { |
| for (const module of this.modules) { |
| for (const t of module.tables) { |
| if (t.name == tableName) { |
| return t; |
| } |
| } |
| } |
| return undefined; |
| } |
| |
| listTables(): SqlTable[] { |
| return this.modules.flatMap((module) => module.tables); |
| } |
| |
| listTablesNames(): string[] { |
| return this.listTables().map((t) => t.name); |
| } |
| |
| getModuleForTable(tableName: string): SqlModule | undefined { |
| for (const module of this.modules) { |
| for (const t of module.tables) { |
| if (t.name == tableName) { |
| return module; |
| } |
| } |
| } |
| return undefined; |
| } |
| |
| getSqlTableDefinition(tableName: string): SqlTableDefinition | undefined { |
| for (const module of this.modules) { |
| for (const t of module.tables) { |
| if (t.name == tableName) { |
| return module.getSqlTableDefinition(tableName); |
| } |
| } |
| } |
| return undefined; |
| } |
| } |
| |
| export class StdlibModuleImpl implements SqlModule { |
| readonly includeKey: string; |
| readonly tags: string[]; |
| readonly tables: SqlTable[]; |
| readonly functions: SqlFunction[]; |
| readonly tableFunctions: SqlTableFunction[]; |
| readonly macros: SqlMacro[]; |
| readonly dataCheckSql?: string; |
| readonly includes: string[]; |
| |
| constructor(trace: Trace, docs: DocsModuleSchemaType) { |
| this.includeKey = docs.module_name; |
| this.tags = docs.tags; |
| this.dataCheckSql = docs.data_check_sql ?? undefined; |
| this.includes = docs.includes ?? []; |
| |
| const neededInclude = this.includeKey.startsWith('prelude') |
| ? undefined |
| : this.includeKey; |
| this.tables = docs.data_objects.map( |
| (json) => new SqlTableImpl(trace, json, neededInclude), |
| ); |
| |
| this.functions = docs.functions.map((json) => new StdlibFunctionImpl(json)); |
| this.tableFunctions = docs.table_functions.map( |
| (json) => new StdlibTableFunctionImpl(json), |
| ); |
| this.macros = docs.macros.map((json) => new StdlibMacroImpl(json)); |
| } |
| |
| getTable(tableName: string): SqlTable | undefined { |
| for (const t of this.tables) { |
| if (t.name == tableName) { |
| return t; |
| } |
| } |
| return undefined; |
| } |
| |
| getSqlTableDefinition(tableName: string): SqlTableDefinition | undefined { |
| const sqlTable = this.getTable(tableName); |
| if (sqlTable === undefined) { |
| return undefined; |
| } |
| return { |
| imports: [this.includeKey], |
| name: sqlTable.name, |
| columns: sqlTable.columns.map((col) => ({ |
| column: col.name, |
| type: col.type, |
| })), |
| }; |
| } |
| } |
| |
| class StdlibMacroImpl implements SqlMacro { |
| readonly name: string; |
| readonly summaryDesc: string; |
| readonly description: string; |
| readonly args: SqlArgument[]; |
| readonly returnType: string; |
| |
| constructor(docs: DocsMacroSchemaType) { |
| this.name = docs.name; |
| this.summaryDesc = docs.summary_desc; |
| this.description = docs.desc; |
| this.returnType = docs.return_type; |
| this.args = []; |
| this.args = docs.args.map((json) => new StdlibFunctionArgImpl(json)); |
| } |
| } |
| |
| class StdlibTableFunctionImpl implements SqlTableFunction { |
| readonly name: string; |
| readonly summaryDesc: string; |
| readonly description: string; |
| readonly args: SqlArgument[]; |
| readonly returnCols: SqlColumn[]; |
| |
| constructor(docs: DocsTableFunctionSchemaType) { |
| this.name = docs.name; |
| this.summaryDesc = docs.summary_desc; |
| this.description = docs.desc; |
| this.args = docs.args.map((json) => new StdlibFunctionArgImpl(json)); |
| this.returnCols = docs.cols.map( |
| (json) => new StdlibColumnImpl(json, this.name), |
| ); |
| } |
| } |
| |
| class StdlibFunctionImpl implements SqlFunction { |
| readonly name: string; |
| readonly summaryDesc: string; |
| readonly description: string; |
| readonly args: SqlArgument[]; |
| readonly returnType: string; |
| readonly returnDesc: string; |
| |
| constructor(docs: DocsFunctionSchemaType) { |
| this.name = docs.name; |
| this.summaryDesc = docs.summary_desc; |
| this.description = docs.desc; |
| this.returnType = docs.return_type; |
| this.returnDesc = docs.return_desc; |
| this.args = docs.args.map((json) => new StdlibFunctionArgImpl(json)); |
| } |
| } |
| |
| class SqlTableImpl implements SqlTable { |
| name: string; |
| includeKey?: string; |
| description: string; |
| type: string; |
| importance?: 'core' | 'high' | 'mid' | 'low'; |
| dataCheckSql?: string; |
| columns: SqlColumn[]; |
| idColumn: SqlColumn | undefined; |
| |
| constructor( |
| readonly trace: Trace, |
| docs: DocsDataObjectSchemaType, |
| includeKey: string | undefined, |
| ) { |
| this.name = docs.name; |
| this.includeKey = includeKey; |
| this.description = docs.desc; |
| this.type = docs.type; |
| this.importance = docs.importance ?? undefined; |
| this.dataCheckSql = docs.data_check_sql ?? undefined; |
| this.columns = docs.cols.map( |
| (json) => new StdlibColumnImpl(json, this.name), |
| ); |
| } |
| |
| getTableColumns(): TableColumn[] { |
| return this.columns.map((col) => |
| createTableColumn({ |
| trace: this.trace, |
| column: col.name, |
| type: col.type, |
| }), |
| ); |
| } |
| } |
| |
| class StdlibColumnImpl implements SqlColumn { |
| name: string; |
| type: PerfettoSqlType; |
| description: string; |
| |
| constructor(docs: DocsArgOrColSchemaType, tableName: string) { |
| this.name = docs.name; |
| this.type = unwrapResult( |
| parsePerfettoSqlTypeFromString({ |
| type: docs.type, |
| table: tableName, |
| column: this.name, |
| }), |
| ); |
| this.description = docs.desc; |
| } |
| } |
| |
| class StdlibFunctionArgImpl implements SqlArgument { |
| name: string; |
| description: string; |
| type: string; |
| |
| constructor(docs: DocsArgOrColSchemaType) { |
| this.type = docs.type; |
| this.description = docs.desc; |
| this.name = docs.name; |
| } |
| } |
| |
| export const ARG_OR_COL_SCHEMA = z.object({ |
| name: z.string(), |
| type: z.string(), |
| desc: z.string(), |
| table: z.string().nullable(), |
| column: z.string().nullable(), |
| }); |
| type DocsArgOrColSchemaType = z.infer<typeof ARG_OR_COL_SCHEMA>; |
| |
| export const DATA_OBJECT_SCHEMA = z.object({ |
| name: z.string(), |
| desc: z.string(), |
| summary_desc: z.string(), |
| type: z.string(), |
| importance: z.enum(['core', 'high', 'mid', 'low']).nullish(), |
| data_check_sql: z.string().nullish(), |
| cols: z.array(ARG_OR_COL_SCHEMA), |
| }); |
| type DocsDataObjectSchemaType = z.infer<typeof DATA_OBJECT_SCHEMA>; |
| |
| export const FUNCTION_SCHEMA = z.object({ |
| name: z.string(), |
| desc: z.string(), |
| summary_desc: z.string(), |
| args: z.array(ARG_OR_COL_SCHEMA), |
| return_type: z.string(), |
| return_desc: z.string(), |
| }); |
| type DocsFunctionSchemaType = z.infer<typeof FUNCTION_SCHEMA>; |
| |
| export const TABLE_FUNCTION_SCHEMA = z.object({ |
| name: z.string(), |
| desc: z.string(), |
| summary_desc: z.string(), |
| args: z.array(ARG_OR_COL_SCHEMA), |
| cols: z.array(ARG_OR_COL_SCHEMA), |
| }); |
| type DocsTableFunctionSchemaType = z.infer<typeof TABLE_FUNCTION_SCHEMA>; |
| |
| export const MACRO_SCHEMA = z.object({ |
| name: z.string(), |
| desc: z.string(), |
| summary_desc: z.string(), |
| return_desc: z.string(), |
| return_type: z.string(), |
| args: z.array(ARG_OR_COL_SCHEMA), |
| }); |
| type DocsMacroSchemaType = z.infer<typeof MACRO_SCHEMA>; |
| |
| const MODULE_SCHEMA = z.object({ |
| module_name: z.string(), |
| tags: z.array(z.string()), |
| data_objects: z.array(DATA_OBJECT_SCHEMA), |
| functions: z.array(FUNCTION_SCHEMA), |
| table_functions: z.array(TABLE_FUNCTION_SCHEMA), |
| macros: z.array(MACRO_SCHEMA), |
| data_check_sql: z.string().nullish(), |
| includes: z.array(z.string()).nullish(), |
| }); |
| type DocsModuleSchemaType = z.infer<typeof MODULE_SCHEMA>; |
| |
| const PACKAGE_SCHEMA = z.object({ |
| name: z.string(), |
| modules: z.array(MODULE_SCHEMA), |
| }); |
| type DocsPackageSchemaType = z.infer<typeof PACKAGE_SCHEMA>; |
| |
| export const SQL_MODULES_DOCS_SCHEMA = z.array(PACKAGE_SCHEMA); |
| export type SqlModulesDocsSchema = z.infer<typeof SQL_MODULES_DOCS_SCHEMA>; |