Finox
A MySQL query parser for Ruby, powered by sqlparser-rs.
Finox parses SQL with Rust and exposes tables, columns, fingerprints and the raw AST.
Installation
bundle add finoxOr without Bundler:
gem install finoxPrecompiled native gems are available for Ruby 3.1–4.0 on the following platforms, so installation there requires no Rust toolchain:
| OS | Platforms |
|---|---|
| Linux | x86_64, aarch64 (glibc and musl variants) |
| macOS | x86_64, arm64 |
| Windows | x64-mingw-ucrt |
On any other platform the source gem is installed instead, and compiling it requires a Rust toolchain.
Usage
Finox.parse returns a Finox::Result:
require "finox"
result = Finox.parse("SELECT `name` FROM `users` WHERE id = 1")
# => #<Finox::Result>
result.tables # => ["users"]
result.columns # => ["name", "id"]
result.statement_types # => ["Query"]
result.normalize # => "SELECT `name` FROM `users` WHERE id = ?"
result.fingerprint # => "24e307ca0f02abfc"Invalid SQL raises Finox::ParseError:
Finox.parse("SELEKT 1")
# => Finox::ParseError: sql parser error: Expected: an SQL statement, found: SELEKT at Line: 1, Column: 1Table classification
#select_tables, #dml_tables and #ddl_tables classify the referenced
tables by how they are used: read, written by DML (INSERT / UPDATE /
DELETE) or targeted by DDL (CREATE TABLE / ALTER TABLE / DROP TABLE /
TRUNCATE etc.). A table appearing in multiple roles is listed in each.
result = Finox.parse("INSERT INTO logs SELECT message FROM events; DROP TABLE archives")
result.tables # => ["logs", "events", "archives"]
result.select_tables # => ["events"]
result.dml_tables # => ["logs"]
result.ddl_tables # => ["archives"]Known limitations
Extraction is syntactic, with no schema knowledge and only coarse scope resolution:
- A CTE name shadows a same-named real table everywhere in the statement, even inside the CTE's own definition.
- Multi-table
UPDATE(UPDATE t1 JOIN t2 ... SET t2.x = 1) reports onlyt1in#dml_tables. -
#columnsdoes not resolve table aliases (u.idstaysu.id).
Normalization and fingerprints
#normalize replaces literals with ? placeholders and deparses from the
AST, so formatting and keyword case are normalized as well. #fingerprint is
a 64-bit hex hash (xxhash64) of the normalized SQL — stable across literal
and formatting differences, but not guaranteed to be stable across finox
versions, since the normalized form depends on the bundled sqlparser-rs.
Recompute stored fingerprints when upgrading finox.
Finox.parse("select * from users where id=1").normalize
# => "SELECT * FROM users WHERE id = ?"
Finox.parse("select * from users where id=1").fingerprint ==
Finox.parse("SELECT * FROM users WHERE id = 42").fingerprint
# => trueRaw sqlparser-rs AST
For anything not covered above, #statements exposes sqlparser-rs's raw AST of each parsed statement as plain
Ruby Hashes and Arrays:
ast = Finox.parse("SELECT `name` FROM `users` WHERE id = 1").statements.first
# => {"Query" => {"with" => nil, "body" => {"Select" => {...}}, ...}}
ast.dig("Query", "body", "Select", "projection")
# => [{"UnnamedExpr" =>
# {"Identifier" =>
# {"value" => "name",
# "quote_style" => "`",
# "span" => {"start" => {"line" => 1, "column" => 8},
# "end" => {"line" => 1, "column" => 14}}}}}]Development
bundle install
bundle exec rake compile # build the Rust extension
bundle exec rake # compile + spec + rubocopLicense
Precompiled gems statically link Rust crates, including sqlparser-rs (Apache-2.0). See LICENSE-THIRD-PARTY.txt for their licenses and attributions.