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 +7 -0
- data/Cargo.toml +9 -0
- data/LICENSE +21 -0
- data/README.md +136 -0
- data/ext/polyglot_rb/Cargo.toml +17 -0
- data/ext/polyglot_rb/extconf.rb +6 -0
- data/ext/polyglot_rb/src/dialect.rs +199 -0
- data/ext/polyglot_rb/src/errors.rs +100 -0
- data/ext/polyglot_rb/src/lib.rs +108 -0
- data/lib/polyglot/validation_result.rb +61 -0
- data/lib/polyglot/version.rb +5 -0
- data/lib/polyglot.rb +109 -0
- metadata +111 -0
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
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
|
+
[](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,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
|
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: []
|