blob: 9255608defe2505276a30cb9132c6ac117408df5 [file]
// 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 m from 'mithril';
import {NodeType} from './query_node';
import {Trace} from '../../public/trace';
import {SqlModules} from '../dev.perfetto.SqlModules/sql_modules';
import {ExplorePageState} from './explore_page';
import {showModal, closeModal} from '../../widgets/modal';
import {Editor} from '../../widgets/editor';
import {
SerializedGraph,
SerializedNode,
deserializeState,
} from './json_handler';
import {SqlSourceSerializedState} from './query_builder/nodes/sources/sql_source';
import {NodeContainerLayout} from './query_builder/graph/node_container';
export function showImportWithStatementModal(
trace: Trace,
sqlModules: SqlModules,
onStateUpdate: (state: ExplorePageState) => void,
) {
let sqlText = '';
showModal({
title: 'Import from WITH statement',
content: m(
'div',
{
style: {
'border-top': '1px solid var(--pf-color-border)',
'min-height': '10rem',
'overflow': 'hidden auto',
},
},
m(Editor, {
text: sqlText,
onUpdate: (text: string) => {
sqlText = text;
},
}),
),
buttons: [
{
text: 'Import',
action: () => {
const json = createGraphFromSql(sqlText);
const newState = deserializeState(json, trace, sqlModules);
onStateUpdate(newState);
closeModal();
},
},
{
text: 'Cancel',
action: () => {
closeModal();
},
},
],
});
}
// Simplified representation of a query node for parsing
interface ParsedNode {
name: string;
query: string;
dependencies: string[];
}
interface ParsedSql {
nodes: ParsedNode[];
modules: string;
}
function parseSqlWithModules(sql: string): ParsedSql {
const modules: string[] = [];
const lines = sql.split('\n');
let lastModuleLine = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.toUpperCase().startsWith('PERFETTO INCLUDE MODULE')) {
modules.push(line);
lastModuleLine = i;
} else if (line !== '' && !line.startsWith('--')) {
break;
}
}
const sqlWithoutModules = lines.slice(lastModuleLine + 1).join('\n');
const nodes = parseSql(sqlWithoutModules);
return {
nodes,
modules: modules.join('\n'),
};
}
function parseSql(sql: string): ParsedNode[] {
// TODO(mayzner): This whole logic is very fragile and should be replaced
// with Trace Processor SQL parser, when it becomes available.
const nodes: ParsedNode[] = [];
const sqlUpperCase = sql.toUpperCase();
const withIndex = sqlUpperCase.indexOf('WITH');
if (withIndex === -1) {
return nodes;
}
let openParens = 0;
let selectIndex = -1;
for (let i = withIndex + 4; i < sql.length; i++) {
if (sql[i] === '(') {
openParens++;
} else if (sql[i] === ')') {
openParens--;
} else if (
openParens === 0 &&
sqlUpperCase.substring(i).startsWith('SELECT')
) {
selectIndex = i;
break;
}
}
if (selectIndex === -1) {
throw new Error(
'Malformed SQL: No SELECT statement found after WITH clause.',
);
}
const withClause = sql.substring(withIndex + 4, selectIndex).trim();
openParens = 0;
let lastSplit = 0;
const parts: string[] = [];
for (let i = 0; i < withClause.length; i++) {
if (withClause[i] === '(') {
openParens++;
} else if (withClause[i] === ')') {
openParens--;
} else if (withClause[i] === ',' && openParens === 0) {
parts.push(withClause.substring(lastSplit, i));
lastSplit = i + 1;
}
}
parts.push(withClause.substring(lastSplit));
for (const part of parts) {
const asMatch = part.trim().match(/([a-zA-Z0-9_]+)\s+AS\s+\((.*)\)/is);
if (!asMatch) {
throw new Error(`Malformed CTE clause: ${part}`);
}
const name = asMatch[1].trim();
let query = asMatch[2].trim();
const dependencies = [];
for (const node of nodes) {
const regex = new RegExp(`\\b${node.name}\\b`, 'g');
if (query.match(regex)) {
dependencies.push(node.name);
query = query.replace(regex, `$${node.name}`);
}
}
nodes.push({
name,
query,
dependencies,
});
}
// Handle the final SELECT statement
const finalSelectQuery = sql.substring(selectIndex).trim();
let finalQueryWithVars = finalSelectQuery;
const finalDependencies = [];
for (const node of nodes) {
const regex = new RegExp(`\\b${node.name}\\b`, 'g');
if (finalQueryWithVars.match(regex)) {
finalDependencies.push(node.name);
finalQueryWithVars = finalQueryWithVars.replace(regex, `$${node.name}`);
}
}
let finalNodeName = 'output';
let counter = 1;
const existingNames = new Set(nodes.map((n) => n.name));
while (existingNames.has(finalNodeName)) {
finalNodeName = `output_${counter}`;
counter++;
}
nodes.push({
name: finalNodeName,
query: finalQueryWithVars,
dependencies: finalDependencies,
});
return nodes;
}
export function createGraphFromSql(sql: string): string {
const {nodes: parsedNodes, modules} = parseSqlWithModules(sql);
const serializedNodes: SerializedNode[] = [];
const nodeLayouts: {[key: string]: NodeContainerLayout} = {};
const rootNodeIds: string[] = [];
const nodeMap = new Map<string, SerializedNode>();
for (const parsedNode of parsedNodes) {
const nodeId = parsedNode.name;
const node: SerializedNode = {
nodeId,
type: NodeType.kSqlSource,
state: {
sql: parsedNode.query,
filters: [],
customTitle: nodeId,
},
nextNodes: [],
prevNodes: [],
};
serializedNodes.push(node);
nodeMap.set(nodeId, node);
}
for (const parsedNode of parsedNodes) {
const node = nodeMap.get(parsedNode.name)!;
for (const dep of parsedNode.dependencies) {
const depNode = nodeMap.get(dep)!;
depNode.nextNodes.push(node.nodeId);
if (!node.prevNodes) {
node.prevNodes = [];
}
node.prevNodes.push(depNode.nodeId);
}
if (parsedNode.dependencies.length === 0) {
rootNodeIds.push(node.nodeId);
}
}
if (modules && rootNodeIds.length > 0) {
const firstRootNode = nodeMap.get(rootNodeIds[0])!;
if (firstRootNode.type === NodeType.kSqlSource) {
const state = firstRootNode.state as SqlSourceSerializedState;
state.sql = `${modules}\n${state.sql}`;
}
}
const serializedGraph: SerializedGraph = {
nodes: serializedNodes,
rootNodeIds,
nodeLayouts,
};
return JSON.stringify(serializedGraph, null, 2);
}