sqlglot 0.1.0-x86_64-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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b272b226c1deca727fe3642163d1f5e5aaa25e9dd5055f28880ee41877659641
4
+ data.tar.gz: 63d25d7d9338a96f64cc86a9b7e2fd41f36703a87c550cd9173d1eae078ee7ac
5
+ SHA512:
6
+ metadata.gz: 417d2c2a7b4804f0141c65a70717be1aca591fd1e0a6f44f18d04bd582cc6626eecaa2b394b3aa8330e9a602cdc2cb70d7185d5d7948ae13a2875f54a41ba573
7
+ data.tar.gz: 8411d255c6aa2938cfc57f694b8107e0eb38cea3a80b1d72b31eea0b3b2e97ad1562ffb36b831a042d038f59259d16c75ebc65555c9fac1198aaf4cde9e2932f
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem "rspec", "~> 3.12"
9
+ gem "rake", "~> 13.0"
10
+ end
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