polyglot-sql 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 23c2e1649bd1bbecd604bda51f8ba41f91e43cfb62a31b90a9ab8a9e351c9157
4
+ data.tar.gz: 1bc16150dd37f2126f08b67716df2dbbf62d6d17416e23c9da374b834d140d46
5
+ SHA512:
6
+ metadata.gz: 3dd316989daa3328c44627dac6b6c7922f023d0c2883584bda1e23e68a43d4c9efde271ff10c5ab4b90b43ba935aaae872c599e4fd9bb7273328a1a1141891de
7
+ data.tar.gz: b14f936805c04980ff56be1b1c04c10dfb47499d2e0344c2ac113d51ab1e36e1ba5485ce9f1103a8b757c58ce7e30591f1f76ce64a0dba66e65e2dce6fff6d94
data/Cargo.toml ADDED
@@ -0,0 +1,9 @@
1
+ [workspace]
2
+ members = ["ext/polyglot_rb"]
3
+ resolver = "2"
4
+
5
+ [profile.release]
6
+ strip = true
7
+
8
+ [profile.dev]
9
+ strip = true
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Atkins
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,136 @@
1
+ # polyglot-sql
2
+
3
+ [![Build status](https://badge.buildkite.com/e9b6acd7cb9addaa60ca20a96e74f3934285e9ccbd969358f2.svg)](https://buildkite.com/catkins-test/polyglot-sql-rb)
4
+
5
+ Ruby bindings for [polyglot-sql](https://github.com/tobilg/polyglot) — a Rust-based SQL transpiler supporting 30+ database dialects.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "polyglot-sql"
13
+ ```
14
+
15
+ Requires Rust toolchain for compilation. Install via [rustup](https://rustup.rs/).
16
+
17
+ ## Usage
18
+
19
+ ### Transpile SQL Between Dialects
20
+
21
+ ```ruby
22
+ require "polyglot"
23
+
24
+ # PostgreSQL to MySQL
25
+ Polyglot.transpile("SELECT NOW()", from: :postgres, to: :mysql)
26
+ # => ["SELECT CURRENT_TIMESTAMP()"]
27
+
28
+ # PostgreSQL to Snowflake
29
+ Polyglot.transpile("SELECT CAST(x AS TEXT)", from: :postgres, to: :snowflake)
30
+ # => ["SELECT CAST(x AS TEXT)"]
31
+ ```
32
+
33
+ ### Parse SQL to AST
34
+
35
+ ```ruby
36
+ ast = Polyglot.parse("SELECT 1", dialect: :postgres)
37
+ # => [{"select" => {...}}]
38
+
39
+ ast = Polyglot.parse_one("SELECT 1", dialect: :postgres)
40
+ # => {"select" => {...}}
41
+ ```
42
+
43
+ ### Generate SQL from AST
44
+
45
+ ```ruby
46
+ ast = Polyglot.parse_one("SELECT 1", dialect: :postgres)
47
+ Polyglot.generate(ast, dialect: :mysql)
48
+ # => "SELECT 1"
49
+ ```
50
+
51
+ ### Format SQL
52
+
53
+ ```ruby
54
+ Polyglot.format("SELECT a, b FROM t WHERE x = 1", dialect: :postgres)
55
+ ```
56
+
57
+ ### Validate SQL
58
+
59
+ ```ruby
60
+ result = Polyglot.validate("SELECT 1", dialect: :postgres)
61
+ result.valid? # => true
62
+ result.errors # => []
63
+
64
+ result = Polyglot.validate("SELEC 1")
65
+ result.valid? # => false
66
+ result.errors.first.message # => "..."
67
+ ```
68
+
69
+ ### List Supported Dialects
70
+
71
+ ```ruby
72
+ Polyglot.dialects
73
+ # => ["generic", "athena", "bigquery", "clickhouse", ..., "tsql"]
74
+ ```
75
+
76
+ ## Supported Dialects
77
+
78
+ <!-- SUPPORTED_DIALECTS:START -->
79
+ - Generic
80
+ - Athena
81
+ - BigQuery
82
+ - ClickHouse
83
+ - CockroachDB
84
+ - Databricks
85
+ - Doris
86
+ - Dremio
87
+ - Drill
88
+ - Druid
89
+ - DuckDB
90
+ - Dune
91
+ - Exasol
92
+ - Fabric
93
+ - Hive
94
+ - Materialize
95
+ - MySQL
96
+ - Oracle
97
+ - PostgreSQL
98
+ - Presto
99
+ - Redshift
100
+ - RisingWave
101
+ - SingleStore
102
+ - Snowflake
103
+ - Solr
104
+ - Spark
105
+ - SQLite
106
+ - StarRocks
107
+ - Tableau
108
+ - Teradata
109
+ - TiDB
110
+ - Trino
111
+ - T-SQL
112
+ <!-- SUPPORTED_DIALECTS:END -->
113
+
114
+ ## Error Handling
115
+
116
+ ```ruby
117
+ Polyglot::Error # Base error class
118
+ Polyglot::ParseError # SQL parsing errors
119
+ Polyglot::GenerateError # SQL generation errors
120
+ Polyglot::UnsupportedError # Unsupported dialect features
121
+ ```
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ bundle install
127
+ bundle exec rake # compile + test
128
+ bundle exec rake compile # compile only
129
+ bundle exec rake spec # test only
130
+ bundle exec standardrb # lint
131
+ bundle exec rake docs:dialects # sync README dialect list
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "polyglot_rb"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ publish = false
6
+ license = "MIT"
7
+ description = "Ruby bindings for polyglot-sql transpiler"
8
+
9
+ [lib]
10
+ name = "polyglot_rb"
11
+ crate-type = ["cdylib"]
12
+
13
+ [dependencies]
14
+ polyglot-sql = "0.1"
15
+ magnus = { version = "0.8", features = ["rb-sys"] }
16
+ rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
17
+ serde_json = "1.0"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("polyglot/polyglot_rb")
@@ -0,0 +1,199 @@
1
+ use magnus::{Error, Ruby};
2
+ use polyglot_sql::dialects::DialectType;
3
+
4
+ struct DialectSpec {
5
+ canonical: &'static str,
6
+ dialect: DialectType,
7
+ aliases: &'static [&'static str],
8
+ }
9
+
10
+ const DIALECT_SPECS: &[DialectSpec] = &[
11
+ DialectSpec {
12
+ canonical: "generic",
13
+ dialect: DialectType::Generic,
14
+ aliases: &[],
15
+ },
16
+ DialectSpec {
17
+ canonical: "athena",
18
+ dialect: DialectType::Athena,
19
+ aliases: &[],
20
+ },
21
+ DialectSpec {
22
+ canonical: "bigquery",
23
+ dialect: DialectType::BigQuery,
24
+ aliases: &[],
25
+ },
26
+ DialectSpec {
27
+ canonical: "clickhouse",
28
+ dialect: DialectType::ClickHouse,
29
+ aliases: &[],
30
+ },
31
+ DialectSpec {
32
+ canonical: "cockroachdb",
33
+ dialect: DialectType::CockroachDB,
34
+ aliases: &[],
35
+ },
36
+ DialectSpec {
37
+ canonical: "databricks",
38
+ dialect: DialectType::Databricks,
39
+ aliases: &[],
40
+ },
41
+ DialectSpec {
42
+ canonical: "doris",
43
+ dialect: DialectType::Doris,
44
+ aliases: &[],
45
+ },
46
+ DialectSpec {
47
+ canonical: "dremio",
48
+ dialect: DialectType::Dremio,
49
+ aliases: &[],
50
+ },
51
+ DialectSpec {
52
+ canonical: "drill",
53
+ dialect: DialectType::Drill,
54
+ aliases: &[],
55
+ },
56
+ DialectSpec {
57
+ canonical: "druid",
58
+ dialect: DialectType::Druid,
59
+ aliases: &[],
60
+ },
61
+ DialectSpec {
62
+ canonical: "duckdb",
63
+ dialect: DialectType::DuckDB,
64
+ aliases: &[],
65
+ },
66
+ DialectSpec {
67
+ canonical: "dune",
68
+ dialect: DialectType::Dune,
69
+ aliases: &[],
70
+ },
71
+ DialectSpec {
72
+ canonical: "exasol",
73
+ dialect: DialectType::Exasol,
74
+ aliases: &[],
75
+ },
76
+ DialectSpec {
77
+ canonical: "fabric",
78
+ dialect: DialectType::Fabric,
79
+ aliases: &[],
80
+ },
81
+ DialectSpec {
82
+ canonical: "hive",
83
+ dialect: DialectType::Hive,
84
+ aliases: &[],
85
+ },
86
+ DialectSpec {
87
+ canonical: "materialize",
88
+ dialect: DialectType::Materialize,
89
+ aliases: &[],
90
+ },
91
+ DialectSpec {
92
+ canonical: "mysql",
93
+ dialect: DialectType::MySQL,
94
+ aliases: &[],
95
+ },
96
+ DialectSpec {
97
+ canonical: "oracle",
98
+ dialect: DialectType::Oracle,
99
+ aliases: &[],
100
+ },
101
+ DialectSpec {
102
+ canonical: "postgres",
103
+ dialect: DialectType::PostgreSQL,
104
+ aliases: &["postgresql"],
105
+ },
106
+ DialectSpec {
107
+ canonical: "presto",
108
+ dialect: DialectType::Presto,
109
+ aliases: &[],
110
+ },
111
+ DialectSpec {
112
+ canonical: "redshift",
113
+ dialect: DialectType::Redshift,
114
+ aliases: &[],
115
+ },
116
+ DialectSpec {
117
+ canonical: "risingwave",
118
+ dialect: DialectType::RisingWave,
119
+ aliases: &[],
120
+ },
121
+ DialectSpec {
122
+ canonical: "singlestore",
123
+ dialect: DialectType::SingleStore,
124
+ aliases: &[],
125
+ },
126
+ DialectSpec {
127
+ canonical: "snowflake",
128
+ dialect: DialectType::Snowflake,
129
+ aliases: &[],
130
+ },
131
+ DialectSpec {
132
+ canonical: "solr",
133
+ dialect: DialectType::Solr,
134
+ aliases: &[],
135
+ },
136
+ DialectSpec {
137
+ canonical: "spark",
138
+ dialect: DialectType::Spark,
139
+ aliases: &[],
140
+ },
141
+ DialectSpec {
142
+ canonical: "sqlite",
143
+ dialect: DialectType::SQLite,
144
+ aliases: &[],
145
+ },
146
+ DialectSpec {
147
+ canonical: "starrocks",
148
+ dialect: DialectType::StarRocks,
149
+ aliases: &[],
150
+ },
151
+ DialectSpec {
152
+ canonical: "tableau",
153
+ dialect: DialectType::Tableau,
154
+ aliases: &[],
155
+ },
156
+ DialectSpec {
157
+ canonical: "teradata",
158
+ dialect: DialectType::Teradata,
159
+ aliases: &[],
160
+ },
161
+ DialectSpec {
162
+ canonical: "tidb",
163
+ dialect: DialectType::TiDB,
164
+ aliases: &[],
165
+ },
166
+ DialectSpec {
167
+ canonical: "trino",
168
+ dialect: DialectType::Trino,
169
+ aliases: &[],
170
+ },
171
+ DialectSpec {
172
+ canonical: "tsql",
173
+ dialect: DialectType::TSQL,
174
+ aliases: &[],
175
+ },
176
+ ];
177
+
178
+ pub fn dialect_from_name(name: &str) -> Result<DialectType, Error> {
179
+ let normalized = name.to_lowercase().replace(['-', '_'], "");
180
+
181
+ for spec in DIALECT_SPECS {
182
+ if spec.canonical == normalized || spec.aliases.contains(&normalized.as_str()) {
183
+ return Ok(spec.dialect);
184
+ }
185
+ }
186
+
187
+ let ruby = Ruby::get().expect("Ruby runtime not available");
188
+ Err(Error::new(
189
+ ruby.exception_arg_error(),
190
+ format!("unknown dialect: '{}'. Use Polyglot.dialects to see supported dialects", name),
191
+ ))
192
+ }
193
+
194
+ pub fn dialect_names() -> Vec<String> {
195
+ DIALECT_SPECS
196
+ .iter()
197
+ .map(|spec| spec.canonical.to_string())
198
+ .collect()
199
+ }
@@ -0,0 +1,100 @@
1
+ use magnus::{Error, ExceptionClass, Module, Ruby};
2
+ use std::cell::RefCell;
3
+
4
+ thread_local! {
5
+ static POLYGLOT_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
6
+ static PARSE_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
7
+ static GENERATE_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
8
+ static UNSUPPORTED_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
9
+ }
10
+
11
+ pub fn define_exceptions(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
12
+ let standard_error = ruby.exception_standard_error();
13
+
14
+ let polyglot_error = module.define_error("Error", standard_error)?;
15
+ POLYGLOT_ERROR.with(|cell| {
16
+ *cell.borrow_mut() = Some(polyglot_error);
17
+ });
18
+
19
+ let parse_error = module.define_error("ParseError", polyglot_error)?;
20
+ PARSE_ERROR.with(|cell| {
21
+ *cell.borrow_mut() = Some(parse_error);
22
+ });
23
+
24
+ let generate_error = module.define_error("GenerateError", polyglot_error)?;
25
+ GENERATE_ERROR.with(|cell| {
26
+ *cell.borrow_mut() = Some(generate_error);
27
+ });
28
+
29
+ let unsupported_error = module.define_error("UnsupportedError", polyglot_error)?;
30
+ UNSUPPORTED_ERROR.with(|cell| {
31
+ *cell.borrow_mut() = Some(unsupported_error);
32
+ });
33
+
34
+ Ok(())
35
+ }
36
+
37
+ pub fn polyglot_error(message: String) -> Error {
38
+ POLYGLOT_ERROR.with(|cell| {
39
+ let class = cell.borrow();
40
+ match class.as_ref() {
41
+ Some(cls) => Error::new(*cls, message),
42
+ None => {
43
+ let ruby = Ruby::get().expect("Ruby runtime not available");
44
+ Error::new(ruby.exception_runtime_error(), message)
45
+ }
46
+ }
47
+ })
48
+ }
49
+
50
+ pub fn parse_error(message: String) -> Error {
51
+ PARSE_ERROR.with(|cell| {
52
+ let class = cell.borrow();
53
+ match class.as_ref() {
54
+ Some(cls) => Error::new(*cls, message),
55
+ None => {
56
+ let ruby = Ruby::get().expect("Ruby runtime not available");
57
+ Error::new(ruby.exception_runtime_error(), message)
58
+ }
59
+ }
60
+ })
61
+ }
62
+
63
+ pub fn generate_error(message: String) -> Error {
64
+ GENERATE_ERROR.with(|cell| {
65
+ let class = cell.borrow();
66
+ match class.as_ref() {
67
+ Some(cls) => Error::new(*cls, message),
68
+ None => {
69
+ let ruby = Ruby::get().expect("Ruby runtime not available");
70
+ Error::new(ruby.exception_runtime_error(), message)
71
+ }
72
+ }
73
+ })
74
+ }
75
+
76
+ pub fn unsupported_error(message: String) -> Error {
77
+ UNSUPPORTED_ERROR.with(|cell| {
78
+ let class = cell.borrow();
79
+ match class.as_ref() {
80
+ Some(cls) => Error::new(*cls, message),
81
+ None => {
82
+ let ruby = Ruby::get().expect("Ruby runtime not available");
83
+ Error::new(ruby.exception_runtime_error(), message)
84
+ }
85
+ }
86
+ })
87
+ }
88
+
89
+ pub fn map_polyglot_error(err: polyglot_sql::error::Error) -> Error {
90
+ let message = err.to_string();
91
+
92
+ match err {
93
+ polyglot_sql::error::Error::Parse(..) => parse_error(message),
94
+ polyglot_sql::error::Error::Tokenize { .. } => parse_error(message),
95
+ polyglot_sql::error::Error::Syntax { .. } => parse_error(message),
96
+ polyglot_sql::error::Error::Generate(..) => generate_error(message),
97
+ polyglot_sql::error::Error::Unsupported { .. } => unsupported_error(message),
98
+ polyglot_sql::error::Error::Internal(..) => polyglot_error(message),
99
+ }
100
+ }
@@ -0,0 +1,108 @@
1
+ use magnus::{function, Error, Object, RArray, Ruby};
2
+
3
+ mod dialect;
4
+ mod errors;
5
+
6
+ fn transpile(sql: String, from: String, to: String) -> Result<RArray, Error> {
7
+ let ruby = Ruby::get().expect("Ruby runtime not available");
8
+ let from_dialect = dialect::dialect_from_name(&from)?;
9
+ let to_dialect = dialect::dialect_from_name(&to)?;
10
+
11
+ let results = polyglot_sql::transpile(&sql, from_dialect, to_dialect)
12
+ .map_err(errors::map_polyglot_error)?;
13
+
14
+ let arr = ruby.ary_new_capa(results.len());
15
+ for s in results {
16
+ arr.push(ruby.str_new(&s))?;
17
+ }
18
+ Ok(arr)
19
+ }
20
+
21
+ fn parse(sql: String, dialect_name: String) -> Result<String, Error> {
22
+ let dialect_type = dialect::dialect_from_name(&dialect_name)?;
23
+
24
+ let expressions = polyglot_sql::parse(&sql, dialect_type)
25
+ .map_err(errors::map_polyglot_error)?;
26
+
27
+ serde_json::to_string(&expressions)
28
+ .map_err(|e| errors::polyglot_error(format!("JSON serialization error: {e}")))
29
+ }
30
+
31
+ fn parse_one(sql: String, dialect_name: String) -> Result<String, Error> {
32
+ let dialect_type = dialect::dialect_from_name(&dialect_name)?;
33
+
34
+ let expression = polyglot_sql::parse_one(&sql, dialect_type)
35
+ .map_err(errors::map_polyglot_error)?;
36
+
37
+ serde_json::to_string(&expression)
38
+ .map_err(|e| errors::polyglot_error(format!("JSON serialization error: {e}")))
39
+ }
40
+
41
+ fn generate(ast_json: String, dialect_name: String) -> Result<String, Error> {
42
+ let dialect_type = dialect::dialect_from_name(&dialect_name)?;
43
+
44
+ let expression: polyglot_sql::expressions::Expression = serde_json::from_str(&ast_json)
45
+ .map_err(|e| {
46
+ errors::generate_error(format!(
47
+ "Invalid AST JSON for SQL generation at line {}, column {}",
48
+ e.line(),
49
+ e.column()
50
+ ))
51
+ })?;
52
+
53
+ polyglot_sql::generate(&expression, dialect_type)
54
+ .map_err(errors::map_polyglot_error)
55
+ }
56
+
57
+ fn format_sql(sql: String, dialect_name: String) -> Result<String, Error> {
58
+ let dialect_type = dialect::dialect_from_name(&dialect_name)?;
59
+ let dialect = polyglot_sql::dialects::Dialect::get(dialect_type);
60
+
61
+ let expressions = polyglot_sql::parse(&sql, dialect_type)
62
+ .map_err(errors::map_polyglot_error)?;
63
+
64
+ let mut results = Vec::with_capacity(expressions.len());
65
+ for expr in &expressions {
66
+ let formatted = dialect
67
+ .generate_pretty(expr)
68
+ .map_err(errors::map_polyglot_error)?;
69
+ results.push(formatted);
70
+ }
71
+
72
+ Ok(results.join(";\n"))
73
+ }
74
+
75
+ fn validate(sql: String, dialect_name: String) -> Result<String, Error> {
76
+ let dialect_type = dialect::dialect_from_name(&dialect_name)?;
77
+
78
+ let result = polyglot_sql::validate(&sql, dialect_type);
79
+
80
+ serde_json::to_string(&result)
81
+ .map_err(|e| errors::polyglot_error(format!("JSON serialization error: {e}")))
82
+ }
83
+
84
+ fn dialects() -> Vec<String> {
85
+ dialect::dialect_names()
86
+ }
87
+
88
+ fn version() -> &'static str {
89
+ env!("CARGO_PKG_VERSION")
90
+ }
91
+
92
+ #[magnus::init]
93
+ fn init(ruby: &Ruby) -> Result<(), Error> {
94
+ let module = ruby.define_module("Polyglot")?;
95
+
96
+ errors::define_exceptions(ruby, &module)?;
97
+
98
+ module.define_singleton_method("_transpile", function!(transpile, 3))?;
99
+ module.define_singleton_method("_parse", function!(parse, 2))?;
100
+ module.define_singleton_method("_parse_one", function!(parse_one, 2))?;
101
+ module.define_singleton_method("_generate", function!(generate, 2))?;
102
+ module.define_singleton_method("_format", function!(format_sql, 2))?;
103
+ module.define_singleton_method("_validate", function!(validate, 2))?;
104
+ module.define_singleton_method("dialects", function!(dialects, 0))?;
105
+ module.define_singleton_method("native_version", function!(version, 0))?;
106
+
107
+ Ok(())
108
+ }
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyglot
4
+ class ValidationResult
5
+ attr_reader :errors
6
+
7
+ def initialize(data)
8
+ @valid = data["valid"]
9
+ @errors = (data["errors"] || []).map { |e| ValidationError.new(e) }
10
+ end
11
+
12
+ def valid?
13
+ @valid
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ valid: @valid,
19
+ errors: @errors.map(&:to_h)
20
+ }
21
+ end
22
+
23
+ def inspect
24
+ "#<Polyglot::ValidationResult valid=#{@valid} errors=#{@errors.length}>"
25
+ end
26
+ end
27
+
28
+ class ValidationError
29
+ attr_reader :message, :line, :column, :severity, :code
30
+
31
+ def initialize(data)
32
+ @message = data["message"]
33
+ @line = data["line"]
34
+ @column = data["column"]
35
+ @severity = data["severity"]
36
+ @code = data["code"]
37
+ end
38
+
39
+ def error?
40
+ @severity == "error"
41
+ end
42
+
43
+ def warning?
44
+ @severity == "warning"
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ message: @message,
50
+ line: @line,
51
+ column: @column,
52
+ severity: @severity,
53
+ code: @code
54
+ }
55
+ end
56
+
57
+ def inspect
58
+ "#<Polyglot::ValidationError #{@severity}: #{@message}>"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyglot
4
+ VERSION = "0.1.0"
5
+ end
data/lib/polyglot.rb ADDED
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "polyglot/version"
4
+
5
+ # Load the native extension
6
+ begin
7
+ RUBY_VERSION =~ /(\d+\.\d+)/
8
+ require "polyglot/#{Regexp.last_match(1)}/polyglot_rb"
9
+ rescue LoadError
10
+ require "polyglot/polyglot_rb"
11
+ end
12
+
13
+ require_relative "polyglot/validation_result"
14
+ require "json"
15
+
16
+ module Polyglot
17
+ class << self
18
+ # Transpile SQL from one dialect to another.
19
+ #
20
+ # @param sql [String] the SQL statement(s) to transpile
21
+ # @param from [String, Symbol] the source dialect
22
+ # @param to [String, Symbol] the target dialect
23
+ # @return [Array<String>] the transpiled SQL statements
24
+ #
25
+ # @example
26
+ # Polyglot.transpile("SELECT NOW()", from: :postgres, to: :mysql)
27
+ # # => ["SELECT CURRENT_TIMESTAMP()"]
28
+ #
29
+ def transpile(sql, from:, to:)
30
+ _transpile(sql, from.to_s, to.to_s)
31
+ end
32
+
33
+ # Parse SQL into an abstract syntax tree.
34
+ #
35
+ # @param sql [String] the SQL to parse
36
+ # @param dialect [String, Symbol] the SQL dialect (default: :generic)
37
+ # @return [Array<Hash>] the parsed AST expressions
38
+ #
39
+ # @example
40
+ # Polyglot.parse("SELECT 1", dialect: :postgres)
41
+ # # => [{"select" => {"expressions" => [...]}}]
42
+ #
43
+ def parse(sql, dialect: :generic)
44
+ JSON.parse(_parse(sql, dialect.to_s))
45
+ end
46
+
47
+ # Parse a single SQL statement into an AST.
48
+ #
49
+ # @param sql [String] a single SQL statement
50
+ # @param dialect [String, Symbol] the SQL dialect (default: :generic)
51
+ # @return [Hash] the parsed AST expression
52
+ # @raise [Polyglot::ParseError] if the SQL contains more or fewer than one statement
53
+ #
54
+ # @example
55
+ # ast = Polyglot.parse_one("SELECT 1", dialect: :postgres)
56
+ #
57
+ def parse_one(sql, dialect: :generic)
58
+ JSON.parse(_parse_one(sql, dialect.to_s))
59
+ end
60
+
61
+ # Generate SQL from an AST expression.
62
+ #
63
+ # @param ast [Hash, String] the AST expression (Hash or JSON string)
64
+ # @param dialect [String, Symbol] the target SQL dialect (default: :generic)
65
+ # @return [String] the generated SQL
66
+ #
67
+ # @example
68
+ # ast = Polyglot.parse_one("SELECT 1", dialect: :postgres)
69
+ # Polyglot.generate(ast, dialect: :mysql)
70
+ #
71
+ def generate(ast, dialect: :generic)
72
+ json = ast.is_a?(String) ? ast : JSON.generate(ast)
73
+ _generate(json, dialect.to_s)
74
+ end
75
+
76
+ # Format (pretty-print) SQL.
77
+ #
78
+ # @param sql [String] the SQL to format
79
+ # @param dialect [String, Symbol] the SQL dialect (default: :generic)
80
+ # @return [String] the formatted SQL
81
+ #
82
+ # @example
83
+ # Polyglot.format("SELECT a, b FROM t WHERE x = 1", dialect: :postgres)
84
+ #
85
+ def format(sql, dialect: :generic)
86
+ _format(sql, dialect.to_s)
87
+ end
88
+
89
+ # Validate SQL syntax.
90
+ #
91
+ # @param sql [String] the SQL to validate
92
+ # @param dialect [String, Symbol] the SQL dialect (default: :generic)
93
+ # @return [Polyglot::ValidationResult]
94
+ #
95
+ # @example
96
+ # result = Polyglot.validate("SELECT 1", dialect: :postgres)
97
+ # result.valid? # => true
98
+ #
99
+ # @example Invalid SQL
100
+ # result = Polyglot.validate("SELEC 1")
101
+ # result.valid? # => false
102
+ # result.errors.first.message # => "..."
103
+ #
104
+ def validate(sql, dialect: :generic)
105
+ data = JSON.parse(_validate(sql, dialect.to_s))
106
+ ValidationResult.new(data)
107
+ end
108
+ end
109
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: polyglot-sql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Polyglot Contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake-compiler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.12'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.12'
68
+ description: SQL dialect translator supporting 30+ databases — Ruby bindings via Magnus
69
+ email: []
70
+ executables: []
71
+ extensions:
72
+ - ext/polyglot_rb/extconf.rb
73
+ extra_rdoc_files: []
74
+ files:
75
+ - Cargo.toml
76
+ - LICENSE
77
+ - README.md
78
+ - ext/polyglot_rb/Cargo.toml
79
+ - ext/polyglot_rb/extconf.rb
80
+ - ext/polyglot_rb/src/dialect.rs
81
+ - ext/polyglot_rb/src/errors.rs
82
+ - ext/polyglot_rb/src/lib.rs
83
+ - lib/polyglot.rb
84
+ - lib/polyglot/validation_result.rb
85
+ - lib/polyglot/version.rb
86
+ homepage: https://github.com/catkins/polyglot-sql-rb
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://github.com/catkins/polyglot-sql-rb
91
+ source_code_uri: https://github.com/catkins/polyglot-sql-rb
92
+ changelog_uri: https://github.com/catkins/polyglot-sql-rb/blob/main/CHANGELOG.md
93
+ rubygems_mfa_required: 'true'
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 3.2.0
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 4.0.4
109
+ specification_version: 4
110
+ summary: Ruby bindings for polyglot-sql
111
+ test_files: []