@elastic/esql
v4.31.0
Published
A set of ts tools to parse, build and transform ES|QL queries programmatically.
Keywords
Readme
ES|QL and PromQL parser, AST builder, traversal, mutation, and pretty-printing for JavaScript and TypeScript. Used in Kibana and other Elastic tooling.
Installation
npm install @elastic/esqlyarn add @elastic/esqlpnpm add @elastic/esqlSub-module reference
| Module | Description |
|--------|-------------|
| src/parser/ | Parser class — parse query strings to AST |
| src/composer/ | esql tagged-template query builder |
| src/composer/synth/ | synth — synthesize AST nodes from template strings; PromQL node builders |
| src/ast/builder/ | Builder — low-level AST node construction |
| src/ast/walker/ | Walker — simple full-tree traversal |
| src/ast/visitor/ | Visitor — typed, controlled traversal with input/output passing |
| src/ast/mutate/ | mutate — named helpers for structural AST edits |
| src/pretty_print/ | BasicPrettyPrinter, WrappingPrettyPrinter |
Creating an ES|QL AST
Parsing a query
The Parser converts a query string into an AST synchronously.
import { Parser } from '@elastic/esql';
const { root, errors } = Parser.parse('FROM index | WHERE col0 > 100');Pass { withFormatting: true } to attach comments and whitespace to AST nodes (needed for round-trip formatting):
const { root } = Parser.parse('FROM /* comment */ index', { withFormatting: true });Parse a single command or expression without a full query string:
const { root: expr } = Parser.parseExpression('count(*) + 1');
const { root: cmd } = Parser.parseCommand('WHERE col > 10');Composing a query
The esql composer is a tagged-template API for building queries with safe parameter injection:
import { esql } from '@elastic/esql';
const start = '2024-01-01';
const limit = 100;
const query = esql`
FROM index
| WHERE @timestamp >= ${{ start }}
| SORT @timestamp DESC
| KEEP service.name, log.level`;
query.pipe`LIMIT ${{ limit }}`;
// Get the Elasticsearch request payload:
query.toRequest();
// { query: 'FROM index | WHERE @timestamp >= ?start | ...', params: [{ start: '2024-01-01' }, ...] }Use .query to append multiple piped commands at once:
query.query`WHERE status == ${{ status }} | STATS count = COUNT(*) | LIMIT ${{ limit }}`;See the synth API for synthesizing individual AST nodes from template strings.
Building AST nodes manually
The Builder is a low-level API for constructing AST nodes programmatically:
import { Builder } from '@elastic/esql';
const node = Builder.command({
name: 'limit',
args: [Builder.expression.literal.integer(10)],
});Traversing an ES|QL AST
Walker
The Walker visits every node in the AST, calling your callbacks for each node type:
import { Walker } from '@elastic/esql';
import type { ESQLCommand, ESQLFunction } from '@elastic/esql-types';
const walker = new Walker({
visitCommand: (node: ESQLCommand) => {
console.log('command:', node.name);
},
visitFunction: (fn: ESQLFunction) => {
console.log('function:', fn.name);
},
});
walker.walk(root);Static helpers like Walker.findAll(), Walker.params(), and Walker.replace() cover the most common search-and-modify patterns without needing to instantiate a walker.
The walker also traverses embedded PromQL expressions — see PromQL support in the walker docs.
Visitor
The Visitor provides a more powerful traversal model where you control which children to visit, and callbacks can pass typed values up and down the tree:
import { Visitor } from '@elastic/esql';
const columnNames = new Visitor()
.on('visitColumnExpression', (ctx) => ctx.node.name)
.on('visitExpression', () => null)
.on('visitCommand', (ctx) => [...ctx.visitArguments()])
.on('visitQuery', (ctx) => [...ctx.visitCommands()])
.visitQuery(root);Modifying an ES|QL AST
The mutate API provides named methods for common structural edits:
import { Parser, mutate, BasicPrettyPrinter } from '@elastic/esql';
const { root } = Parser.parse('FROM index METADATA _lang');
console.log([...mutate.commands.from.metadata.list(root)]);
// [ '_lang' ]
mutate.commands.from.metadata.upsert(root, '_id');
console.log([...mutate.commands.from.metadata.list(root)]);
// [ '_lang', '_id' ]
console.log(BasicPrettyPrinter.print(root));
// FROM index METADATA _lang, _idPretty-printing
The pretty_print module formats an AST back to a query string.
BasicPrettyPrinter — single-line or pipe-separated multiline output:
import { Parser, BasicPrettyPrinter } from '@elastic/esql';
const { root } = Parser.parse('FROM index | WHERE x > 100 | LIMIT 100');
BasicPrettyPrinter.print(root);
// FROM index | WHERE x > 100 | LIMIT 100
BasicPrettyPrinter.multiline(root, { pipeTab: ' ' });
// FROM index
// | WHERE x > 100
// | LIMIT 100WrappingPrettyPrinter — width-aware, preserves comments (requires { withFormatting: true } on parse):
import { Parser, WrappingPrettyPrinter } from '@elastic/esql';
const { root } = Parser.parse(
'FROM index /* source */ | WHERE x > 100 | LIMIT 100',
{ withFormatting: true },
);
console.log(WrappingPrettyPrinter.print(root, { multiline: true }));
// FROM index /* source */
// | WHERE x > 100
// | LIMIT 100PromQL support
ES|QL embeds PromQL via the PROMQL source command. Use esql.promql() in the composer:
import { esql } from '@elastic/esql';
esql.promql(esql.pql`sum(rate(metric[5m])) by (job)`)
.where`value > 0`
.limit(100)
.print();
// PROMQL (sum(rate(metric[5m])) by (job)) | WHERE value > 0 | LIMIT 100See the synth README for pql, pqlSel, pqlFunc, and other PromQL node builders.
Licence
Licensed under Elastic License 2.0.
