abstract-sql 0.0.1
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.
- data/.gitignore +6 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.markdown +23 -0
- data/Rakefile +24 -0
- data/abstract-sql.gemspec +25 -0
- data/lib/abstract-sql.rb +23 -0
- data/lib/abstract-sql/parser/statement.rb +63 -0
- data/lib/abstract-sql/transform/statement.rb +51 -0
- data/lib/abstract-sql/version.rb +5 -0
- data/test/helper.rb +12 -0
- data/test/test_abstract.rb +42 -0
- metadata +103 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create ruby-1.9.2@abstract-sql
|
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Abstract SQL
|
2
|
+
|
3
|
+
You ca use this project to transform a SQL string into a Perl SQL::Abstract statement format (as a Hash)
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
All you need to do is to add this line in your Gemfile
|
8
|
+
|
9
|
+
gem 'abstract-sql'
|
10
|
+
|
11
|
+
Then use it like this :
|
12
|
+
|
13
|
+
statement = "(id = 1 AND label like '%webo%') OR (id !=1 and label like '%api%')"
|
14
|
+
abstract = SQL::Abstract.new
|
15
|
+
abstract.parse statement
|
16
|
+
#=> {
|
17
|
+
:"-or"=>
|
18
|
+
[{:"-and"=>[{:"-="=>{:id=>1}}, {:"-like"=>{:label=>"%webo%"}}]},
|
19
|
+
{:"-and"=>[{:"-!="=>{:id=>1}}, {:"-like"=>{:label=>"%api%"}}]}
|
20
|
+
]
|
21
|
+
}
|
22
|
+
|
23
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
Rake::TestTask.new(:test) do |test|
|
5
|
+
test.libs << 'lib' << 'test'
|
6
|
+
test.pattern = 'test/**/test_*.rb'
|
7
|
+
test.verbose = true
|
8
|
+
test.options = '--verbose --use-color'
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'rcov/rcovtask'
|
13
|
+
Rcov::RcovTask.new do |test|
|
14
|
+
test.libs << 'test'
|
15
|
+
test.pattern = 'test/**/test_*.rb'
|
16
|
+
test.verbose = true
|
17
|
+
test.rcov_opts += ["--exclude /gems/"]
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "Install rcov gem : `gem install rcov`"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Default task"
|
24
|
+
task :default => "test"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "abstract-sql/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "abstract-sql"
|
7
|
+
s.version = Abstract::Sql::VERSION
|
8
|
+
s.authors = ["Hallelujah"]
|
9
|
+
s.email = ["hery@rails-royce.org"]
|
10
|
+
s.homepage = "https://github.com/hallelujah/sql-abstract"
|
11
|
+
s.summary = %q{Transform a SQL statement to Perl SQL::Abstract JSON format}
|
12
|
+
s.description = %q{It reverse an SQL statement to a Perl SQL::Abstract JSON format.}
|
13
|
+
|
14
|
+
|
15
|
+
s.add_dependency 'parslet'
|
16
|
+
s.add_development_dependency 'test-unit', '>= 2.1.0'
|
17
|
+
s.add_development_dependency 'rcov'
|
18
|
+
s.add_development_dependency 'rake'
|
19
|
+
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split("\n")
|
22
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
end
|
data/lib/abstract-sql.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "abstract-sql/version"
|
2
|
+
require "abstract-sql/parser/statement"
|
3
|
+
require "abstract-sql/transform/statement"
|
4
|
+
|
5
|
+
module SQL
|
6
|
+
class Abstract
|
7
|
+
|
8
|
+
attr_reader :parser, :transformer
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@parser = Parser::Statement.new
|
12
|
+
@transformer = Transform::Statement.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse(sql)
|
16
|
+
tree = @parser.parse(sql)
|
17
|
+
while tree != (t = @transformer.apply(tree) )
|
18
|
+
tree = t
|
19
|
+
end
|
20
|
+
t
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
module SQL
|
3
|
+
module Parser
|
4
|
+
class Statement < Parslet::Parser
|
5
|
+
# At least one space character (space, tab, new line, carriage return)
|
6
|
+
rule(:spaces) { match('\s').repeat(1) }
|
7
|
+
rule(:lparen){ spaces? >> str('(') >> spaces?}
|
8
|
+
rule(:rparen){ spaces? >> str(')') >> spaces?}
|
9
|
+
rule(:spaces?){ spaces.maybe}
|
10
|
+
rule(:digit) { match('[0-9]') }
|
11
|
+
rule(:int){ digit.repeat(1).as(:int) }
|
12
|
+
rule(:float) do
|
13
|
+
(str('-').maybe >> (
|
14
|
+
str('0') | (match('[1-9]') >> digit.repeat)
|
15
|
+
) >> (
|
16
|
+
str('.') >> digit.repeat(1)
|
17
|
+
) >> (
|
18
|
+
match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
|
19
|
+
).maybe
|
20
|
+
).as(:float)
|
21
|
+
end
|
22
|
+
|
23
|
+
rule(:string) do
|
24
|
+
(str('"') >> (str('\\') >> any | str('"').absent? >> any).repeat.as(:string) >> str('"')) |
|
25
|
+
(str('\'') >> (str('\\') >> any | str('\'').absent? >> any ).repeat.as(:string) >> str('\''))
|
26
|
+
end
|
27
|
+
|
28
|
+
rule(:boolean_operator) do
|
29
|
+
str('and') | str('or') | str('AND') | str('OR')
|
30
|
+
end
|
31
|
+
|
32
|
+
rule(:operator) do
|
33
|
+
(str('!=') | str('like') | str('LIKE') | str('=') | str('<=') | str('>=') | str('<') | str('>')).as(:operator)
|
34
|
+
end
|
35
|
+
|
36
|
+
rule(:column) do
|
37
|
+
match('[-_0-9a-zA-Z]').repeat.as(:column)
|
38
|
+
end
|
39
|
+
|
40
|
+
rule(:value) do
|
41
|
+
spaces? >> (val | column) >> spaces?
|
42
|
+
end
|
43
|
+
|
44
|
+
rule(:val) do
|
45
|
+
(string | int | float).as(:val)
|
46
|
+
end
|
47
|
+
|
48
|
+
rule(:entity) do
|
49
|
+
(value.as(:lvalue) >> operator >> value.as(:rvalue))
|
50
|
+
end
|
51
|
+
|
52
|
+
rule(:statement) do
|
53
|
+
(lparen >> (top | statement | entity) >> rparen) | entity
|
54
|
+
end
|
55
|
+
|
56
|
+
rule(:top) do
|
57
|
+
statement.as(:lstatement) >> (boolean_operator.as(:boolean_operator) >> top.as(:rstatement)) | statement.as(:lstatement)
|
58
|
+
end
|
59
|
+
|
60
|
+
root(:top)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
module SQL
|
3
|
+
module Transform
|
4
|
+
class Statement < Parslet::Transform
|
5
|
+
rule(:val => simple(:x)){ x }
|
6
|
+
rule(:val =>{:float => simple(:x)}){ String(x).to_f }
|
7
|
+
rule(:val =>{:int => simple(:x)}){ String(x).to_i }
|
8
|
+
rule(:val =>{:string => simple(:x)}){ String(x) }
|
9
|
+
# rule(:boolean_operator => simple(:x)){{ :boolean_operator => String(x).downcase.to_sym }}
|
10
|
+
rule(:lvalue => {:column => simple(:x) }, :operator => simple(:op), :rvalue => {:column => simple(:z)}){ {operator(op) => { String(x).to_sym => String(z).to_sym } } }
|
11
|
+
rule(:lvalue => {:column => simple(:x) }, :operator => simple(:op), :rvalue => simple(:z) ){ {operator(op) => { String(x).to_sym => z } } }
|
12
|
+
rule(:lvalue => simple(:x) , :operator => simple(:op), :rvalue => {:column => simple(:z)}){ {operator(op) => { String(z).to_sym => x } } }
|
13
|
+
|
14
|
+
# Statement right or left without right leaf
|
15
|
+
rule(:lstatement => { :lstatement => subtree(:y) }){ {:lstatement => y }}
|
16
|
+
rule(:rstatement => { :lstatement => subtree(:y) }){ {:rstatement => y}}
|
17
|
+
|
18
|
+
rule({:lstatement=>{:lstatement=> subtree(:l)},
|
19
|
+
:boolean_operator => simple(:op),
|
20
|
+
:rstatement=>{:lstatement => subtree(:r)}}) do {:lstatement => l, :boolean_operator => op, :rstatement => r} end
|
21
|
+
|
22
|
+
# Statement right or left without right leaf and a boolean operator
|
23
|
+
rule(:lstatement => { :lstatement => subtree(:y) }){ {:lstatement => y }}
|
24
|
+
rule(:rstatement => { :lstatement => subtree(:y) }){ {:rstatement => y}}
|
25
|
+
|
26
|
+
rule({:lstatement => subtree(:l),
|
27
|
+
:boolean_operator=> simple(:op),
|
28
|
+
:rstatement=>{:lstatement=>subtree(:r)}}) do {:lstatement => l, :boolean_operator => operator(op), :rstatement => r} end
|
29
|
+
|
30
|
+
rule({:rstatement => subtree(:r),
|
31
|
+
:boolean_operator=> simple(:op),
|
32
|
+
:lstatement=>{:lstatement=>subtree(:l)}}) do {:lstatement => l, :boolean_operator => operator(op), :rstatement => r} end
|
33
|
+
|
34
|
+
|
35
|
+
rule(:boolean_operator => simple(:op), :lstatement => subtree(:l), :rstatement => subtree(:r) ) { {operator(op) => [l, r] } }
|
36
|
+
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Parslet::Pattern::Context
|
43
|
+
def operator(op)
|
44
|
+
if op.is_a?(Symbol)
|
45
|
+
op
|
46
|
+
else
|
47
|
+
( "-" + String(op) ).downcase.to_sym
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.setup
|
5
|
+
require 'test/unit'
|
6
|
+
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
9
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
10
|
+
require 'abstract-sql'
|
11
|
+
class Test::Unit::TestCase
|
12
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestAbstract < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@abstract = SQL::Abstract.new
|
7
|
+
end
|
8
|
+
|
9
|
+
test "parse a simple condition" do
|
10
|
+
assert_equal( {:"-and" => [{:"-=" => { :id => 1}}, {:"-like" => {:label => '%webo%'}}]}, @abstract.parse("id = 1 and label like '%webo%'"))
|
11
|
+
assert_equal( {:"-and" => [{:"-=" => { :id => 1}}, {:"-like" => {:label => '%webo%'}}]}, @abstract.parse("((id = 1 )) and label like '%webo%'"))
|
12
|
+
end
|
13
|
+
|
14
|
+
test "parse a complex condition" do
|
15
|
+
assert_equal(
|
16
|
+
{
|
17
|
+
:"-or" => [
|
18
|
+
{:"-and" => [{:"-=" => { :id => 1}}, {:"-like" => {:label => '%webo%'}}]},
|
19
|
+
{:"-and" => [{:"-!=" => { :id => 1}}, {:"-like" => {:label => '%api%'}}]}
|
20
|
+
]
|
21
|
+
},
|
22
|
+
@abstract.parse("(id = 1 AND label like '%webo%') OR (id !=1 and label like '%api%')")
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
test "raise error when bad formatted sql" do
|
27
|
+
assert_raise Parslet::ParseFailed do
|
28
|
+
# No AND
|
29
|
+
@abstract.parse("(id = 1 label like '%webo%')")
|
30
|
+
end
|
31
|
+
assert_raise Parslet::ParseFailed do
|
32
|
+
@abstract.parse("label liike '%webo%'")
|
33
|
+
end
|
34
|
+
assert_raise Parslet::UnconsumedInput do
|
35
|
+
@abstract.parse("id = 1 ET label like '%webo%'")
|
36
|
+
end
|
37
|
+
assert_raise Parslet::UnconsumedInput do
|
38
|
+
@abstract.parse("(id = 1 AND label like '%webo%') OR (id !=1 and label like '%api%'")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: abstract-sql
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Hallelujah
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-08-01 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: parslet
|
16
|
+
requirement: &9785380 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *9785380
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: test-unit
|
27
|
+
requirement: &9784880 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.1.0
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *9784880
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rcov
|
38
|
+
requirement: &9784460 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *9784460
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &9784000 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *9784000
|
58
|
+
description: It reverse an SQL statement to a Perl SQL::Abstract JSON format.
|
59
|
+
email:
|
60
|
+
- hery@rails-royce.org
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- .gitignore
|
66
|
+
- .rvmrc
|
67
|
+
- Gemfile
|
68
|
+
- README.markdown
|
69
|
+
- Rakefile
|
70
|
+
- abstract-sql.gemspec
|
71
|
+
- lib/abstract-sql.rb
|
72
|
+
- lib/abstract-sql/parser/statement.rb
|
73
|
+
- lib/abstract-sql/transform/statement.rb
|
74
|
+
- lib/abstract-sql/version.rb
|
75
|
+
- test/helper.rb
|
76
|
+
- test/test_abstract.rb
|
77
|
+
homepage: https://github.com/hallelujah/sql-abstract
|
78
|
+
licenses: []
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 1.8.6
|
98
|
+
signing_key:
|
99
|
+
specification_version: 3
|
100
|
+
summary: Transform a SQL statement to Perl SQL::Abstract JSON format
|
101
|
+
test_files:
|
102
|
+
- test/helper.rb
|
103
|
+
- test/test_abstract.rb
|