sqlglot 0.1.0
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/Rakefile +32 -0
- data/ext/sqlglot_rust/extconf.rb +85 -0
- data/lib/sqlglot/ast_walker.rb +121 -0
- data/lib/sqlglot/dialect.rb +79 -0
- data/lib/sqlglot/error.rb +18 -0
- data/lib/sqlglot/libsqlglot_rust.so +0 -0
- data/lib/sqlglot/native.rb +138 -0
- data/lib/sqlglot/query.rb +785 -0
- data/lib/sqlglot/railtie.rb +20 -0
- data/lib/sqlglot/version.rb +5 -0
- data/lib/sqlglot.rb +110 -0
- data/sqlglot.gemspec +32 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7e7068cefebcdddf4c1e83780bdebe453572b343fef8d53f32c7865ca2b4c0f4
|
|
4
|
+
data.tar.gz: 60bb54a9bb6d0a4b1ea48c95fc6f667567f70961d54e6df569cd977f357de885
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 61842a1180e105b41277d082f25d9c4d38c5f8ea300677c1e91647dbf55985d204606c8c40ac94d95c746efacfab0358502e39e87d117a5ce3538988904b6f52
|
|
7
|
+
data.tar.gz: d8a038f5683d79dd9809ad9af05224007fd74552b5ae060945b4b6bb2a4d77e6d5fc140b58ee3f4ddcb2ccd374a11fc6ff7b6208984337cf5177efaff2ccf7eb
|
data/Gemfile
ADDED
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core/rake_task"
|
|
4
|
+
|
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
6
|
+
|
|
7
|
+
namespace :cargo do
|
|
8
|
+
desc "Build the sql-glot-rust shared library"
|
|
9
|
+
task :build do
|
|
10
|
+
ext_dir = File.expand_path("ext/sqlglot_rust", __dir__)
|
|
11
|
+
rust_dir = File.join(ext_dir, "sql-glot-rust")
|
|
12
|
+
|
|
13
|
+
unless Dir.exist?(rust_dir)
|
|
14
|
+
sh "git clone --depth 1 --branch v0.10.0 " \
|
|
15
|
+
"https://github.com/protegrity/sql-glot-rust.git #{rust_dir}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sh "cargo build --release --manifest-path #{File.join(rust_dir, 'Cargo.toml')}"
|
|
19
|
+
|
|
20
|
+
lib_dir = File.expand_path("lib/sqlglot", __dir__)
|
|
21
|
+
release_dir = File.join(rust_dir, "target", "release")
|
|
22
|
+
|
|
23
|
+
so_file = Dir[File.join(release_dir, "libsqlglot_rust.{so,dylib,dll}")].first
|
|
24
|
+
if so_file
|
|
25
|
+
cp so_file, lib_dir, verbose: true
|
|
26
|
+
else
|
|
27
|
+
abort "ERROR: shared library not found in #{release_dir}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
task default: :spec
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# extconf.rb — invoked by `gem install` or `bundle install` to build the
|
|
4
|
+
# sql-glot-rust shared library from source.
|
|
5
|
+
#
|
|
6
|
+
# Requirements:
|
|
7
|
+
# - git (to clone the repository)
|
|
8
|
+
# - cargo / rustc (Rust toolchain, 1.85+)
|
|
9
|
+
|
|
10
|
+
require "fileutils"
|
|
11
|
+
|
|
12
|
+
RUST_REPO = "https://github.com/protegrity/sql-glot-rust.git"
|
|
13
|
+
RUST_TAG = "v0.10.0"
|
|
14
|
+
EXT_DIR = __dir__
|
|
15
|
+
RUST_DIR = File.join(EXT_DIR, "sql-glot-rust")
|
|
16
|
+
LIB_DIR = File.expand_path("../../lib/sqlglot", EXT_DIR)
|
|
17
|
+
|
|
18
|
+
# ── Skip build if the shared library already exists ────────────────────
|
|
19
|
+
|
|
20
|
+
so_already_built = Dir[File.join(LIB_DIR, "libsqlglot_rust.{so,dylib,dll}")].any?
|
|
21
|
+
|
|
22
|
+
if so_already_built
|
|
23
|
+
puts "libsqlglot_rust already exists in #{LIB_DIR}, skipping Rust build."
|
|
24
|
+
File.write(File.join(EXT_DIR, "Makefile"), "all:\ninstall:\nclean:\n")
|
|
25
|
+
exit 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ── Pre-flight checks ─────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
def command_exists?(cmd)
|
|
31
|
+
system("command -v #{cmd} > /dev/null 2>&1")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
unless command_exists?("cargo")
|
|
35
|
+
abort <<~MSG
|
|
36
|
+
ERROR: `cargo` not found on PATH.
|
|
37
|
+
|
|
38
|
+
The sqlglot gem requires the Rust toolchain to compile the native library.
|
|
39
|
+
Install Rust via https://rustup.rs and ensure `cargo` is on your PATH,
|
|
40
|
+
then run `gem install sqlglot` again.
|
|
41
|
+
MSG
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless command_exists?("git")
|
|
45
|
+
abort "ERROR: `git` not found on PATH. It is needed to fetch the Rust source."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ── Clone the Rust source ─────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
unless Dir.exist?(RUST_DIR)
|
|
51
|
+
puts "Cloning #{RUST_REPO} at #{RUST_TAG}..."
|
|
52
|
+
ok = system("git", "clone", "--depth", "1", "--branch", RUST_TAG, RUST_REPO, RUST_DIR)
|
|
53
|
+
abort "ERROR: git clone failed" unless ok
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# ── Build the shared library ──────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
puts "Building sql-glot-rust (release)..."
|
|
59
|
+
ok = system("cargo", "build", "--release",
|
|
60
|
+
"--manifest-path", File.join(RUST_DIR, "Cargo.toml"))
|
|
61
|
+
abort "ERROR: cargo build failed" unless ok
|
|
62
|
+
|
|
63
|
+
# ── Copy the artifact into lib/sqlglot/ ───────────────────────────────
|
|
64
|
+
|
|
65
|
+
release_dir = File.join(RUST_DIR, "target", "release")
|
|
66
|
+
|
|
67
|
+
so_glob = File.join(release_dir, "libsqlglot_rust.{so,dylib,dll}")
|
|
68
|
+
so_file = Dir[so_glob].first
|
|
69
|
+
|
|
70
|
+
if so_file
|
|
71
|
+
FileUtils.mkdir_p(LIB_DIR)
|
|
72
|
+
FileUtils.cp(so_file, LIB_DIR, verbose: true)
|
|
73
|
+
puts "Installed #{File.basename(so_file)} into #{LIB_DIR}"
|
|
74
|
+
else
|
|
75
|
+
abort "ERROR: shared library not found in #{release_dir}. " \
|
|
76
|
+
"Expected libsqlglot_rust.so, .dylib, or .dll"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ── Write a dummy Makefile (required by rubygems extension protocol) ──
|
|
80
|
+
|
|
81
|
+
File.write(File.join(EXT_DIR, "Makefile"), <<~MAKEFILE)
|
|
82
|
+
all:
|
|
83
|
+
install:
|
|
84
|
+
clean:
|
|
85
|
+
MAKEFILE
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sqlglot
|
|
4
|
+
# Generic helpers for traversing the JSON AST Hash returned by
|
|
5
|
+
# {Sqlglot.parse}.
|
|
6
|
+
#
|
|
7
|
+
# The AST is a nested structure of Hashes and Arrays. Statement and
|
|
8
|
+
# expression types appear as single-key Hashes:
|
|
9
|
+
#
|
|
10
|
+
# {"Column" => {"name" => "id", "table" => "users", ...}}
|
|
11
|
+
# {"Number" => "42"}
|
|
12
|
+
# {"BinaryOp" => {"left" => ..., "op" => "Eq", "right" => ...}}
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
module AstWalker
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Depth-first walk of the AST. Yields every Hash node whose single
|
|
19
|
+
# key is a recognised AST type name (capitalised, e.g. "Column").
|
|
20
|
+
#
|
|
21
|
+
# @param node [Hash, Array, Object] the AST subtree
|
|
22
|
+
# @yieldparam type_key [String] the AST type name
|
|
23
|
+
# @yieldparam value [Object] the contents of that node
|
|
24
|
+
# @yieldparam node [Hash] the full single-key Hash
|
|
25
|
+
def walk(node, &block)
|
|
26
|
+
case node
|
|
27
|
+
when Hash
|
|
28
|
+
node.each do |key, value|
|
|
29
|
+
# Yield if this looks like a typed AST node (capitalised key).
|
|
30
|
+
if key.is_a?(String) && key =~ /\A[A-Z]/
|
|
31
|
+
yield(key, value, node)
|
|
32
|
+
end
|
|
33
|
+
walk(value, &block)
|
|
34
|
+
end
|
|
35
|
+
when Array
|
|
36
|
+
node.each { |child| walk(child, &block) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Collect all nodes of a given AST type anywhere in the subtree.
|
|
41
|
+
#
|
|
42
|
+
# @param node [Hash, Array] the AST subtree
|
|
43
|
+
# @param type_key [String] e.g. "Column", "Table", "Function"
|
|
44
|
+
# @return [Array<Object>] the value side of each matching node
|
|
45
|
+
def find_all(node, type_key)
|
|
46
|
+
results = []
|
|
47
|
+
walk(node) do |key, value, _|
|
|
48
|
+
results << value if key == type_key
|
|
49
|
+
end
|
|
50
|
+
results
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Collect all Column references within an expression subtree.
|
|
54
|
+
# Returns an array of +{name:, table:}+ hashes.
|
|
55
|
+
#
|
|
56
|
+
# @param node [Hash, Array]
|
|
57
|
+
# @return [Array<Hash>]
|
|
58
|
+
def extract_columns(node)
|
|
59
|
+
find_all(node, "Column").map do |col|
|
|
60
|
+
{ name: col["name"], table: col["table"] }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Convert an AST literal node to a Ruby value.
|
|
65
|
+
#
|
|
66
|
+
# {"Number" => "42"} => 42
|
|
67
|
+
# {"Number" => "3.14"} => 3.14
|
|
68
|
+
# {"StringLiteral" => "foo"} => "foo"
|
|
69
|
+
# {"Boolean" => true} => true
|
|
70
|
+
# {"Null" => ...} => nil
|
|
71
|
+
#
|
|
72
|
+
# @param node [Hash]
|
|
73
|
+
# @return [Integer, Float, String, true, false, nil]
|
|
74
|
+
def extract_value(node)
|
|
75
|
+
return nil unless node.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
if node.key?("Number")
|
|
78
|
+
num = node["Number"]
|
|
79
|
+
num.include?(".") ? num.to_f : num.to_i
|
|
80
|
+
elsif node.key?("StringLiteral")
|
|
81
|
+
node["StringLiteral"]
|
|
82
|
+
elsif node.key?("Boolean")
|
|
83
|
+
node["Boolean"]
|
|
84
|
+
elsif node.key?("Null")
|
|
85
|
+
nil
|
|
86
|
+
else
|
|
87
|
+
# Unknown literal type -- return the raw node.
|
|
88
|
+
node
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Return the single AST type key of a node, if it has exactly one
|
|
93
|
+
# capitalised key (the standard pattern).
|
|
94
|
+
#
|
|
95
|
+
# @param node [Hash]
|
|
96
|
+
# @return [String, nil]
|
|
97
|
+
def node_type(node)
|
|
98
|
+
return nil unless node.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
node.each_key do |k|
|
|
101
|
+
return k if k.is_a?(String) && k =~ /\A[A-Z]/
|
|
102
|
+
end
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Unwrap nested single-key wrappers to get the inner payload.
|
|
107
|
+
#
|
|
108
|
+
# For example:
|
|
109
|
+
# {"Expr" => {"expr" => {"Column" => {...}}, "alias" => nil}}
|
|
110
|
+
#
|
|
111
|
+
# @param node [Hash]
|
|
112
|
+
# @return [Hash]
|
|
113
|
+
def unwrap(node)
|
|
114
|
+
return node unless node.is_a?(Hash) && node.size == 1
|
|
115
|
+
|
|
116
|
+
key = node.keys.first
|
|
117
|
+
val = node.values.first
|
|
118
|
+
val.is_a?(Hash) ? val : node
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sqlglot
|
|
4
|
+
# Constants for all 30 SQL dialects supported by sql-glot-rust.
|
|
5
|
+
#
|
|
6
|
+
# Each constant holds the string name passed to the Rust FFI.
|
|
7
|
+
# Use symbols or strings interchangeably in the public API --
|
|
8
|
+
# {Dialect.resolve} normalizes them.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# Sqlglot.transpile(sql, from: Sqlglot::Dialect::POSTGRES, to: Sqlglot::Dialect::BIGQUERY)
|
|
12
|
+
# Sqlglot.transpile(sql, from: :postgres, to: :bigquery) # same thing
|
|
13
|
+
module Dialect
|
|
14
|
+
# ── Official dialects ──────────────────────────────────────
|
|
15
|
+
ANSI = "ansi"
|
|
16
|
+
ATHENA = "athena"
|
|
17
|
+
BIGQUERY = "bigquery"
|
|
18
|
+
CLICKHOUSE = "clickhouse"
|
|
19
|
+
DATABRICKS = "databricks"
|
|
20
|
+
DUCKDB = "duckdb"
|
|
21
|
+
HIVE = "hive"
|
|
22
|
+
MYSQL = "mysql"
|
|
23
|
+
ORACLE = "oracle"
|
|
24
|
+
POSTGRES = "postgres"
|
|
25
|
+
PRESTO = "presto"
|
|
26
|
+
REDSHIFT = "redshift"
|
|
27
|
+
SNOWFLAKE = "snowflake"
|
|
28
|
+
SPARK = "spark"
|
|
29
|
+
SQLITE = "sqlite"
|
|
30
|
+
STARROCKS = "starrocks"
|
|
31
|
+
TRINO = "trino"
|
|
32
|
+
TSQL = "tsql"
|
|
33
|
+
|
|
34
|
+
# ── Community dialects ─────────────────────────────────────
|
|
35
|
+
DORIS = "doris"
|
|
36
|
+
DREMIO = "dremio"
|
|
37
|
+
DRILL = "drill"
|
|
38
|
+
DRUID = "druid"
|
|
39
|
+
EXASOL = "exasol"
|
|
40
|
+
FABRIC = "fabric"
|
|
41
|
+
MATERIALIZE = "materialize"
|
|
42
|
+
PRQL = "prql"
|
|
43
|
+
RISINGWAVE = "risingwave"
|
|
44
|
+
SINGLESTORE = "singlestore"
|
|
45
|
+
TABLEAU = "tableau"
|
|
46
|
+
TERADATA = "teradata"
|
|
47
|
+
|
|
48
|
+
# Common aliases accepted by the Rust library's Dialect::from_str.
|
|
49
|
+
ALIASES = {
|
|
50
|
+
"postgresql" => POSTGRES,
|
|
51
|
+
"mssql" => TSQL,
|
|
52
|
+
"sqlserver" => TSQL,
|
|
53
|
+
"mariadb" => MYSQL,
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
# All known dialect names (constants + aliases).
|
|
57
|
+
ALL = constants
|
|
58
|
+
.reject { |c| %i[ALIASES ALL].include?(c) }
|
|
59
|
+
.map { |c| const_get(c) }
|
|
60
|
+
.freeze
|
|
61
|
+
|
|
62
|
+
# Normalize a dialect argument to the string the Rust FFI expects.
|
|
63
|
+
#
|
|
64
|
+
# Accepts symbols, strings, or nil. Unknown values raise ArgumentError.
|
|
65
|
+
#
|
|
66
|
+
# @param name [Symbol, String, nil]
|
|
67
|
+
# @return [String, nil] the dialect string, or nil if name is nil
|
|
68
|
+
def self.resolve(name)
|
|
69
|
+
return nil if name.nil?
|
|
70
|
+
|
|
71
|
+
key = name.to_s.downcase.strip
|
|
72
|
+
return key if ALL.include?(key)
|
|
73
|
+
return ALIASES[key] if ALIASES.key?(key)
|
|
74
|
+
|
|
75
|
+
raise ArgumentError, "Unknown dialect: #{name.inspect}. " \
|
|
76
|
+
"Known dialects: #{ALL.sort.join(', ')}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sqlglot
|
|
4
|
+
# Base error for all Sqlglot errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the shared library cannot be found or loaded.
|
|
8
|
+
class LibraryNotFoundError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when SQL parsing fails (the Rust FFI returned NULL).
|
|
11
|
+
class ParseError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when SQL transpilation fails.
|
|
14
|
+
class TranspileError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when SQL generation from an AST fails.
|
|
17
|
+
class GenerateError < Error; end
|
|
18
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Sqlglot
|
|
7
|
+
# Low-level FFI bindings to libsqlglot_rust.
|
|
8
|
+
#
|
|
9
|
+
# This module is not intended for direct use -- see the public API on
|
|
10
|
+
# {Sqlglot} instead. All returned C strings are freed via +sqlglot_free+
|
|
11
|
+
# in +ensure+ blocks so callers never need to manage memory.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module Native
|
|
15
|
+
extend FFI::Library
|
|
16
|
+
|
|
17
|
+
# Locate the shared library. Search order:
|
|
18
|
+
# 1. SQLGLOT_LIB_PATH environment variable (explicit override)
|
|
19
|
+
# 2. lib/sqlglot/ inside the gem (where extconf.rb copies the build)
|
|
20
|
+
# 3. ext/sqlglot_rust/sql-glot-rust/target/release/ (dev builds)
|
|
21
|
+
# 4. System library path (LD_LIBRARY_PATH / DYLD_LIBRARY_PATH)
|
|
22
|
+
LIB_NAME = "sqlglot_rust"
|
|
23
|
+
|
|
24
|
+
SOEXT = FFI::Platform::LIBSUFFIX # "so" on Linux, "dylib" on macOS
|
|
25
|
+
|
|
26
|
+
def self.find_library
|
|
27
|
+
# 1. Explicit override
|
|
28
|
+
if (env_path = ENV["SQLGLOT_LIB_PATH"])
|
|
29
|
+
return env_path if File.exist?(env_path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
gem_root = File.expand_path("../..", __dir__)
|
|
33
|
+
|
|
34
|
+
candidates = [
|
|
35
|
+
# 2. Installed location (lib/sqlglot/)
|
|
36
|
+
File.join(gem_root, "lib", "sqlglot", "lib#{LIB_NAME}.#{SOEXT}"),
|
|
37
|
+
# 3. Dev build location
|
|
38
|
+
File.join(gem_root, "ext", "sqlglot_rust", "sql-glot-rust",
|
|
39
|
+
"target", "release", "lib#{LIB_NAME}.#{SOEXT}"),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
candidates.each { |path| return path if File.exist?(path) }
|
|
43
|
+
|
|
44
|
+
# 4. System path fallback (FFI will search LD_LIBRARY_PATH etc.)
|
|
45
|
+
LIB_NAME
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
ffi_lib find_library
|
|
50
|
+
rescue LoadError => e
|
|
51
|
+
raise Sqlglot::LibraryNotFoundError,
|
|
52
|
+
"Could not load libsqlglot_rust. #{e.message}\n\n" \
|
|
53
|
+
"Make sure the Rust library is built. Run:\n" \
|
|
54
|
+
" rake cargo:build\n\n" \
|
|
55
|
+
"Or set SQLGLOT_LIB_PATH to the full path of the .so/.dylib file."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ── C function declarations ────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
# char *sqlglot_parse(const char *sql, const char *dialect);
|
|
61
|
+
attach_function :sqlglot_parse, [:string, :string], :pointer
|
|
62
|
+
|
|
63
|
+
# char *sqlglot_transpile(const char *sql, const char *from, const char *to);
|
|
64
|
+
attach_function :sqlglot_transpile, [:string, :string, :string], :pointer
|
|
65
|
+
|
|
66
|
+
# char *sqlglot_generate(const char *ast_json, const char *dialect);
|
|
67
|
+
attach_function :sqlglot_generate, [:string, :string], :pointer
|
|
68
|
+
|
|
69
|
+
# const char *sqlglot_version(void);
|
|
70
|
+
attach_function :sqlglot_version, [], :string
|
|
71
|
+
|
|
72
|
+
# void sqlglot_free(char *ptr);
|
|
73
|
+
attach_function :sqlglot_free, [:pointer], :void
|
|
74
|
+
|
|
75
|
+
# ── Safe wrappers ──────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
# Read a C string returned by the FFI, free it, and return a Ruby String.
|
|
78
|
+
# Raises the given error class if the pointer is NULL.
|
|
79
|
+
#
|
|
80
|
+
# @param ptr [FFI::Pointer]
|
|
81
|
+
# @param error_class [Class<Sqlglot::Error>]
|
|
82
|
+
# @param message [String]
|
|
83
|
+
# @return [String]
|
|
84
|
+
def self.read_and_free(ptr, error_class:, message:)
|
|
85
|
+
if ptr.null?
|
|
86
|
+
raise error_class, message
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
ptr.read_string.force_encoding("UTF-8")
|
|
90
|
+
ensure
|
|
91
|
+
sqlglot_free(ptr) if ptr && !ptr.null?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Parse SQL and return the JSON AST string.
|
|
95
|
+
#
|
|
96
|
+
# @param sql [String]
|
|
97
|
+
# @param dialect [String, nil]
|
|
98
|
+
# @return [String] JSON string
|
|
99
|
+
def self.parse(sql, dialect)
|
|
100
|
+
ptr = sqlglot_parse(sql, dialect)
|
|
101
|
+
read_and_free(ptr,
|
|
102
|
+
error_class: ParseError,
|
|
103
|
+
message: "Failed to parse SQL: #{sql.inspect}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Transpile SQL from one dialect to another.
|
|
107
|
+
#
|
|
108
|
+
# @param sql [String]
|
|
109
|
+
# @param from_dialect [String, nil]
|
|
110
|
+
# @param to_dialect [String, nil]
|
|
111
|
+
# @return [String] transpiled SQL
|
|
112
|
+
def self.transpile(sql, from_dialect, to_dialect)
|
|
113
|
+
ptr = sqlglot_transpile(sql, from_dialect, to_dialect)
|
|
114
|
+
read_and_free(ptr,
|
|
115
|
+
error_class: TranspileError,
|
|
116
|
+
message: "Failed to transpile SQL: #{sql.inspect}")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generate SQL from a JSON AST string.
|
|
120
|
+
#
|
|
121
|
+
# @param ast_json [String]
|
|
122
|
+
# @param dialect [String, nil]
|
|
123
|
+
# @return [String] generated SQL
|
|
124
|
+
def self.generate(ast_json, dialect)
|
|
125
|
+
ptr = sqlglot_generate(ast_json, dialect)
|
|
126
|
+
read_and_free(ptr,
|
|
127
|
+
error_class: GenerateError,
|
|
128
|
+
message: "Failed to generate SQL from AST")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Return the library version string.
|
|
132
|
+
#
|
|
133
|
+
# @return [String]
|
|
134
|
+
def self.version
|
|
135
|
+
sqlglot_version()
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|