NAME
SQL::AST::Simple - Parse SQL into a plain Perl data structure and back again
VERSION
version 0.01
SYNOPSIS
use SQL::AST::Simple qw( parse unparse parse_expr unparse_expr );
my $ast = parse('SELECT a, b FROM t WHERE a > 1', dialect => 'postgresql');
# $ast is an array reference of statements, each a tree of plain
# hashes, arrays and scalars. Poke at it however you like:
$ast->[0]{Query}{body}{Select}{from}[0]{relation}{Table}{name}[0]{Identifier}{value} = 'u';
say unparse($ast); # SELECT a, b FROM u WHERE a > 1
# Expressions can be handled on their own, without a statement around them:
$ast->[0]{Query}{body}{Select}{selection} = parse_expr('a > 1 AND b = 2');
say unparse_expr($ast->[0]{Query}{body}{Select}{selection}); # a > 1 AND b = 2
DESCRIPTION
This module provides Perl bindings for the Rust sqlparser crate. It exposes two operations: turning SQL text into the parser's abstract syntax tree as an ordinary Perl data structure, and turning such a data structure back into SQL text. Each comes in a form for whole statements and a form for a lone expression. There is no object layer; the tree is what the crate's serde serialization produces, decoded from JSON. That keeps the module small and makes every node the crate knows about available without any wrapping, at the cost of a somewhat verbose structure.
Nothing is exported by default.
FUNCTIONS
parse
my $ast = parse($sql);
my $ast = parse($sql, dialect => $name);
Parses $sql, which may contain several semicolon separated statements, and returns an array reference with one element per statement. Throws an exception with the parser's message, including line and column, if the text cannot be parsed.
Options:
- dialect
-
Which SQL dialect to parse with. Defaults to
generic, which is the most permissive. Recognized names (case insensitive) aregeneric,ansi,postgresql(orpostgres),mysql,sqlite,mssql,oracle,snowflake,bigquery,redshift,clickhouse,duckdb,databricks,hive,spark(orsparksql) andteradata.
parse_expr
my $expr = parse_expr($sql);
my $expr = parse_expr($sql, dialect => $name);
Parses $sql as a single expression, such as the condition of a WHERE clause, and returns it as a hash reference. The whole of $sql must be consumed by the expression; a leading WHERE keyword or anything left over after the expression is an error. Takes the same dialect option as "parse".
The result is exactly what appears inside a statement wherever the crate expects an expression, so it can be spliced into a tree from "parse", for instance as the selection of a SELECT.
unparse
my $sql = unparse($ast);
my $sql = unparse($ast, pretty => 1);
Takes an array reference of statements as returned by "parse", or a single statement hash reference, and returns the SQL text. Multiple statements are joined with "; ". Throws an exception if the structure does not deserialize into a valid AST.
Options:
- pretty
-
If true, statements are formatted with indentation and newlines rather than on a single line, and are joined with
";\n".
unparse_expr
my $sql = unparse_expr($expr);
Takes an expression hash reference, as returned by "parse_expr" or lifted out of a statement, and returns the SQL text. Throws an exception if the structure does not deserialize into a valid expression. There is no pretty option; expressions are always rendered on one line.
THE DATA STRUCTURE
The tree mirrors the Rust types of the sqlparser crate one to one, as serialized by serde. A few rules of thumb cover most of it:
Rust enums are "externally tagged": a hash with a single key naming the variant, whose value is the payload. A
SELECTstatement is{ Query => {...} }, a column reference in an expression is{ Identifier => {...} }, a literal is{ Value => {...} }. Variants without payload are plain strings.Rust structs are hashes keyed by field name;
Optionfields that are absent areundef;Vecfields are array references.Booleans come back as JSON boolean objects. When you set a boolean field yourself use
\1or\0(or thetrue/falseconstants from your JSON module). A plain Perl1would be encoded as a number and rejected by "unparse".Numeric literals are kept as strings, exactly as they appeared in the source, so that precision is never lost. Any field that holds a string must be given a Perl string; if you have computed a number, stringify it first.
Most nodes carry a
spanhash recording where they appeared in the source. "unparse" ignores the contents but requires the field to be present, so the easiest way to build a new node is to parse a small snippet (with "parse_expr" for an expression) and lift the piece you need out of the result, rather than constructing hashes by hand.
The easiest way to learn the shape for a given construct is to parse an example and dump it. The exact shape depends on the version of the crate the bindings are built against, which is pinned in the distribution's ffi/Cargo.toml; a release that bumps it may change the structure and will say so in the change log.
CAVEATS
The parser is syntactic only and deliberately permissive. It will accept some SQL that a given database would reject, and occasionally reject vendor syntax it does not yet know. Round tripping is not byte for byte: comments are dropped, keywords are upper cased, and whitespace is normalized.
Building this distribution requires a Rust toolchain (cargo) at install time.
SEE ALSO
- https://crates.io/crates/sqlparser
-
The parser this module wraps.
AUTHOR
Graham Ollis <plicease@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Graham Ollis.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.