taozhi8833998 / node-sql-parser

Parse simple SQL statements into an abstract syntax tree (AST) with the visited tableList and convert it back to SQL
https://taozhi8833998.github.io/node-sql-parser/
Apache License 2.0
818 stars 181 forks source link

Option to include original column expressions (extending includeLocations) #2185

Closed evil-shrike closed 3 weeks ago

evil-shrike commented 4 weeks ago

Hi. Thanks for a great lib. I looking for a way to get original expressions for columns For example, given a query:

`SELECT
  campaign.id,
  ad_group.id,
  'http://' + ad_group_ad.ad.final_urls + '?tm=123' as url,
  ad_group_ad.resource_name,
  ad_group_ad.policy_summary.policy_topic_entries as policy_topic_entries,
  ad_group_ad.policy_summary.policy_topic_entries:topic as topic,
  ad_group_ad.policy_summary.policy_topic_entries:topic + '!' as topic2
FROM ad_group_ad

I need to extract the list of columns but I don't need their ast's, only string expressions. Basically, "campaign.id", "ad_group.id", "'http://' + ad_group_ad.ad.final_urls + '?tm=123'", ... "ad_group_ad.policy_summary.policy_topic_entries:topic + '!'".

I guess it's not possible at the moment, at least I couldn't find anything in resulting ASTs There's a parsing option includeLocations which roughly can connect column ast with its original string but it's a bit tedious.

export interface ParseOptions {
  includeLocations?: boolean;
}

Can you extend the meaning of includeLocations, or introduce another option, for including original expressions into AST nodes?

like additional expression field:

  {
    expr: {
      type: 'binary_expr',
      operator: '+',
      left: [Object],
      right: [Object],
      loc: [Object],
      expression: "ad_group_ad.policy_summary.policy_topic_entries:topic + '!'"
    },
    as: 'topic2'
  }
evil-shrike commented 4 weeks ago

ok, it's relatively easy to implement:

function getOrgExpr(query, node) {
  const loc = node.expr.loc;
  const lines = query.split('\n');
  console.log(node)
  let expr = '';
  for(let i=loc.start.line; i<=loc.end.line;i++) {
    const line = lines[i-1];
    if (i === loc.start.line && i === loc.end.line) {
      // first and last line
      expr += line.substring(loc.start.column-1, loc.end.column-1);
      console.log(`first and last. Line #${i}:"${line}", offsets: ${loc.start.column} - ${loc.end.column}`);
    } else if (i === loc.start.line) {
      // first line but not last
      expr += line.substring(loc.start.column-1);
    } else if (i === loc.end.line) {
      // last but not first
      expr += line.substring(0, loc.end.column-1);
    } else {
      // not first and not last, grab it whole
      exprt += line;
    }
  }
  return expr;
}
taozhi8833998 commented 4 weeks ago

@evil-shrike I exported the columnsToSQL function so you can use it directly. Can it get what you want?

const ast = parser.astify(`SELECT
  campaign.id,
  ad_group.id,
  'http://' + ad_group_ad.ad.final_urls + '?tm=123' as url,
  ad_group_ad.resource_name,
  ad_group_ad.policy_summary.policy_topic_entries as policy_topic_entries,
  ad_group_ad.policy_summary.policy_topic_entries:topic as topic,
  ad_group_ad.policy_summary.policy_topic_entries:topic + '!' as topic2
  FROM ad_group_ad`);
const columns = parser.columnsToSQL(ast.columns, ast.from);
console.log(columns);
/**
[
  '`campaign`.`id`',
  '`ad_group`.`id`',
  "'http://' + `ad_group_ad`.`ad`.`final_urls` + '?tm=123' AS `url`",
  '`ad_group_ad`.`resource_name`',
  '`ad_group_ad`.`policy_summary`.`policy_topic_entries` AS `policy_topic_entries`',
  '`ad_group_ad`.`policy_summary`.`policy_topic_entries:topic` AS `topic`',
  "`ad_group_ad`.`policy_summary`.`policy_topic_entries:topic` + '!' AS `topic2`"
]
**/
evil-shrike commented 3 weeks ago

Hi @taozhi8833998 , that's super cool, thanks. I'd only add line breaks in the tests (between "+", before "AS", etc, to test columns span over several lines)