polyglot_sql_ffi 0.1.0-aarch64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +81 -0
- data/Rakefile +14 -0
- data/ext/polyglot_sql_ffi/extconf.rb +69 -0
- data/lib/polyglot_sql/dialect.rb +30 -0
- data/lib/polyglot_sql/error.rb +62 -0
- data/lib/polyglot_sql/libpolyglot_sql_ffi.so +0 -0
- data/lib/polyglot_sql/native.rb +167 -0
- data/lib/polyglot_sql/railtie.rb +15 -0
- data/lib/polyglot_sql/validation_result.rb +29 -0
- data/lib/polyglot_sql/version.rb +8 -0
- data/lib/polyglot_sql.rb +386 -0
- data/lib/polyglot_sql_ffi.rb +4 -0
- data/polyglot_sql_ffi.gemspec +34 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 61e5ac76e197d3843bb8e8d44e6d588ca33b31e83b878dd3cd1a6f1ddd8b45a7
|
|
4
|
+
data.tar.gz: 4ebff0650298fd2f8d2c8cd2b73d94e1f1a2c426a6e3c73d3826421994c3ba95
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 55a9e3864f3cbaa59ed35ad9d0bb11c0db6749c5c811f5cb60e22d7cd2a8d7a44978c62617053cad1870ffd895dfd1844239c9c7de7e1d3a9a35efa00f742e48
|
|
7
|
+
data.tar.gz: a169c32f8af62575b3499bec91ec2d626c3d2cda55a792d64d1b5f9ad0342c87232e8ae03f952bd299a47d2a53a624774da6219ca3b11ffda0717e259f1d6185
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TobiLG <github@tobilg.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# polyglot_sql_ffi
|
|
2
|
+
|
|
3
|
+
Ruby wrapper for [polyglot](https://github.com/tobilg/polyglot) — a SQL parser, transpiler,
|
|
4
|
+
optimizer, and lineage engine written in Rust. Mirrors the polyglot-sql Python package's
|
|
5
|
+
module-level API via FFI.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "polyglot_sql_ffi"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The library itself is namespaced `PolyglotSql` and required as `polyglot_sql` (both happen
|
|
14
|
+
automatically under bundler).
|
|
15
|
+
|
|
16
|
+
Platform gems bundle the native library (linux/macos, x86_64/arm64). Installs that bypass platform
|
|
17
|
+
gems — JRuby, bundler's `force_ruby_platform`, a lockfile missing your platform — get the source gem
|
|
18
|
+
instead, which downloads the same official binary at install time. Platforms polyglot publishes no
|
|
19
|
+
binary for (e.g. Alpine/musl) must build `libpolyglot_sql_ffi` themselves and point
|
|
20
|
+
`POLYGLOT_SQL_FFI_PATH` at it.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require "polyglot_sql"
|
|
26
|
+
|
|
27
|
+
# Transpile between dialects
|
|
28
|
+
PolyglotSql.transpile("SELECT IFNULL(a, b) FROM t", read: :mysql, write: :postgres)
|
|
29
|
+
# => "SELECT COALESCE(a, b) FROM t"
|
|
30
|
+
|
|
31
|
+
# Parse to a Hash AST and generate back
|
|
32
|
+
ast = PolyglotSql.parse_one("SELECT 1 + 2", dialect: :sqlite)
|
|
33
|
+
PolyglotSql.generate(ast, dialect: :sqlite)
|
|
34
|
+
|
|
35
|
+
# Pretty-format
|
|
36
|
+
PolyglotSql.format("SELECT a,b FROM t WHERE x=1", dialect: :sqlite)
|
|
37
|
+
|
|
38
|
+
# Validate
|
|
39
|
+
PolyglotSql.validate("SELECT a FROM t", dialect: :sqlite).valid?
|
|
40
|
+
|
|
41
|
+
# Canonicalize / optimize
|
|
42
|
+
PolyglotSql.optimize("WITH cte AS (SELECT a FROM t) SELECT a FROM cte", dialect: :sqlite)
|
|
43
|
+
# => "SELECT a FROM t"
|
|
44
|
+
|
|
45
|
+
# Lineage
|
|
46
|
+
PolyglotSql.lineage("total", sql, dialect: :sqlite) # by column name
|
|
47
|
+
PolyglotSql.lineage_at(0, sql, dialect: :sqlite) # by output ordinal
|
|
48
|
+
PolyglotSql.output_columns(sql, dialect: :sqlite) # ordered output shape
|
|
49
|
+
PolyglotSql.source_tables("total", sql, dialect: :sqlite)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Set a default dialect once instead of passing it everywhere:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
PolyglotSql.configure { |c| c.default_dialect = :sqlite }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Errors raise subclasses of `PolyglotSql::Error` (`ParseError`, `TranspileError`,
|
|
59
|
+
`ColumnResolutionError`, ...).
|
|
60
|
+
|
|
61
|
+
## Native library
|
|
62
|
+
|
|
63
|
+
The gem loads `libpolyglot_sql_ffi` from `lib/polyglot_sql/`; set `POLYGLOT_SQL_FFI_PATH` to
|
|
64
|
+
override with your own build. The bundled version is pinned by `PolyglotSql::POLYGLOT_VERSION`.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
bundle install
|
|
70
|
+
rake native:fetch
|
|
71
|
+
bundle exec rspec
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Releasing
|
|
75
|
+
|
|
76
|
+
Tag `vX.Y.Z` and push. The release workflow packages platform gems plus the source gem and publishes
|
|
77
|
+
to RubyGems via trusted publishing.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core/rake_task"
|
|
4
|
+
|
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
6
|
+
|
|
7
|
+
namespace :native do
|
|
8
|
+
desc "Fetch the prebuilt polyglot-sql-ffi library for this platform"
|
|
9
|
+
task :fetch do
|
|
10
|
+
ruby "scripts/fetch_native.rb"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
task default: :spec
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# extconf.rb — invoked by `gem install` / `bundle install` to provide the
|
|
4
|
+
# polyglot-sql-ffi shared library, downloaded from the official polyglot
|
|
5
|
+
# GitHub release and checksum-verified.
|
|
6
|
+
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "open-uri"
|
|
9
|
+
require "digest"
|
|
10
|
+
|
|
11
|
+
RUST_REPO = "https://github.com/tobilg/polyglot"
|
|
12
|
+
VERSION = File.read(File.expand_path("../../lib/polyglot_sql/version.rb", __dir__))[/POLYGLOT_VERSION = "(.+?)"/, 1]
|
|
13
|
+
RUST_TAG = "v#{VERSION}"
|
|
14
|
+
EXT_DIR = __dir__
|
|
15
|
+
LIB_DIR = File.expand_path("../../lib/polyglot_sql", EXT_DIR)
|
|
16
|
+
|
|
17
|
+
# Release artifact name for this platform, nil if none is published.
|
|
18
|
+
def release_artifact
|
|
19
|
+
cpu = RbConfig::CONFIG["host_cpu"]
|
|
20
|
+
arch = { "x86_64" => "x86_64", "arm64" => "aarch64", "aarch64" => "aarch64" }[cpu]
|
|
21
|
+
return nil unless arch
|
|
22
|
+
|
|
23
|
+
case RbConfig::CONFIG["host_os"]
|
|
24
|
+
when /linux/ then "polyglot-sql-ffi-linux-#{arch}.tar.gz"
|
|
25
|
+
when /darwin/ then "polyglot-sql-ffi-macos-#{arch}.tar.gz"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def install_prebuilt
|
|
30
|
+
artifact = release_artifact
|
|
31
|
+
unless artifact
|
|
32
|
+
abort <<~MSG
|
|
33
|
+
ERROR: no prebuilt polyglot-sql-ffi binary for this platform
|
|
34
|
+
(#{RbConfig::CONFIG["host_os"]} / #{RbConfig::CONFIG["host_cpu"]}).
|
|
35
|
+
|
|
36
|
+
Build libpolyglot_sql_ffi from #{RUST_REPO} (tag #{RUST_TAG}) with
|
|
37
|
+
`cargo build -p polyglot-sql-ffi --profile ffi_release`, then set
|
|
38
|
+
POLYGLOT_SQL_FFI_PATH to the resulting library and reinstall.
|
|
39
|
+
MSG
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
base = "#{RUST_REPO}/releases/download/#{RUST_TAG}"
|
|
43
|
+
puts "Downloading #{base}/#{artifact}..."
|
|
44
|
+
tarball = URI.open("#{base}/#{artifact}").read
|
|
45
|
+
checksums = URI.open("#{base}/checksums.sha256").read
|
|
46
|
+
|
|
47
|
+
expected = checksums[/^(\h{64})\s+.*#{Regexp.escape(artifact)}$/, 1]
|
|
48
|
+
abort "ERROR: checksum mismatch for #{artifact}" unless Digest::SHA256.hexdigest(tarball) == expected
|
|
49
|
+
|
|
50
|
+
dir = File.join(EXT_DIR, "prebuilt")
|
|
51
|
+
FileUtils.mkdir_p(dir)
|
|
52
|
+
File.binwrite(File.join(dir, artifact), tarball)
|
|
53
|
+
system("tar", "xzf", artifact, chdir: dir) or abort "ERROR: tar extraction failed"
|
|
54
|
+
|
|
55
|
+
so_file = Dir[File.join(dir, "**", "libpolyglot_sql_ffi.{so,dylib}")].first
|
|
56
|
+
abort "ERROR: shared library not found in #{artifact}" unless so_file
|
|
57
|
+
|
|
58
|
+
FileUtils.mkdir_p(LIB_DIR)
|
|
59
|
+
FileUtils.cp(so_file, LIB_DIR, verbose: true)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if Dir[File.join(LIB_DIR, "libpolyglot_sql_ffi.{so,dylib}")].any?
|
|
63
|
+
puts "libpolyglot_sql_ffi already present in #{LIB_DIR}, skipping."
|
|
64
|
+
else
|
|
65
|
+
install_prebuilt
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Dummy Makefile — required by the rubygems extension protocol.
|
|
69
|
+
File.write(File.join(EXT_DIR, "Makefile"), "all:\ninstall:\nclean:\n")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PolyglotSql
|
|
4
|
+
# Normalizes dialect arguments (Symbol or String) into the string names the
|
|
5
|
+
# native library understands. The authoritative list comes from
|
|
6
|
+
# {PolyglotSql.dialects}; unknown names are rejected by the native layer.
|
|
7
|
+
module Dialect
|
|
8
|
+
DEFAULT = "generic"
|
|
9
|
+
|
|
10
|
+
# Convenience aliases accepted in addition to the canonical names.
|
|
11
|
+
ALIASES = {
|
|
12
|
+
"postgresql" => "postgres",
|
|
13
|
+
"pg" => "postgres",
|
|
14
|
+
"mssql" => "tsql",
|
|
15
|
+
"sqlserver" => "tsql",
|
|
16
|
+
"mariadb" => "mysql",
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# @param name [Symbol, String, nil]
|
|
20
|
+
# @return [String] canonical dialect name (default: "generic")
|
|
21
|
+
def self.resolve(name)
|
|
22
|
+
return DEFAULT if name.nil?
|
|
23
|
+
|
|
24
|
+
key = name.to_s.strip.downcase
|
|
25
|
+
return DEFAULT if key.empty?
|
|
26
|
+
|
|
27
|
+
ALIASES.fetch(key, key)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PolyglotSql
|
|
4
|
+
# Base class for every error raised by this gem. Mirrors the Python
|
|
5
|
+
# package's `PolyglotError` hierarchy.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised when the SQL cannot be tokenized or parsed.
|
|
9
|
+
class ParseError < Error; end
|
|
10
|
+
|
|
11
|
+
# Raised when SQL cannot be generated from an AST.
|
|
12
|
+
class GenerateError < Error; end
|
|
13
|
+
|
|
14
|
+
# Raised when transpilation between dialects fails.
|
|
15
|
+
class TranspileError < Error; end
|
|
16
|
+
|
|
17
|
+
# Raised when a lineage column cannot be resolved (not found,
|
|
18
|
+
# indeterminate, or ambiguous).
|
|
19
|
+
class ColumnResolutionError < TranspileError; end
|
|
20
|
+
|
|
21
|
+
# Raised when semantic/syntactic validation fails at the boundary
|
|
22
|
+
# (not the per-statement diagnostics returned by {PolyglotSql.validate}).
|
|
23
|
+
class ValidationError < Error; end
|
|
24
|
+
|
|
25
|
+
# Raised for invalid arguments: NULL/blank input, unknown dialect, bad UTF-8.
|
|
26
|
+
class ArgumentError < Error; end
|
|
27
|
+
|
|
28
|
+
# Raised when the native library cannot be found or loaded.
|
|
29
|
+
class LibraryNotFoundError < Error; end
|
|
30
|
+
|
|
31
|
+
# Status codes returned across the FFI boundary (see polyglot-sql-ffi types.rs).
|
|
32
|
+
module Status
|
|
33
|
+
SUCCESS = 0
|
|
34
|
+
PARSE_ERROR = 1
|
|
35
|
+
GENERATE_ERROR = 2
|
|
36
|
+
TRANSPILE_ERROR = 3
|
|
37
|
+
VALIDATION_ERROR = 4
|
|
38
|
+
INVALID_ARGUMENT = 5
|
|
39
|
+
SERIALIZATION_ERROR = 6
|
|
40
|
+
COLUMN_NOT_FOUND = 7
|
|
41
|
+
COLUMN_INDETERMINATE = 8
|
|
42
|
+
COLUMN_AMBIGUOUS = 9
|
|
43
|
+
INTERNAL_ERROR = 99
|
|
44
|
+
|
|
45
|
+
CLASS_FOR = {
|
|
46
|
+
PARSE_ERROR => ParseError,
|
|
47
|
+
GENERATE_ERROR => GenerateError,
|
|
48
|
+
TRANSPILE_ERROR => TranspileError,
|
|
49
|
+
VALIDATION_ERROR => ValidationError,
|
|
50
|
+
INVALID_ARGUMENT => ArgumentError,
|
|
51
|
+
SERIALIZATION_ERROR => Error,
|
|
52
|
+
COLUMN_NOT_FOUND => ColumnResolutionError,
|
|
53
|
+
COLUMN_INDETERMINATE => ColumnResolutionError,
|
|
54
|
+
COLUMN_AMBIGUOUS => ColumnResolutionError,
|
|
55
|
+
INTERNAL_ERROR => Error,
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
def self.error_class(status)
|
|
59
|
+
CLASS_FOR.fetch(status, Error)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module PolyglotSql
|
|
7
|
+
# Low-level FFI bindings to libpolyglot_sql_ffi.
|
|
8
|
+
#
|
|
9
|
+
# Not intended for direct use -- see the public API on {PolyglotSql}.
|
|
10
|
+
# Every FFI function returns a +PolyglotResult+ (or +PolyglotValidationResult+)
|
|
11
|
+
# struct by value; the safe wrappers here read the payload, free the struct's
|
|
12
|
+
# owned strings, and raise on a non-zero status so callers never touch raw
|
|
13
|
+
# pointers or manage memory.
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
module Native
|
|
17
|
+
extend FFI::Library
|
|
18
|
+
|
|
19
|
+
LIB_NAME = "polyglot_sql_ffi"
|
|
20
|
+
SOEXT = FFI::Platform::LIBSUFFIX # "so" on Linux, "dylib" on macOS
|
|
21
|
+
|
|
22
|
+
# Search order:
|
|
23
|
+
# 1. POLYGLOT_SQL_FFI_PATH (explicit override, matches the Go SDK)
|
|
24
|
+
# 2. lib/polyglot_sql/ inside the gem (where extconf.rb / fetch_native.rb put it)
|
|
25
|
+
# 3. system library path
|
|
26
|
+
def self.find_library
|
|
27
|
+
if (env_path = ENV["POLYGLOT_SQL_FFI_PATH"])
|
|
28
|
+
return env_path if File.exist?(env_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
bundled = File.expand_path("lib#{LIB_NAME}.#{SOEXT}", __dir__)
|
|
32
|
+
return bundled if File.exist?(bundled)
|
|
33
|
+
|
|
34
|
+
LIB_NAME
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
ffi_lib find_library
|
|
39
|
+
rescue LoadError => e
|
|
40
|
+
raise PolyglotSql::LibraryNotFoundError,
|
|
41
|
+
"Could not load libpolyglot_sql_ffi. #{e.message}\n\n" \
|
|
42
|
+
"Fetch the native library with:\n" \
|
|
43
|
+
" rake native:fetch\n\n" \
|
|
44
|
+
"Or set POLYGLOT_SQL_FFI_PATH to the full path of the .so/.dylib file."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ── Result structs (returned by value; see polyglot-sql-ffi types.rs) ──
|
|
48
|
+
|
|
49
|
+
class Result < FFI::Struct
|
|
50
|
+
layout :data, :pointer, :error, :pointer, :status, :int32
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class ValidationResult < FFI::Struct
|
|
54
|
+
layout :valid, :int32,
|
|
55
|
+
:errors_json, :pointer,
|
|
56
|
+
:error, :pointer,
|
|
57
|
+
:status, :int32
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ── C function declarations ────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
attach_function :polyglot_transpile, %i[string string string], Result.by_value
|
|
63
|
+
attach_function :polyglot_transpile_with_options, %i[string string string string], Result.by_value
|
|
64
|
+
attach_function :polyglot_parse, %i[string string], Result.by_value
|
|
65
|
+
attach_function :polyglot_parse_one, %i[string string], Result.by_value
|
|
66
|
+
attach_function :polyglot_parse_data_type, %i[string string], Result.by_value
|
|
67
|
+
attach_function :polyglot_tokenize, %i[string string], Result.by_value
|
|
68
|
+
attach_function :polyglot_generate, %i[string string], Result.by_value
|
|
69
|
+
attach_function :polyglot_generate_data_type, %i[string string], Result.by_value
|
|
70
|
+
attach_function :polyglot_format, %i[string string], Result.by_value
|
|
71
|
+
attach_function :polyglot_format_with_options, %i[string string string], Result.by_value
|
|
72
|
+
attach_function :polyglot_optimize, %i[string string], Result.by_value
|
|
73
|
+
attach_function :polyglot_lineage, %i[string string string], Result.by_value
|
|
74
|
+
attach_function :polyglot_lineage_with_schema, %i[string string string string], Result.by_value
|
|
75
|
+
attach_function :polyglot_lineage_at, %i[size_t string string], Result.by_value
|
|
76
|
+
attach_function :polyglot_lineage_at_with_schema, %i[size_t string string string], Result.by_value
|
|
77
|
+
attach_function :polyglot_output_columns, %i[string string], Result.by_value
|
|
78
|
+
attach_function :polyglot_output_columns_with_schema, %i[string string string], Result.by_value
|
|
79
|
+
attach_function :polyglot_source_tables, %i[string string string], Result.by_value
|
|
80
|
+
attach_function :polyglot_analyze_query, %i[string string], Result.by_value
|
|
81
|
+
attach_function :polyglot_openlineage_column_lineage, %i[string string], Result.by_value
|
|
82
|
+
attach_function :polyglot_openlineage_job_event, %i[string string], Result.by_value
|
|
83
|
+
attach_function :polyglot_openlineage_run_event, %i[string string], Result.by_value
|
|
84
|
+
attach_function :polyglot_diff, %i[string string string], Result.by_value
|
|
85
|
+
attach_function :polyglot_qualify_tables, %i[string string], Result.by_value
|
|
86
|
+
attach_function :polyglot_set_limit, %i[string uint64], Result.by_value
|
|
87
|
+
attach_function :polyglot_set_offset, %i[string uint64], Result.by_value
|
|
88
|
+
attach_function :polyglot_set_order_by, %i[string string], Result.by_value
|
|
89
|
+
attach_function :polyglot_rename_tables_with_options, %i[string string string], Result.by_value
|
|
90
|
+
attach_function :polyglot_annotate_types, %i[string string string], Result.by_value
|
|
91
|
+
|
|
92
|
+
attach_function :polyglot_validate, %i[string string], ValidationResult.by_value
|
|
93
|
+
attach_function :polyglot_validate_with_options, %i[string string string], ValidationResult.by_value
|
|
94
|
+
|
|
95
|
+
attach_function :polyglot_dialect_list, [], :pointer
|
|
96
|
+
attach_function :polyglot_dialect_count, [], :int32
|
|
97
|
+
# Returns a static pointer -- :string lets FFI read it without freeing.
|
|
98
|
+
attach_function :polyglot_version, [], :string
|
|
99
|
+
|
|
100
|
+
attach_function :polyglot_free_string, [:pointer], :void
|
|
101
|
+
attach_function :polyglot_free_result, [Result.by_value], :void
|
|
102
|
+
attach_function :polyglot_free_validation_result, [ValidationResult.by_value], :void
|
|
103
|
+
|
|
104
|
+
# ── Safe wrappers ──────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
# Read the payload from a Result struct, free its owned strings, and return
|
|
107
|
+
# the data. Raises the mapped error class on a non-zero status.
|
|
108
|
+
#
|
|
109
|
+
# @param result [Result]
|
|
110
|
+
# @return [String] the +data+ payload
|
|
111
|
+
def self.unwrap(result)
|
|
112
|
+
status = result[:status]
|
|
113
|
+
data = pointer_to_string(result[:data])
|
|
114
|
+
error = pointer_to_string(result[:error])
|
|
115
|
+
|
|
116
|
+
return data if status == Status::SUCCESS
|
|
117
|
+
|
|
118
|
+
raise Status.error_class(status),
|
|
119
|
+
error || "polyglot-sql failed with status #{status}"
|
|
120
|
+
ensure
|
|
121
|
+
polyglot_free_result(result)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Read a ValidationResult into a plain Hash and free its owned strings.
|
|
125
|
+
#
|
|
126
|
+
# @param result [ValidationResult]
|
|
127
|
+
# @return [Hash] { valid: Boolean, errors: Array<Hash> }
|
|
128
|
+
def self.unwrap_validation(result)
|
|
129
|
+
status = result[:status]
|
|
130
|
+
error = pointer_to_string(result[:error])
|
|
131
|
+
|
|
132
|
+
# status 4 (VALIDATION_ERROR) is not a failure here: it means the SQL is
|
|
133
|
+
# invalid, and errors_json carries the diagnostics.
|
|
134
|
+
if status != Status::SUCCESS && status != Status::VALIDATION_ERROR
|
|
135
|
+
raise Status.error_class(status),
|
|
136
|
+
error || "polyglot-sql validation failed with status #{status}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
errors_json = pointer_to_string(result[:errors_json])
|
|
140
|
+
{
|
|
141
|
+
valid: result[:valid] == 1,
|
|
142
|
+
errors: errors_json ? JSON.parse(errors_json, max_nesting: false) : [],
|
|
143
|
+
}
|
|
144
|
+
ensure
|
|
145
|
+
polyglot_free_validation_result(result)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Read, then free, a bare +char*+ returned by e.g. polyglot_dialect_list.
|
|
149
|
+
#
|
|
150
|
+
# @param ptr [FFI::Pointer]
|
|
151
|
+
# @return [String, nil]
|
|
152
|
+
def self.take_string(ptr)
|
|
153
|
+
return nil if ptr.null?
|
|
154
|
+
|
|
155
|
+
ptr.read_string.force_encoding("UTF-8")
|
|
156
|
+
ensure
|
|
157
|
+
polyglot_free_string(ptr) unless ptr.null?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def self.pointer_to_string(ptr)
|
|
161
|
+
return nil if ptr.null?
|
|
162
|
+
|
|
163
|
+
ptr.read_string.force_encoding("UTF-8")
|
|
164
|
+
end
|
|
165
|
+
private_class_method :pointer_to_string
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PolyglotSql
|
|
4
|
+
# Wires a `config.polyglot_sql.default_dialect` setting into Rails so the
|
|
5
|
+
# dialect can be omitted from calls.
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
config.polyglot_sql = ActiveSupport::OrderedOptions.new
|
|
8
|
+
|
|
9
|
+
initializer "polyglot_sql.configure" do |app|
|
|
10
|
+
PolyglotSql.configure do |c|
|
|
11
|
+
c.default_dialect = app.config.polyglot_sql.default_dialect
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PolyglotSql
|
|
4
|
+
# Result of {PolyglotSql.validate}. Mirrors the Python package's
|
|
5
|
+
# +ValidationResult+: a validity flag plus a list of diagnostic Hashes
|
|
6
|
+
# (each with +message+, +line+, +col+/+column+, +code+, +severity+).
|
|
7
|
+
class ValidationResult
|
|
8
|
+
# @return [Array<Hash>]
|
|
9
|
+
attr_reader :errors
|
|
10
|
+
|
|
11
|
+
def initialize(valid, errors)
|
|
12
|
+
@valid = valid
|
|
13
|
+
@errors = errors
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def valid?
|
|
18
|
+
@valid
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
{ valid: @valid, errors: @errors }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inspect
|
|
26
|
+
"#<PolyglotSql::ValidationResult valid=#{@valid} errors=#{@errors.size}>"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/polyglot_sql.rb
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "polyglot_sql/version"
|
|
4
|
+
require_relative "polyglot_sql/error"
|
|
5
|
+
require_relative "polyglot_sql/dialect"
|
|
6
|
+
require_relative "polyglot_sql/validation_result"
|
|
7
|
+
require_relative "polyglot_sql/native"
|
|
8
|
+
|
|
9
|
+
require_relative "polyglot_sql/railtie" if defined?(Rails::Railtie)
|
|
10
|
+
|
|
11
|
+
# PolyglotSql wraps the polyglot-sql-ffi native library, mirroring the
|
|
12
|
+
# polyglot-sql Python package: parse, transpile across 30+ dialects, generate,
|
|
13
|
+
# pretty-format, validate, optimize, lineage, and query analysis.
|
|
14
|
+
#
|
|
15
|
+
# The parsed AST is a plain Ruby Hash/Array (deserialized from the engine's
|
|
16
|
+
# JSON), and can be round-tripped back through {.generate}.
|
|
17
|
+
#
|
|
18
|
+
# @example Transpile between dialects
|
|
19
|
+
# PolyglotSql.transpile("SELECT `a` FROM `t`", read: :mysql, write: :sqlite)
|
|
20
|
+
# # => "SELECT \"a\" FROM \"t\""
|
|
21
|
+
#
|
|
22
|
+
# @example Pretty-format SQLite
|
|
23
|
+
# PolyglotSql.format("SELECT a,b FROM t WHERE x=1", dialect: :sqlite)
|
|
24
|
+
module PolyglotSql
|
|
25
|
+
class Configuration
|
|
26
|
+
# Default dialect used when none is specified.
|
|
27
|
+
# @return [Symbol, String, nil]
|
|
28
|
+
attr_accessor :default_dialect
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@default_dialect = nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# @return [Configuration]
|
|
37
|
+
def configuration
|
|
38
|
+
@configuration ||= Configuration.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def configure
|
|
42
|
+
yield(configuration)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ── Transpile ──────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
# Transpile SQL from one dialect to another.
|
|
48
|
+
#
|
|
49
|
+
# @param sql [String]
|
|
50
|
+
# @param read [Symbol, String, nil] source dialect (alias: +from+)
|
|
51
|
+
# @param write [Symbol, String, nil] target dialect (alias: +to+)
|
|
52
|
+
# @param pretty [Boolean] format the output with indentation and newlines
|
|
53
|
+
# @param from [Symbol, String, nil] alias for +read+
|
|
54
|
+
# @param to [Symbol, String, nil] alias for +write+
|
|
55
|
+
# @return [String, Array<String>] one String, or an Array for multi-statement input
|
|
56
|
+
def transpile(sql, read: nil, write: nil, pretty: false, from: nil, to: nil)
|
|
57
|
+
read_str = resolve_dialect(read || from)
|
|
58
|
+
write_str = resolve_dialect(write || to)
|
|
59
|
+
|
|
60
|
+
json =
|
|
61
|
+
if pretty
|
|
62
|
+
Native.unwrap(Native.polyglot_transpile_with_options(
|
|
63
|
+
sql, read_str, write_str, encode(pretty: true)
|
|
64
|
+
))
|
|
65
|
+
else
|
|
66
|
+
Native.unwrap(Native.polyglot_transpile(sql, read_str, write_str))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unwrap_list(decode(json))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ── Parse / tokenize ───────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
# Parse SQL into an Array of AST statements (each a Hash).
|
|
75
|
+
#
|
|
76
|
+
# @return [Array<Hash>]
|
|
77
|
+
def parse(sql, dialect: nil)
|
|
78
|
+
decode(Native.unwrap(Native.polyglot_parse(sql, resolve_dialect(dialect))))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Parse SQL expected to contain exactly one statement.
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash]
|
|
84
|
+
def parse_one(sql, dialect: nil)
|
|
85
|
+
decode(Native.unwrap(Native.polyglot_parse_one(sql, resolve_dialect(dialect))))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Parse a SQL type expression (e.g. "DECIMAL(10, 2)") into a DataType AST.
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash]
|
|
91
|
+
def parse_data_type(sql, dialect: nil)
|
|
92
|
+
decode(Native.unwrap(Native.polyglot_parse_data_type(sql, resolve_dialect(dialect))))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Tokenize SQL into an Array of tokens.
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<Hash>]
|
|
98
|
+
def tokenize(sql, dialect: nil)
|
|
99
|
+
decode(Native.unwrap(Native.polyglot_tokenize(sql, resolve_dialect(dialect))))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ── Generate ───────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
# Generate SQL from an AST (a Hash from {.parse_one} or an Array from {.parse}).
|
|
105
|
+
#
|
|
106
|
+
# @param ast [Hash, Array<Hash>]
|
|
107
|
+
# @param pretty [Boolean] format the output with indentation and newlines
|
|
108
|
+
# @return [String, Array<String>] one String, or an Array for multi-statement AST
|
|
109
|
+
def generate(ast, dialect: nil, pretty: false)
|
|
110
|
+
dialect_str = resolve_dialect(dialect)
|
|
111
|
+
ast_json = encode(statements(ast))
|
|
112
|
+
sql = unwrap_list(decode(Native.unwrap(Native.polyglot_generate(ast_json, dialect_str))))
|
|
113
|
+
return sql unless pretty
|
|
114
|
+
|
|
115
|
+
# The FFI has no pretty-generate; re-format the generated SQL instead.
|
|
116
|
+
pretty_each(sql, dialect_str)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generate a SQL type string from a DataType AST.
|
|
120
|
+
#
|
|
121
|
+
# @return [String]
|
|
122
|
+
def generate_data_type(data_type, dialect: nil)
|
|
123
|
+
Native.unwrap(Native.polyglot_generate_data_type(encode(data_type), resolve_dialect(dialect)))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ── Format (pretty-print) ──────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
# Pretty-format SQL, honoring the target dialect's quoting rules.
|
|
129
|
+
#
|
|
130
|
+
# @param max_input_bytes [Integer, nil] guard override
|
|
131
|
+
# @param max_tokens [Integer, nil] guard override
|
|
132
|
+
# @param max_ast_nodes [Integer, nil] guard override
|
|
133
|
+
# @param max_set_op_chain [Integer, nil] guard override
|
|
134
|
+
# @return [String, Array<String>]
|
|
135
|
+
def format(sql, dialect: nil, max_input_bytes: nil, max_tokens: nil,
|
|
136
|
+
max_ast_nodes: nil, max_set_op_chain: nil)
|
|
137
|
+
dialect_str = resolve_dialect(dialect)
|
|
138
|
+
options = compact(
|
|
139
|
+
maxInputBytes: max_input_bytes,
|
|
140
|
+
maxTokens: max_tokens,
|
|
141
|
+
maxAstNodes: max_ast_nodes,
|
|
142
|
+
maxSetOpChain: max_set_op_chain,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
json =
|
|
146
|
+
if options.empty?
|
|
147
|
+
Native.unwrap(Native.polyglot_format(sql, dialect_str))
|
|
148
|
+
else
|
|
149
|
+
Native.unwrap(Native.polyglot_format_with_options(sql, dialect_str, encode(options)))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
unwrap_list(decode(json))
|
|
153
|
+
end
|
|
154
|
+
alias_method :format_sql, :format
|
|
155
|
+
|
|
156
|
+
# ── Validate ───────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
# Validate SQL syntactically and (optionally) semantically.
|
|
159
|
+
#
|
|
160
|
+
# @param strict_syntax [Boolean] reject compatibility forms (trailing commas, ...)
|
|
161
|
+
# @param semantic [Boolean] add W001-W004 semantic warnings
|
|
162
|
+
# @return [ValidationResult]
|
|
163
|
+
def validate(sql, dialect: nil, strict_syntax: false, semantic: false)
|
|
164
|
+
dialect_str = resolve_dialect(dialect)
|
|
165
|
+
|
|
166
|
+
result =
|
|
167
|
+
if strict_syntax || semantic
|
|
168
|
+
Native.unwrap_validation(Native.polyglot_validate_with_options(
|
|
169
|
+
sql, dialect_str,
|
|
170
|
+
encode(strictSyntax: strict_syntax, semantic: semantic)
|
|
171
|
+
))
|
|
172
|
+
else
|
|
173
|
+
Native.unwrap_validation(Native.polyglot_validate(sql, dialect_str))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
ValidationResult.new(result[:valid], result[:errors])
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# ── Optimize ───────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
# Run the full optimizer pipeline over the SQL.
|
|
182
|
+
#
|
|
183
|
+
# @return [String, Array<String>]
|
|
184
|
+
def optimize(sql, dialect: nil)
|
|
185
|
+
unwrap_list(decode(Native.unwrap(Native.polyglot_optimize(sql, resolve_dialect(dialect)))))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# ── Lineage / analysis ─────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
# Column-level lineage for +column+ within +sql+.
|
|
191
|
+
#
|
|
192
|
+
# @param schema [Hash, nil] optional ValidationSchema for schema-aware lineage
|
|
193
|
+
# @return [Hash] a LineageNode
|
|
194
|
+
def lineage(column, sql, dialect: nil, schema: nil)
|
|
195
|
+
dialect_str = resolve_dialect(dialect)
|
|
196
|
+
json =
|
|
197
|
+
if schema
|
|
198
|
+
Native.unwrap(Native.polyglot_lineage_with_schema(column, sql, encode(schema), dialect_str))
|
|
199
|
+
else
|
|
200
|
+
Native.unwrap(Native.polyglot_lineage(column, sql, dialect_str))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
decode(json)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Column-level lineage for the zero-based output +ordinal+ within +sql+.
|
|
207
|
+
# Use instead of {lineage} when output names are duplicated or ambiguous.
|
|
208
|
+
#
|
|
209
|
+
# @param schema [Hash, nil] optional ValidationSchema for schema-aware lineage
|
|
210
|
+
# @return [Hash] a LineageNode
|
|
211
|
+
def lineage_at(ordinal, sql, dialect: nil, schema: nil)
|
|
212
|
+
dialect_str = resolve_dialect(dialect)
|
|
213
|
+
json =
|
|
214
|
+
if schema
|
|
215
|
+
Native.unwrap(Native.polyglot_lineage_at_with_schema(ordinal, sql, encode(schema), dialect_str))
|
|
216
|
+
else
|
|
217
|
+
Native.unwrap(Native.polyglot_lineage_at(ordinal, sql, dialect_str))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
decode(json)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Physical source tables feeding +column+.
|
|
224
|
+
#
|
|
225
|
+
# @return [Array<String>]
|
|
226
|
+
def source_tables(column, sql, dialect: nil)
|
|
227
|
+
decode(Native.unwrap(Native.polyglot_source_tables(column, sql, resolve_dialect(dialect))))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Ordered output columns of +sql+. With +schema+, wildcards are expanded
|
|
231
|
+
# into concrete columns.
|
|
232
|
+
#
|
|
233
|
+
# @param schema [Hash, nil] optional ValidationSchema
|
|
234
|
+
# @return [Hash] columns with name/ordinal plus an +ordinalComplete+ flag
|
|
235
|
+
def output_columns(sql, dialect: nil, schema: nil)
|
|
236
|
+
dialect_str = resolve_dialect(dialect)
|
|
237
|
+
json =
|
|
238
|
+
if schema
|
|
239
|
+
Native.unwrap(Native.polyglot_output_columns_with_schema(sql, encode(schema), dialect_str))
|
|
240
|
+
else
|
|
241
|
+
Native.unwrap(Native.polyglot_output_columns(sql, dialect_str))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
decode(json)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Structural analysis of a query (relations, base tables, projections, CTEs).
|
|
248
|
+
#
|
|
249
|
+
# @param options [Hash, nil] AnalyzeQueryOptions (may include +dialect+ and +schema+)
|
|
250
|
+
# @return [Hash] a QueryAnalysis
|
|
251
|
+
def analyze_query(sql, options: nil, dialect: nil)
|
|
252
|
+
opts = options ? options.dup : {}
|
|
253
|
+
opts[:dialect] ||= resolve_dialect(dialect) if dialect
|
|
254
|
+
decode(Native.unwrap(Native.polyglot_analyze_query(sql, encode(opts))))
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# ── OpenLineage ────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
# @return [Hash] OpenLineageColumnLineageResult
|
|
260
|
+
def openlineage_column_lineage(sql, options)
|
|
261
|
+
decode(Native.unwrap(Native.polyglot_openlineage_column_lineage(sql, encode(options))))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @return [Hash] OpenLineageEventResult
|
|
265
|
+
def openlineage_job_event(sql, options)
|
|
266
|
+
decode(Native.unwrap(Native.polyglot_openlineage_job_event(sql, encode(options))))
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# @return [Hash] OpenLineageEventResult
|
|
270
|
+
def openlineage_run_event(sql, options)
|
|
271
|
+
decode(Native.unwrap(Native.polyglot_openlineage_run_event(sql, encode(options))))
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# ── Diff ───────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
# Structural diff between two SQL statements.
|
|
277
|
+
#
|
|
278
|
+
# @return [Array<Hash>] diff edits
|
|
279
|
+
def diff(sql1, sql2, dialect: nil)
|
|
280
|
+
decode(Native.unwrap(Native.polyglot_diff(sql1, sql2, resolve_dialect(dialect))))
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# ── AST mutators ───────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
# @return [Array<Hash>] the modified AST
|
|
286
|
+
def set_limit(ast, limit)
|
|
287
|
+
decode(Native.unwrap(Native.polyglot_set_limit(encode(statements(ast)), limit)))
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# @return [Array<Hash>] the modified AST
|
|
291
|
+
def set_offset(ast, offset)
|
|
292
|
+
decode(Native.unwrap(Native.polyglot_set_offset(encode(statements(ast)), offset)))
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# @param order_by [Hash, Array<Hash>] ORDER BY expression AST
|
|
296
|
+
# @return [Array<Hash>] the modified AST
|
|
297
|
+
def set_order_by(ast, order_by)
|
|
298
|
+
decode(Native.unwrap(Native.polyglot_set_order_by(encode(statements(ast)), encode(statements(order_by)))))
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# @param options [Hash] qualify options (e.g. catalog/schema defaults)
|
|
302
|
+
# @return [Array<Hash>] the modified AST
|
|
303
|
+
def qualify_tables(ast, options: {})
|
|
304
|
+
decode(Native.unwrap(Native.polyglot_qualify_tables(encode(statements(ast)), encode(options))))
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# @param mapping [Hash] old-name => new-name
|
|
308
|
+
# @param options [Hash] rename options
|
|
309
|
+
# @return [Array<Hash>] the modified AST
|
|
310
|
+
def rename_tables(ast, mapping, options: {})
|
|
311
|
+
decode(Native.unwrap(Native.polyglot_rename_tables_with_options(
|
|
312
|
+
encode(statements(ast)), encode(mapping), encode(options)
|
|
313
|
+
)))
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Annotate an AST with inferred column types.
|
|
317
|
+
#
|
|
318
|
+
# @param schema [Hash, nil] optional ValidationSchema
|
|
319
|
+
# @return [Hash]
|
|
320
|
+
def annotate_types(sql, dialect: nil, schema: nil)
|
|
321
|
+
schema_json = schema ? encode(schema) : nil
|
|
322
|
+
decode(Native.unwrap(Native.polyglot_annotate_types(sql, resolve_dialect(dialect), schema_json)))
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# ── Introspection ──────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
# @return [Array<String>] the dialect names the engine supports
|
|
328
|
+
def dialects
|
|
329
|
+
decode(Native.take_string(Native.polyglot_dialect_list))
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# @return [Integer]
|
|
333
|
+
def dialect_count
|
|
334
|
+
Native.polyglot_dialect_count
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# @return [String] the underlying polyglot-sql engine version, e.g. "0.8.1"
|
|
338
|
+
def version
|
|
339
|
+
Native.polyglot_version
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
private
|
|
343
|
+
|
|
344
|
+
def resolve_dialect(name)
|
|
345
|
+
name = configuration.default_dialect if name.nil?
|
|
346
|
+
Dialect.resolve(name)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# The FFI expects a Vec<Expression> JSON array. A single statement from
|
|
350
|
+
# {.parse_one} is a Hash and must be wrapped without Array()'s Hash-to-pairs
|
|
351
|
+
# coercion; an Array from {.parse} passes through unchanged.
|
|
352
|
+
def statements(ast)
|
|
353
|
+
ast.is_a?(Array) ? ast : [ast]
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# A JSON array of strings collapses to a bare String for single-statement
|
|
357
|
+
# results (smart unwrap); multi-statement results stay an Array.
|
|
358
|
+
def unwrap_list(list)
|
|
359
|
+
return list unless list.is_a?(Array)
|
|
360
|
+
|
|
361
|
+
list.length == 1 ? list.first : list
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def pretty_each(sql, dialect_str)
|
|
365
|
+
if sql.is_a?(Array)
|
|
366
|
+
sql.map { |s| unwrap_list(decode(Native.unwrap(Native.polyglot_format(s, dialect_str)))) }
|
|
367
|
+
else
|
|
368
|
+
unwrap_list(decode(Native.unwrap(Native.polyglot_format(sql, dialect_str))))
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def compact(hash)
|
|
373
|
+
hash.reject { |_, v| v.nil? }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Real-world ASTs nest far deeper than Ruby's default max_nesting of 100,
|
|
377
|
+
# so both directions disable the limit.
|
|
378
|
+
def decode(json)
|
|
379
|
+
JSON.parse(json, max_nesting: false)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def encode(obj)
|
|
383
|
+
JSON.generate(obj, max_nesting: false)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/polyglot_sql/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "polyglot_sql_ffi"
|
|
7
|
+
spec.version = PolyglotSql::VERSION
|
|
8
|
+
spec.authors = ["Accountaim"]
|
|
9
|
+
spec.summary = "Ruby wrapper for polyglot-sql: a SQL parser, optimizer, and transpiler"
|
|
10
|
+
spec.description = <<~DESC
|
|
11
|
+
A Ruby gem that wraps the polyglot-sql-ffi native library, mirroring the
|
|
12
|
+
polyglot-sql Python package: SQL parsing, transpilation across 30+ dialects,
|
|
13
|
+
pretty-formatting, validation, optimization, lineage, and query analysis.
|
|
14
|
+
DESC
|
|
15
|
+
spec.homepage = "https://github.com/AccountAim/polyglot-sql-ruby"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
18
|
+
|
|
19
|
+
spec.files = Dir.chdir(__dir__) do
|
|
20
|
+
Dir["{lib,ext}/**/*", "Gemfile", "Rakefile", "polyglot_sql_ffi.gemspec", "README.md", "LICENSE"].reject do |file|
|
|
21
|
+
file.match?(%r{\Alib/polyglot_sql/libpolyglot_sql_ffi\.(so|dylib)\z})
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
spec.require_paths = ["lib"]
|
|
26
|
+
spec.extensions = ["ext/polyglot_sql_ffi/extconf.rb"]
|
|
27
|
+
|
|
28
|
+
spec.add_dependency "ffi", "~> 1.15"
|
|
29
|
+
|
|
30
|
+
spec.metadata = {
|
|
31
|
+
"source_code_uri" => "https://github.com/AccountAim/polyglot-sql-ruby",
|
|
32
|
+
"rubygems_mfa_required" => "true",
|
|
33
|
+
}
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: polyglot_sql_ffi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: aarch64-linux-gnu
|
|
6
|
+
authors:
|
|
7
|
+
- Accountaim
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: ffi
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.15'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.15'
|
|
27
|
+
description: |
|
|
28
|
+
A Ruby gem that wraps the polyglot-sql-ffi native library, mirroring the
|
|
29
|
+
polyglot-sql Python package: SQL parsing, transpilation across 30+ dialects,
|
|
30
|
+
pretty-formatting, validation, optimization, lineage, and query analysis.
|
|
31
|
+
email:
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- Gemfile
|
|
37
|
+
- LICENSE
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- ext/polyglot_sql_ffi/extconf.rb
|
|
41
|
+
- lib/polyglot_sql.rb
|
|
42
|
+
- lib/polyglot_sql/dialect.rb
|
|
43
|
+
- lib/polyglot_sql/error.rb
|
|
44
|
+
- lib/polyglot_sql/libpolyglot_sql_ffi.so
|
|
45
|
+
- lib/polyglot_sql/native.rb
|
|
46
|
+
- lib/polyglot_sql/railtie.rb
|
|
47
|
+
- lib/polyglot_sql/validation_result.rb
|
|
48
|
+
- lib/polyglot_sql/version.rb
|
|
49
|
+
- lib/polyglot_sql_ffi.rb
|
|
50
|
+
- polyglot_sql_ffi.gemspec
|
|
51
|
+
homepage: https://github.com/AccountAim/polyglot-sql-ruby
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
source_code_uri: https://github.com/AccountAim/polyglot-sql-ruby
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
post_install_message:
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: 3.2.0
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 3.5.22
|
|
73
|
+
signing_key:
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: 'Ruby wrapper for polyglot-sql: a SQL parser, optimizer, and transpiler'
|
|
76
|
+
test_files: []
|