Sqlglot
A Ruby gem that wraps sql-glot-rust via FFI, providing:
- Parse SQL into a structured AST (Ruby Hash)
- Transpile SQL between 30 dialects (Postgres, MySQL, BigQuery, Snowflake, T-SQL, ...)
- Generate SQL from an AST
- Extract metadata from queries -- tables, columns, aliases, subqueries, CTEs, values, and more (inspired by Python's sql-metadata)
Requirements
- Ruby >= 3.0
- Rust toolchain (cargo 1.85+) -- only needed when building from source
- git -- only needed when building from source
Installation
Add to your Gemfile:
gem "sqlglot"Then run bundle install. Precompiled native gems are published to
rubygems.org for these platforms:
| Platform | Architecture |
|---|---|
| Linux (glibc) | x86_64, aarch64 |
| macOS | x86_64 (Intel), arm64 (Apple Silicon) |
On these platforms, no Rust toolchain is needed -- Bundler automatically picks the right precompiled gem.
Building from source
On other platforms (e.g. Alpine/musl), the Rust library is compiled
automatically during gem install via extconf.rb. This requires the Rust
toolchain (cargo 1.85+) and git.
The github: and path: Gemfile options always build from source, since
Bundler bypasses RubyGems platform selection for these sources:
gem "sqlglot", github: "AccountAim/sql-glot-ruby" # requires Rust
gem "sqlglot", path: "/path/to/this/repo" # requires RustFor local development, you can also build manually:
bundle install
rake cargo:buildrake cargo:build clones sql-glot-rust at a pinned tag, runs cargo build --release, and copies the resulting .so/.dylib into lib/sqlglot/. This only needs to be done once (or when upgrading the Rust library version).
If the .so already exists, extconf.rb skips the Rust build automatically.
Usage
Transpile between dialects
Sqlglot.transpile("SELECT NOW()", from: :postgres, to: :bigquery)
# => "SELECT CURRENT_TIMESTAMP()"
Sqlglot.transpile("SELECT * FROM t LIMIT 10", from: :mysql, to: :tsql)
# => "SELECT TOP 10 * FROM t"
Sqlglot.transpile("SELECT NVL(a, b) FROM t", from: :oracle, to: :tsql)
# => "SELECT ISNULL(a, b) FROM t"Dialect arguments accept symbols, strings, or Sqlglot::Dialect constants:
Sqlglot.transpile(sql, from: :postgres, to: :bigquery)
Sqlglot.transpile(sql, from: "postgres", to: "bigquery")
Sqlglot.transpile(sql, from: Sqlglot::Dialect::POSTGRES, to: Sqlglot::Dialect::BIGQUERY)Parse SQL to an AST
ast = Sqlglot.parse("SELECT name, age FROM users WHERE active = true", dialect: :postgres)
# => {"Select" => {"columns" => [...], "from" => {...}, "where_clause" => {...}, ...}}The return value is a plain Ruby Hash deserialized from the Rust library's JSON AST.
Generate SQL from an AST
ast = Sqlglot.parse("SELECT name FROM users WHERE id = 1")
# Roundtrip back to SQL
Sqlglot.generate(ast)
# => "SELECT name FROM users WHERE id = 1"
# Generate for a different dialect
Sqlglot.generate(ast, dialect: :tsql)Modify an AST and regenerate
ast = Sqlglot.parse("SELECT name FROM users WHERE id = 1")
# Change the WHERE value
ast["Select"]["where_clause"]["BinaryOp"]["right"] = { "Number" => "42" }
Sqlglot.generate(ast)
# => "SELECT name FROM users WHERE id = 42"Library version
Sqlglot.version
# => "0.10.0"Query Metadata Extraction
Sqlglot::Query parses SQL once, then provides lazy-evaluated properties for extracting tables, columns, aliases, and more.
q = Sqlglot::Query.new(<<~SQL, dialect: :postgres)
SELECT u.name, COUNT(o.id) AS order_count
FROM users AS u
JOIN orders AS o ON u.id = o.user_id
WHERE u.active = true
ORDER BY order_count DESC
LIMIT 10 OFFSET 20
SQLQuery type
q.query_type # => :select
# Also: :insert, :update, :delete, :create_table, :merge, etc.Tables
q.tables # => ["users", "orders"]
q.tables_aliases # => {"u" => "users", "o" => "orders"}CTE names are automatically excluded from the tables list:
q = Sqlglot::Query.new("WITH cte AS (SELECT id FROM real_table) SELECT * FROM cte")
q.tables # => ["real_table"]Columns
q.columns
# => ["users.name", "orders.id", "users.id", "orders.user_id", "users.active"]
# Table aliases are resolved: u.name becomes users.nameColumns by clause
q.columns_dict
# => {
# select: ["users.name", "orders.id"],
# join: ["users.id", "orders.user_id"],
# where: ["users.active"],
# order_by: ["orders.id"] # resolved from the "order_count" alias
# }Output columns
What the SELECT would produce (uses aliases where present):
q.output_columns # => ["name", "order_count"]Column aliases
q = Sqlglot::Query.new("SELECT a, b + c AS total FROM t ORDER BY total")
q.columns_aliases # => {"total" => ["b", "c"]}
q.columns_aliases_names # => ["total"]
q.columns_aliases_dict # => {select: ["total"], order_by: ["total"]}CTEs
q = Sqlglot::Query.new(<<~SQL)
WITH active AS (SELECT * FROM users WHERE active = true)
SELECT * FROM active
SQL
q.with_names # => ["active"]
q.with_queries # => {"active" => "SELECT * FROM users WHERE active = true"}Subqueries
q = Sqlglot::Query.new(<<~SQL)
SELECT * FROM (SELECT id FROM users) AS sub
JOIN orders ON sub.id = orders.user_id
SQL
q.subqueries # => {"sub" => "SELECT id FROM users"}
q.subqueries_names # => ["sub"]LIMIT and OFFSET
q.limit_and_offset # => [10, 20]
# Returns nil when no LIMIT is present
Sqlglot::Query.new("SELECT 1").limit_and_offset # => nilINSERT values
q = Sqlglot::Query.new("INSERT INTO users (name, age) VALUES ('Alice', 30)")
q.values # => ["Alice", 30]
q.values_dict # => {"name" => "Alice", "age" => 30}
# Auto-generates column names when not specified
q = Sqlglot::Query.new("INSERT INTO t VALUES (1, 'hello')")
q.values_dict # => {"column_1" => 1, "column_2" => "hello"}Query generalization
Replaces literals with placeholders for query fingerprinting:
q = Sqlglot::Query.new("SELECT * FROM t WHERE id = 42 AND name = 'Alice'")
q.generalize
# => "SELECT * FROM t WHERE id = N AND name = 'X'"Raw AST access
q.ast # => the full parsed Hash (same as Sqlglot.parse)Rails Integration
The gem includes a Railtie that loads automatically when Rails is present.
# config/application.rb (or an initializer)
config.sqlglot.default_dialect = :sqliteThen dialect can be omitted from calls:
Sqlglot.parse("SELECT 1") # uses :sqlite
Sqlglot.transpile(sql, to: :bigquery) # from: defaults to :sqliteWithout Rails, use the configure block:
Sqlglot.configure do |c|
c.default_dialect = :sqlite
endSupported Dialects
Official
| Dialect | Constant |
|---|---|
| ANSI SQL | Sqlglot::Dialect::ANSI |
| Athena | Sqlglot::Dialect::ATHENA |
| BigQuery | Sqlglot::Dialect::BIGQUERY |
| ClickHouse | Sqlglot::Dialect::CLICKHOUSE |
| Databricks | Sqlglot::Dialect::DATABRICKS |
| DuckDB | Sqlglot::Dialect::DUCKDB |
| Hive | Sqlglot::Dialect::HIVE |
| MySQL | Sqlglot::Dialect::MYSQL |
| Oracle | Sqlglot::Dialect::ORACLE |
| PostgreSQL | Sqlglot::Dialect::POSTGRES |
| Presto | Sqlglot::Dialect::PRESTO |
| Redshift | Sqlglot::Dialect::REDSHIFT |
| Snowflake | Sqlglot::Dialect::SNOWFLAKE |
| Spark | Sqlglot::Dialect::SPARK |
| SQLite | Sqlglot::Dialect::SQLITE |
| StarRocks | Sqlglot::Dialect::STARROCKS |
| Trino | Sqlglot::Dialect::TRINO |
| T-SQL | Sqlglot::Dialect::TSQL |
Community
| Dialect | Constant |
|---|---|
| Doris | Sqlglot::Dialect::DORIS |
| Dremio | Sqlglot::Dialect::DREMIO |
| Drill | Sqlglot::Dialect::DRILL |
| Druid | Sqlglot::Dialect::DRUID |
| Exasol | Sqlglot::Dialect::EXASOL |
| Fabric | Sqlglot::Dialect::FABRIC |
| Materialize | Sqlglot::Dialect::MATERIALIZE |
| PRQL | Sqlglot::Dialect::PRQL |
| RisingWave | Sqlglot::Dialect::RISINGWAVE |
| SingleStore | Sqlglot::Dialect::SINGLESTORE |
| Tableau | Sqlglot::Dialect::TABLEAU |
| Teradata | Sqlglot::Dialect::TERADATA |
Aliases are also accepted: :postgresql, :mssql, :sqlserver, :mariadb.
Configuration
| Setting | Description |
|---|---|
Sqlglot.configure { |c| c.default_dialect = :sqlite } |
Default dialect when none is specified |
ENV["SQLGLOT_LIB_PATH"] |
Override the path to libsqlglot_rust.so / .dylib
|
Development
git clone <this-repo>
cd sqlglot
bundle install
rake cargo:build # build the Rust shared library
bundle exec rspec # run tests (49 examples)License
MIT