finox 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +25 -1
- data/Cargo.lock +60 -10
- data/LICENSE-THIRD-PARTY.txt +1095 -0
- data/README.md +89 -17
- data/ext/finox/Cargo.toml +4 -3
- data/ext/finox/src/lib.rs +362 -3
- data/lib/finox/version.rb +1 -1
- metadata +2 -3
- data/.rspec +0 -3
- data/.rubocop.yml +0 -8
data/README.md
CHANGED
|
@@ -2,44 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
A MySQL query parser for Ruby, powered by [sqlparser-rs](https://github.com/apache/datafusion-sqlparser-rs).
|
|
4
4
|
|
|
5
|
-
Finox parses SQL with Rust and
|
|
5
|
+
Finox parses SQL with Rust and exposes tables, columns, fingerprints and the raw AST.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
-
Add to your Gemfile:
|
|
10
|
-
|
|
11
9
|
```bash
|
|
12
10
|
bundle add finox
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
Or
|
|
13
|
+
Or without Bundler:
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
16
|
gem install finox
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
Precompiled native gems are available for Ruby 3.1–4.0 on the following
|
|
20
|
+
platforms, so installation there requires no Rust toolchain:
|
|
21
|
+
|
|
22
|
+
| OS | Platforms |
|
|
23
|
+
| ------- | ------------------------------------------------ |
|
|
24
|
+
| Linux | x86_64, aarch64 (glibc and musl variants) |
|
|
25
|
+
| macOS | x86_64, arm64 |
|
|
26
|
+
| Windows | x64-mingw-ucrt |
|
|
27
|
+
|
|
28
|
+
On any other platform the source gem is installed instead, and compiling it
|
|
29
|
+
requires a Rust toolchain.
|
|
22
30
|
|
|
23
31
|
## Usage
|
|
24
32
|
|
|
33
|
+
`Finox.parse` returns a `Finox::Result`:
|
|
34
|
+
|
|
25
35
|
```ruby
|
|
26
36
|
require "finox"
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
# =>
|
|
38
|
+
result = Finox.parse("SELECT `name` FROM `users` WHERE id = 1")
|
|
39
|
+
# => #<Finox::Result>
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
# => [
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# span: {start: {line: 1, column: 8}, end: {line: 1, column: 14}}}}}]
|
|
41
|
+
result.tables # => ["users"]
|
|
42
|
+
result.columns # => ["name", "id"]
|
|
43
|
+
result.statement_types # => ["Query"]
|
|
44
|
+
result.normalize # => "SELECT `name` FROM `users` WHERE id = ?"
|
|
45
|
+
result.fingerprint # => "24e307ca0f02abfc"
|
|
37
46
|
```
|
|
38
47
|
|
|
39
|
-
`Finox.parse` returns one Hash per statement, so `"SELECT 1; SELECT 2"` yields an array of two.
|
|
40
|
-
|
|
41
|
-
Key convention (from serde's externally tagged enums): enum variant names are `String` keys (`"Query"`, `"Select"`, `"Identifier"`), struct fields are `Symbol` keys (`:body`, `:projection`, `:value`).
|
|
42
|
-
|
|
43
48
|
Invalid SQL raises `Finox::ParseError`:
|
|
44
49
|
|
|
45
50
|
```ruby
|
|
@@ -47,6 +52,69 @@ Finox.parse("SELEKT 1")
|
|
|
47
52
|
# => Finox::ParseError: sql parser error: Expected: an SQL statement, found: SELEKT at Line: 1, Column: 1
|
|
48
53
|
```
|
|
49
54
|
|
|
55
|
+
### Table classification
|
|
56
|
+
|
|
57
|
+
`#select_tables`, `#dml_tables` and `#ddl_tables` classify the referenced
|
|
58
|
+
tables by how they are used: read, written by DML (`INSERT` / `UPDATE` /
|
|
59
|
+
`DELETE`) or targeted by DDL (`CREATE TABLE` / `ALTER TABLE` / `DROP TABLE` /
|
|
60
|
+
`TRUNCATE` etc.). A table appearing in multiple roles is listed in each.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
result = Finox.parse("INSERT INTO logs SELECT message FROM events; DROP TABLE archives")
|
|
64
|
+
|
|
65
|
+
result.tables # => ["logs", "events", "archives"]
|
|
66
|
+
result.select_tables # => ["events"]
|
|
67
|
+
result.dml_tables # => ["logs"]
|
|
68
|
+
result.ddl_tables # => ["archives"]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Known limitations
|
|
72
|
+
|
|
73
|
+
Extraction is syntactic, with no schema knowledge and only coarse scope
|
|
74
|
+
resolution:
|
|
75
|
+
|
|
76
|
+
- A CTE name shadows a same-named real table everywhere in the statement,
|
|
77
|
+
even inside the CTE's own definition.
|
|
78
|
+
- Multi-table `UPDATE` (`UPDATE t1 JOIN t2 ... SET t2.x = 1`) reports only
|
|
79
|
+
`t1` in `#dml_tables`.
|
|
80
|
+
- `#columns` does not resolve table aliases (`u.id` stays `u.id`).
|
|
81
|
+
|
|
82
|
+
### Normalization and fingerprints
|
|
83
|
+
|
|
84
|
+
`#normalize` replaces literals with `?` placeholders and deparses from the
|
|
85
|
+
AST, so formatting and keyword case are normalized as well. `#fingerprint` is
|
|
86
|
+
a 64-bit hex hash (xxhash64) of the normalized SQL — stable across literal
|
|
87
|
+
and formatting differences, but not guaranteed to be stable across finox
|
|
88
|
+
versions, since the normalized form depends on the bundled sqlparser-rs.
|
|
89
|
+
Recompute stored fingerprints when upgrading finox.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
Finox.parse("select * from users where id=1").normalize
|
|
93
|
+
# => "SELECT * FROM users WHERE id = ?"
|
|
94
|
+
|
|
95
|
+
Finox.parse("select * from users where id=1").fingerprint ==
|
|
96
|
+
Finox.parse("SELECT * FROM users WHERE id = 42").fingerprint
|
|
97
|
+
# => true
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Raw sqlparser-rs AST
|
|
101
|
+
|
|
102
|
+
For anything not covered above, `#statements` exposes sqlparser-rs's raw AST of each parsed statement as plain
|
|
103
|
+
Ruby Hashes and Arrays:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
ast = Finox.parse("SELECT `name` FROM `users` WHERE id = 1").statements.first
|
|
107
|
+
# => {"Query" => {"with" => nil, "body" => {"Select" => {...}}, ...}}
|
|
108
|
+
|
|
109
|
+
ast.dig("Query", "body", "Select", "projection")
|
|
110
|
+
# => [{"UnnamedExpr" =>
|
|
111
|
+
# {"Identifier" =>
|
|
112
|
+
# {"value" => "name",
|
|
113
|
+
# "quote_style" => "`",
|
|
114
|
+
# "span" => {"start" => {"line" => 1, "column" => 8},
|
|
115
|
+
# "end" => {"line" => 1, "column" => 14}}}}}]
|
|
116
|
+
```
|
|
117
|
+
|
|
50
118
|
## Development
|
|
51
119
|
|
|
52
120
|
```bash
|
|
@@ -58,3 +126,7 @@ bundle exec rake # compile + spec + rubocop
|
|
|
58
126
|
## License
|
|
59
127
|
|
|
60
128
|
[MIT](LICENSE.txt)
|
|
129
|
+
|
|
130
|
+
Precompiled gems statically link Rust crates, including sqlparser-rs
|
|
131
|
+
(Apache-2.0). See [LICENSE-THIRD-PARTY.txt](LICENSE-THIRD-PARTY.txt) for
|
|
132
|
+
their licenses and attributions.
|
data/ext/finox/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "finox"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
authors = ["kyuuri1791"]
|
|
6
6
|
license = "MIT"
|
|
@@ -11,5 +11,6 @@ crate-type = ["cdylib"]
|
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
13
|
magnus = "0.8"
|
|
14
|
-
|
|
15
|
-
sqlparser = { version = "0.62.0", features = ["serde"] }
|
|
14
|
+
serde_json = { version = "1", features = ["preserve_order"] }
|
|
15
|
+
sqlparser = { version = "0.62.0", features = ["serde", "visitor"] }
|
|
16
|
+
twox-hash = { version = "2.1", default-features = false, features = ["xxhash64"] }
|
data/ext/finox/src/lib.rs
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
use
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
use std::fmt::{self, Write as _};
|
|
3
|
+
use std::ops::ControlFlow;
|
|
4
|
+
|
|
5
|
+
use magnus::{
|
|
6
|
+
function, method, prelude::*, value::Lazy, Error, ExceptionClass, IntoValue, Ruby, Value,
|
|
7
|
+
};
|
|
8
|
+
use sqlparser::ast::{
|
|
9
|
+
visit_expressions_mut, AssignmentTarget, Expr, FromTable, ObjectName, ObjectType, Query,
|
|
10
|
+
Statement, TableFactor, TableObject, TableWithJoins, Value as SqlValue, Visit, Visitor,
|
|
11
|
+
};
|
|
2
12
|
use sqlparser::{dialect::MySqlDialect, parser::Parser};
|
|
13
|
+
use twox_hash::XxHash64;
|
|
3
14
|
|
|
4
15
|
static PARSE_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
5
16
|
ruby.define_module("Finox")
|
|
@@ -8,16 +19,364 @@ static PARSE_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
|
8
19
|
.unwrap()
|
|
9
20
|
});
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
#[magnus::wrap(class = "Finox::Result", free_immediately, size)]
|
|
23
|
+
struct ParseResult {
|
|
24
|
+
statements: Vec<Statement>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl ParseResult {
|
|
28
|
+
fn statements(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
29
|
+
let json = serde_json::to_value(&rb_self.statements)
|
|
30
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
31
|
+
json_to_ruby(ruby, &json)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn tables(&self) -> Vec<String> {
|
|
35
|
+
dedup(self.statements.iter().flat_map(collect_tables).collect())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn select_tables(&self) -> Vec<String> {
|
|
39
|
+
dedup(self.statements.iter().flat_map(collect_select_tables).collect())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn dml_tables(&self) -> Vec<String> {
|
|
43
|
+
dedup(self.statements.iter().flat_map(collect_dml_tables).collect())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn ddl_tables(&self) -> Vec<String> {
|
|
47
|
+
dedup(self.statements.iter().flat_map(collect_ddl_tables).collect())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn columns(&self) -> Vec<String> {
|
|
51
|
+
collect_columns(&self.statements)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn statement_types(&self) -> Vec<String> {
|
|
55
|
+
self.statements.iter().map(statement_type).collect()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn normalize(&self) -> String {
|
|
59
|
+
self.statements
|
|
60
|
+
.iter()
|
|
61
|
+
.map(normalize_statement)
|
|
62
|
+
.collect::<Vec<_>>()
|
|
63
|
+
.join("; ")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn fingerprint(&self) -> String {
|
|
67
|
+
fingerprint(&self.normalize())
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn fingerprint(sql: &str) -> String {
|
|
72
|
+
format!("{:016x}", XxHash64::oneshot(0, sql.as_bytes()))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn json_to_ruby(ruby: &Ruby, value: &serde_json::Value) -> Result<Value, Error> {
|
|
76
|
+
use serde_json::Value as Json;
|
|
77
|
+
|
|
78
|
+
Ok(match value {
|
|
79
|
+
Json::Null => ruby.qnil().as_value(),
|
|
80
|
+
Json::Bool(boolean) => (*boolean).into_value_with(ruby),
|
|
81
|
+
Json::Number(number) => {
|
|
82
|
+
if let Some(int) = number.as_i64() {
|
|
83
|
+
int.into_value_with(ruby)
|
|
84
|
+
} else if let Some(int) = number.as_u64() {
|
|
85
|
+
int.into_value_with(ruby)
|
|
86
|
+
} else {
|
|
87
|
+
number.as_f64().into_value_with(ruby)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
Json::String(string) => string.as_str().into_value_with(ruby),
|
|
91
|
+
Json::Array(items) => {
|
|
92
|
+
let array = ruby.ary_new_capa(items.len());
|
|
93
|
+
for item in items {
|
|
94
|
+
array.push(json_to_ruby(ruby, item)?)?;
|
|
95
|
+
}
|
|
96
|
+
array.as_value()
|
|
97
|
+
}
|
|
98
|
+
Json::Object(map) => {
|
|
99
|
+
let hash = ruby.hash_new();
|
|
100
|
+
for (key, item) in map {
|
|
101
|
+
hash.aset(key.as_str(), json_to_ruby(ruby, item)?)?;
|
|
102
|
+
}
|
|
103
|
+
hash.as_value()
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn normalize_statement(statement: &Statement) -> String {
|
|
109
|
+
let mut statement = statement.clone();
|
|
110
|
+
let _ = visit_expressions_mut(&mut statement, |expr| {
|
|
111
|
+
if let Expr::Value(value) = expr {
|
|
112
|
+
value.value = SqlValue::Placeholder("?".to_string());
|
|
113
|
+
}
|
|
114
|
+
ControlFlow::<()>::Continue(())
|
|
115
|
+
});
|
|
116
|
+
statement.to_string()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn statement_type(statement: &Statement) -> String {
|
|
120
|
+
struct VariantName(String);
|
|
121
|
+
|
|
122
|
+
impl fmt::Write for VariantName {
|
|
123
|
+
fn write_str(&mut self, s: &str) -> fmt::Result {
|
|
124
|
+
for ch in s.chars() {
|
|
125
|
+
if ch.is_alphanumeric() || ch == '_' {
|
|
126
|
+
self.0.push(ch);
|
|
127
|
+
} else {
|
|
128
|
+
return Err(fmt::Error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
Ok(())
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let mut name = VariantName(String::new());
|
|
136
|
+
let _ = write!(name, "{statement:?}");
|
|
137
|
+
name.0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn table_info<T: Visit>(node: &T) -> TableCollector {
|
|
141
|
+
let mut collector = TableCollector::default();
|
|
142
|
+
let _ = node.visit(&mut collector);
|
|
143
|
+
collector
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn collect_tables<T: Visit>(node: &T) -> Vec<String> {
|
|
147
|
+
let info = table_info(node);
|
|
148
|
+
let mut names = info.relations;
|
|
149
|
+
names.extend(info.dml_targets);
|
|
150
|
+
names.extend(info.ddl_targets);
|
|
151
|
+
dedup(reject_cte_names(names, &info.cte_names))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn collect_select_tables<T: Visit>(node: &T) -> Vec<String> {
|
|
155
|
+
let info = table_info(node);
|
|
156
|
+
let mut targets = info.dml_targets;
|
|
157
|
+
targets.extend(info.ddl_targets);
|
|
158
|
+
|
|
159
|
+
let mut names = Vec::new();
|
|
160
|
+
for name in info.relations {
|
|
161
|
+
match targets.iter().position(|target| *target == name) {
|
|
162
|
+
Some(position) => {
|
|
163
|
+
targets.remove(position);
|
|
164
|
+
}
|
|
165
|
+
None => names.push(name),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
dedup(reject_cte_names(names, &info.cte_names))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn collect_dml_tables<T: Visit>(node: &T) -> Vec<String> {
|
|
172
|
+
dedup(table_info(node).dml_targets)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn collect_ddl_tables<T: Visit>(node: &T) -> Vec<String> {
|
|
176
|
+
dedup(table_info(node).ddl_targets)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn reject_cte_names(names: Vec<String>, cte_names: &HashSet<String>) -> Vec<String> {
|
|
180
|
+
names
|
|
181
|
+
.into_iter()
|
|
182
|
+
.filter(|name| !cte_names.contains(name))
|
|
183
|
+
.collect()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn collect_columns<T: Visit>(node: &T) -> Vec<String> {
|
|
187
|
+
let mut collector = ColumnCollector::default();
|
|
188
|
+
let _ = node.visit(&mut collector);
|
|
189
|
+
dedup(collector.columns)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn dedup(names: Vec<String>) -> Vec<String> {
|
|
193
|
+
let mut result = Vec::new();
|
|
194
|
+
for name in names {
|
|
195
|
+
if !result.contains(&name) {
|
|
196
|
+
result.push(name);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
result
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn object_name_to_string(name: &ObjectName) -> String {
|
|
203
|
+
name.0
|
|
204
|
+
.iter()
|
|
205
|
+
.map(|part| match part.as_ident() {
|
|
206
|
+
Some(ident) => ident.value.clone(),
|
|
207
|
+
None => part.to_string(),
|
|
208
|
+
})
|
|
209
|
+
.collect::<Vec<_>>()
|
|
210
|
+
.join(".")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn table_with_joins_relation(table: &TableWithJoins) -> Option<String> {
|
|
214
|
+
match &table.relation {
|
|
215
|
+
TableFactor::Table { name, .. } => Some(object_name_to_string(name)),
|
|
216
|
+
_ => None,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#[derive(Default)]
|
|
221
|
+
struct TableCollector {
|
|
222
|
+
relations: Vec<String>,
|
|
223
|
+
dml_targets: Vec<String>,
|
|
224
|
+
ddl_targets: Vec<String>,
|
|
225
|
+
cte_names: HashSet<String>,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
impl Visitor for TableCollector {
|
|
229
|
+
type Break = ();
|
|
230
|
+
|
|
231
|
+
fn pre_visit_relation(&mut self, relation: &ObjectName) -> ControlFlow<()> {
|
|
232
|
+
self.relations.push(object_name_to_string(relation));
|
|
233
|
+
ControlFlow::Continue(())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn pre_visit_query(&mut self, query: &Query) -> ControlFlow<()> {
|
|
237
|
+
if let Some(with) = &query.with {
|
|
238
|
+
for cte in &with.cte_tables {
|
|
239
|
+
self.cte_names.insert(cte.alias.name.value.clone());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
ControlFlow::Continue(())
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn pre_visit_statement(&mut self, statement: &Statement) -> ControlFlow<()> {
|
|
246
|
+
match statement {
|
|
247
|
+
Statement::Insert(insert) => {
|
|
248
|
+
if let TableObject::TableName(name) = &insert.table {
|
|
249
|
+
self.dml_targets.push(object_name_to_string(name));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
Statement::Update(update) => {
|
|
253
|
+
if let Some(name) = table_with_joins_relation(&update.table) {
|
|
254
|
+
self.dml_targets.push(name);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
Statement::Delete(delete) => {
|
|
258
|
+
if delete.tables.is_empty() {
|
|
259
|
+
let (FromTable::WithFromKeyword(from) | FromTable::WithoutKeyword(from)) =
|
|
260
|
+
&delete.from;
|
|
261
|
+
for table in from {
|
|
262
|
+
if let Some(name) = table_with_joins_relation(table) {
|
|
263
|
+
self.dml_targets.push(name);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
for name in &delete.tables {
|
|
268
|
+
self.dml_targets.push(object_name_to_string(name));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
Statement::CreateTable(create) => {
|
|
273
|
+
self.ddl_targets.push(object_name_to_string(&create.name));
|
|
274
|
+
}
|
|
275
|
+
Statement::AlterTable(alter) => {
|
|
276
|
+
self.ddl_targets.push(object_name_to_string(&alter.name));
|
|
277
|
+
}
|
|
278
|
+
Statement::CreateIndex(index) => {
|
|
279
|
+
self.ddl_targets
|
|
280
|
+
.push(object_name_to_string(&index.table_name));
|
|
281
|
+
}
|
|
282
|
+
Statement::Drop {
|
|
283
|
+
object_type: ObjectType::Table,
|
|
284
|
+
names,
|
|
285
|
+
..
|
|
286
|
+
} => {
|
|
287
|
+
for name in names {
|
|
288
|
+
self.ddl_targets.push(object_name_to_string(name));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
Statement::Truncate(truncate) => {
|
|
292
|
+
for target in &truncate.table_names {
|
|
293
|
+
self.ddl_targets.push(object_name_to_string(&target.name));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
Statement::RenameTable(renames) => {
|
|
297
|
+
for rename in renames {
|
|
298
|
+
self.ddl_targets.push(object_name_to_string(&rename.old_name));
|
|
299
|
+
self.ddl_targets.push(object_name_to_string(&rename.new_name));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
_ => {}
|
|
303
|
+
}
|
|
304
|
+
ControlFlow::Continue(())
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[derive(Default)]
|
|
309
|
+
struct ColumnCollector {
|
|
310
|
+
columns: Vec<String>,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
impl Visitor for ColumnCollector {
|
|
314
|
+
type Break = ();
|
|
315
|
+
|
|
316
|
+
fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
|
|
317
|
+
match expr {
|
|
318
|
+
Expr::Identifier(ident) => self.columns.push(ident.value.clone()),
|
|
319
|
+
Expr::CompoundIdentifier(idents) => self.columns.push(
|
|
320
|
+
idents
|
|
321
|
+
.iter()
|
|
322
|
+
.map(|ident| ident.value.clone())
|
|
323
|
+
.collect::<Vec<_>>()
|
|
324
|
+
.join("."),
|
|
325
|
+
),
|
|
326
|
+
_ => {}
|
|
327
|
+
}
|
|
328
|
+
ControlFlow::Continue(())
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fn pre_visit_statement(&mut self, statement: &Statement) -> ControlFlow<()> {
|
|
332
|
+
match statement {
|
|
333
|
+
Statement::Insert(insert) => {
|
|
334
|
+
for column in &insert.columns {
|
|
335
|
+
self.columns.push(object_name_to_string(column));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
Statement::Update(update) => {
|
|
339
|
+
for assignment in &update.assignments {
|
|
340
|
+
match &assignment.target {
|
|
341
|
+
AssignmentTarget::ColumnName(name) => {
|
|
342
|
+
self.columns.push(object_name_to_string(name));
|
|
343
|
+
}
|
|
344
|
+
AssignmentTarget::Tuple(names) => {
|
|
345
|
+
for name in names {
|
|
346
|
+
self.columns.push(object_name_to_string(name));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
_ => {}
|
|
353
|
+
}
|
|
354
|
+
ControlFlow::Continue(())
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn parse(ruby: &Ruby, sql: String) -> Result<ParseResult, Error> {
|
|
12
359
|
let statements = Parser::parse_sql(&MySqlDialect {}, &sql)
|
|
13
360
|
.map_err(|e| Error::new(ruby.get_inner(&PARSE_ERROR), e.to_string()))?;
|
|
14
|
-
|
|
361
|
+
Ok(ParseResult { statements })
|
|
15
362
|
}
|
|
16
363
|
|
|
17
364
|
#[magnus::init]
|
|
18
365
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
19
366
|
let module = ruby.define_module("Finox")?;
|
|
20
367
|
Lazy::force(&PARSE_ERROR, ruby);
|
|
368
|
+
|
|
369
|
+
let result = module.define_class("Result", ruby.class_object())?;
|
|
370
|
+
result.define_method("statements", method!(ParseResult::statements, 0))?;
|
|
371
|
+
result.define_method("tables", method!(ParseResult::tables, 0))?;
|
|
372
|
+
result.define_method("select_tables", method!(ParseResult::select_tables, 0))?;
|
|
373
|
+
result.define_method("dml_tables", method!(ParseResult::dml_tables, 0))?;
|
|
374
|
+
result.define_method("ddl_tables", method!(ParseResult::ddl_tables, 0))?;
|
|
375
|
+
result.define_method("columns", method!(ParseResult::columns, 0))?;
|
|
376
|
+
result.define_method("statement_types", method!(ParseResult::statement_types, 0))?;
|
|
377
|
+
result.define_method("normalize", method!(ParseResult::normalize, 0))?;
|
|
378
|
+
result.define_method("fingerprint", method!(ParseResult::fingerprint, 0))?;
|
|
379
|
+
|
|
21
380
|
module.define_singleton_method("parse", function!(parse, 1))?;
|
|
22
381
|
Ok(())
|
|
23
382
|
}
|
data/lib/finox/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: finox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kyuuri1791
|
|
@@ -30,11 +30,10 @@ extensions:
|
|
|
30
30
|
- ext/finox/extconf.rb
|
|
31
31
|
extra_rdoc_files: []
|
|
32
32
|
files:
|
|
33
|
-
- ".rspec"
|
|
34
|
-
- ".rubocop.yml"
|
|
35
33
|
- CHANGELOG.md
|
|
36
34
|
- Cargo.lock
|
|
37
35
|
- Cargo.toml
|
|
36
|
+
- LICENSE-THIRD-PARTY.txt
|
|
38
37
|
- LICENSE.txt
|
|
39
38
|
- README.md
|
|
40
39
|
- Rakefile
|
data/.rspec
DELETED