npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@questdb/sql-ast-parser

v0.1.1

Published

Simple QuestDB SQL parser/modifier

Downloads

65

Readme

Questdb-AST-Parser

Questdb-AST-Parser is a QuestDB SQL syntax parser. It produces a typed AST (Abstract Syntax Tree), covering the most common syntaxes of QuestDB.

It's based on pgsql-ast-parser

Installation

With NodeJS

npm i questdb-ast-parser

With Deno

Just reference it like:

import { /* imports here */ } from 'https://deno.land/x/pgsql_ast_parser/mod.ts';

Parsing SQL

Parse sql to an AST like this:

import { parse, Statement } from 'questdb-ast-parser';

// parse multiple statements
const ast: Statement[] = parse(`BEGIN TRANSACTION;
                                insert into my_table values (1, 'two')`);

// parse a single statement
const ast: Statement = parseFirst(`SELECT * FROM "my_table";`);

Inspecting SQL AST

Once you have parsed an AST, you might want to traverse it easily to know what's in it.

There is a helper for that: astVisitor.

Here is an example which lists all the tables used in a request, and which counts how many joins it contains:


import { astVisitor, parse } from 'questdb-ast-parser';

const tables = new Set();
let joins = 0;
const visitor = astVisitor(map => ({

    // implement here AST parts you want to hook

    tableRef: t => tables.add(t.name),
    join: t => {
        joins++;
        // call the default implementation of 'join'
        // this will ensure that the subtree is also traversed.
        map.super().join(t);
    }
}))

// start traversing a statement
visitor.statement(parseFirst(`select * from ta left join tb on ta.id=tb.id`));

// print result
console.log(`Used tables ${[...tables].join(', ')} with ${joins} joins !`)

You'll find that AST visitors (that's the name of this pattern) are quite flexible and powerful once you get used to them !

Here is the implementation of toSql which uses an astVisitor to reconstitude SQL from an AST (see below).

Converting an AST to SQL

That's super easy:

import { toSql } from 'questdb-ast-parser';

const sql: string = toSql.statement(myAst);

Like with astVisitor() or astModifier(), you can also convert subparts of AST to SQL (not necessarily a whole statement) by calling other methods of toSql.

Modifying SQL AST

There is a special kind of visitor, which I called astMapper, which allows you to traverse & modify ASTs on the fly.

For instance, you could rename a table in a request like this:

import { toSql, parseFirst, astMapper } from 'questdb-ast-parser';

// create a mapper
const mapper = astMapper(map => ({
    tableRef: t => {
        if (t.name === 'foo') {
            return {
                 // Dont do that... see below
                 // (I wrote this like that for the sake of explainability)
                ...t,
                name: 'bar',
            }
        }

        // call the default implementation of 'tableRef'
        // this will ensure that the subtree is also traversed.
        return map.super().tableRef(t);
    }
}))

// parse + map + reconvert to sql
const modified = mapper.statement(parseFirst('select * from foo'));

console.log(toSql.statement(modified!)); //  =>  SELECT * FROM "bar"

Good to know: If you use Typescript, return types will force you to return something compatible with a valid AST.

However, if you wish to remove a node from a tree, you can return null. For instance, this sample removes all references to column 'foo':

// create a mapper
const mapper = astMapper(map => ({
    ref: c => c.name === 'foo' ? null : c,
}))

// process sql
const result = mapper.statement(parseFirst('select foo, bar from test'));

// Prints: SELECT "bar" FROM "test"
console.log(toSql.statement(result!));

If no valid AST can be produced after having removed it, result will be null.

A note on astMapper performance:

The AST default modifier tries to be as efficient as possible: It does not copy AST parts as long as they do not have changed.

If you wan to avoid unnecessary copies, try to return the original argument as much as possible when nothing has changed.

For instance, instead of writing this:

    member(val: a.ExprMember) {
        const operand = someOperandTransformation(val.operand);
        if (!operand) {
            return null;
        }
        return {
            ...val,
            operand,
        }
    }

Prefer an implement that checks that nothing has changed, for instance by using the assignChanged() helper.

    member(val: a.ExprMember) {
        const operand = someOperandTransformation(val.operand);
        if (!operand) {
            return null;
        }
        return assignChanged(val, {
            operand,
        });
    }

It's pretty easy to implement. To deal with this kind optimization with arrays, there is a arrayNilMap() helper exposed:

const newArray = arrayNilMap(array, elem => transform(elem));
if (newArray === array) {
    // transform() has not changed any element in the array !
}

Parsing literal values

Postgres implements several literal syntaxes (string-to-something converters), whiches parsers are exposed as helper functions by this questdb-ast-parser:

  • parseArrayLiteral() parses arrays literals syntaxes (for instance {a,b,c})
  • parseGeometricLiteral() parses geometric types (for instance, things like (1,2) or <(1,2),3>)
  • parseIntervalLiteral() parses interval inputs literals (such as P1Y2DT1H or 1 yr 2 days 1 hr)

FAQ

  • How to parse named parameters like :name ? See here (TLDR)
  • Can I get detailed a location for each AST node ? Yes. Pass the option {locationTracking: true} to parse(), and use the locationOf(node) function.
  • Can I get the comments that the parser has ignored ? Yes. Use parseWithComments() instead of parse()

Development

To start hacking this lib, you'll have to:

... once done, tests should appear. HMR is on, which means that changes in your code are instantly propagated to unit tests. This allows for ultra fast development cycles (running tests takes less than 1 sec).

To debug tests: Just hit "run" (F5)... vscode should attach the mocha worker. Then run the test you want to debug.