Home Reference Source Test

src/modules/constructRelationalGraph.js

// @flow
import {initialState} from './data';

import type {
  JoinCondition,
  SelectionCondition,
  Expression,
  Graph,
} from './types';

// Create a node of the table name with relevant fields
const addNode = (
  graph: Graph,
  tableName: string,
  selection?: ?SelectionCondition
) => {
  if (!(tableName in graph)) {
    graph[tableName] = {
      selections: [],
      edges: {},
    };
  }
  if (selection) {
    graph[tableName]['selections'].push(selection);
  }
  return tableName;
};

// Add an edge for two tables if there's a valid join relation between them
const addEdge = (
  graph: Graph,
  leftTable: string,
  rightTable: string,
  joinCondition: JoinCondition,
  joinType: string,
  leftSelection?: ?SelectionCondition,
  rightSelection?: ?SelectionCondition
) => {
  addNode(graph, leftTable, leftSelection);
  addNode(graph, rightTable, rightSelection);
  if (!(rightTable in graph[leftTable]['edges'])) {
    graph[leftTable]['edges'][rightTable] = [];
  }
  if (!(leftTable in graph[rightTable]['edges'])) {
    graph[rightTable]['edges'][leftTable] = [];
  }
  const edge = {condition: joinCondition, type: joinType};
  graph[leftTable]['edges'][rightTable].push(edge);
  graph[rightTable]['edges'][leftTable].push(edge);
};

const conditionIsOnValidAttributes = (
  attribute: string,
  tables: Array<string>
) => {
  const {sourceData} = initialState;
  for (const table of tables) {
    if (sourceData[table] && sourceData[table].columns.includes(attribute)) {
      return true;
    }
  }
  return false;
};

// function to check if a selection condition is a join condition
// assuming that the selection condition is on a single table and join condition is between two tables
const isValidJoinColumn = (attribute: string, tables: Array<string>) => {
  if (attribute.includes('.')) {
    const table = attribute.split('.')[0];
    if (!tables.includes(table)) return false;
  } else if (!conditionIsOnValidAttributes(attribute, tables)) {
    return false;
  }
  return true;
};

export const getTableFromAttribute = (
  attribute: string,
  tables: Array<string>
): string => {
  if (attribute.includes('.')) {
    return attribute.split('.')[0];
  }
  for (const table of tables) {
    if (conditionIsOnValidAttributes(attribute, [table])) {
      return table;
    }
  }
  // this should never happen except for edge cases
  return '';
};

const parseSelectionExpression = (
  graph: Graph,
  expr: Expression,
  tables: Array<string>,
  globalSelections: Array<SelectionCondition>
) => {
  if ('and' in expr) {
    const conditions = expr['and'].clauses;
    for (const condition of conditions) {
      parseSelectionExpression(graph, condition, tables, globalSelections);
    }
  } else if ('cmp' in expr) {
    const selection = expr['cmp'];
    if (
      isValidJoinColumn(selection.lhs, tables) &&
      isValidJoinColumn(selection.rhs, tables)
    ) {
      // this in future can be used as a join condition
      // right now we are just adding it to the global selections
      globalSelections.push(expr);
    } else {
      const {lhs, rhs} = selection;
      const leftTable = getTableFromAttribute(lhs, tables);
      const rightTable = getTableFromAttribute(rhs, tables);
      const table = leftTable || rightTable;
      if (table !== '') addNode(graph, table, expr);
      else globalSelections.push(expr);
    }
  } else {
    console.error(
      'Invalid selection expression for Join Order Optimization',
      expr
    );
  }
};

const parseJoinExpression = (
  graph: Graph,
  expr: Expression,
  joinType: string,
  tables: Array<string>
): void => {
  if (typeof expr === 'string') {
    throw new Error(
      'Invalid join expression for Join Order Optimization. This type of join condition is not supported currently'
    );
  } else if ('and' in expr) {
    const conditions = expr['and'].clauses;
    for (const condition of conditions) {
      parseJoinExpression(graph, condition, joinType, tables);
    }
  } else if ('cmp' in expr) {
    const {lhs, rhs} = expr['cmp'];
    const leftTable = getTableFromAttribute(lhs, tables);
    const rightTable = getTableFromAttribute(rhs, tables);
    if (leftTable === '' || rightTable === '') {
      throw new Error(
        'Invalid join expression for Join Order Optimization. This type of join condition is not supported currently'
      );
    }
    addEdge(graph, leftTable, rightTable, expr['cmp'], joinType);
  } else {
    throw new Error('Invalid join expression for Join Order Optimization');
  }
};

// function used to check all the tables that are involved in this query
const preParseExpression = (expr: Expression, tables: Array<string>) => {
  if (typeof expr === 'object') {
    if ('relation' in expr) tables.push(expr['relation']);
    for (const key in expr) {
      preParseExpression(expr[key], tables);
    }
  } else if (Array.isArray(expr)) {
    for (const item of expr) {
      preParseExpression(item, tables);
    }
  }
};

const parseExpression = (
  graph: Graph,
  expr: Expression,
  tables: Array<string>,
  globalSelections: Array<SelectionCondition>
): void => {
  if ('selection' in expr) {
    const selection = expr['selection'].arguments.select;
    parseSelectionExpression(graph, selection, tables, globalSelections);
    const children = expr['selection'].children;
    for (const child of children) {
      parseExpression(graph, child, tables, globalSelections);
    }
  } else if ('join' in expr) {
    const joinExpr = expr['join'];
    const {left, right, type, condition} = joinExpr;
    parseJoinExpression(graph, condition, type, tables);
    parseExpression(graph, left, tables, globalSelections);
    parseExpression(graph, right, tables, globalSelections);
  } else if ('relation' in expr) {
    const table = expr['relation'];
    addNode(graph, table);
  } else {
    throw new Error('Invalid expression type for join order optimization');
  }
};

/**
 * Constructs a relational graph from the expr
 * Nodes will have (tableName and selection criteria if any on that table)
 * Edges are the join relations
 * @param {*} expr
 */
export const constructRelationalGraph = (expr: {
  [key: string]: any,
}): {
  graph: Graph,
  globalSelections: Array<SelectionCondition>,
  canOptimize: boolean,
} => {
  const graph: Graph = {};
  const tables: Array<string> = [];
  const globalSelections: Array<SelectionCondition> = [];
  preParseExpression(expr, tables);
  // if duplicate tables found, console error and don't proceed
  const uniqueTables = new Set(tables);
  if (uniqueTables.size !== tables.length) {
    console.error('Duplicate tables found in the query');
    return {
      graph: graph,
      globalSelections: globalSelections,
      canOptimize: false,
    };
  }

  let canOptimize = true;
  try {
    parseExpression(graph, expr, tables, globalSelections);
  } catch (e) {
    console.error(e.message);
    canOptimize = false;
  }
  return {graph: graph, globalSelections: globalSelections, canOptimize};
};