Project

finox

0.0
The project is in a healthy, maintained state
Parses MySQL queries with the Rust sqlparser crate (via magnus) and returns the AST as plain Ruby Hashes and Arrays.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.9.128
 Project Readme

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 finox

Or without Bundler:

gem install finox

Precompiled 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: 1

Table 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 only t1 in #dml_tables.
  • #columns does not resolve table aliases (u.id stays u.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
# => true

Raw 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 + rubocop

License

MIT

Precompiled gems statically link Rust crates, including sqlparser-rs (Apache-2.0). See LICENSE-THIRD-PARTY.txt for their licenses and attributions.