nql 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 +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/lib/nql.rb +15 -0
- data/lib/nql/grammar.rb +761 -0
- data/lib/nql/grammar.treetop +109 -0
- data/lib/nql/version.rb +3 -0
- data/nql.gemspec +25 -0
- data/spec/comparison_parser_spec.rb +147 -0
- data/spec/coordination_parser_spec.rb +43 -0
- data/spec/migrations/20121108154439_create_countries.rb +9 -0
- data/spec/migrations/20121108154508_create_cities.rb +10 -0
- data/spec/models/city.rb +3 -0
- data/spec/models/country.rb +3 -0
- data/spec/ransack_spec.rb +135 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/sql_spec.rb +93 -0
- metadata +139 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
module NQL
|
2
|
+
grammar Syntax
|
3
|
+
|
4
|
+
rule expression
|
5
|
+
boolean / primary
|
6
|
+
end
|
7
|
+
|
8
|
+
rule boolean
|
9
|
+
left:primary space coordinator:coordinator space right:expression {
|
10
|
+
def to_ransack
|
11
|
+
group = {'g' => [{'m' => coordinator.to_ransack}]}
|
12
|
+
|
13
|
+
[left, right].each do |side|
|
14
|
+
if side.is_node?(:boolean)
|
15
|
+
group['g'][0].merge! side.to_ransack
|
16
|
+
else
|
17
|
+
group['g'][0]['c'] ||= []
|
18
|
+
group['g'][0]['c'] << side.to_ransack
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
group
|
23
|
+
end
|
24
|
+
|
25
|
+
def is_node?(node_type)
|
26
|
+
node_type.to_sym == :boolean
|
27
|
+
end
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
rule primary
|
32
|
+
(space comparison space / '(' space expression space ')') {
|
33
|
+
def to_ransack
|
34
|
+
detect_node.to_ransack
|
35
|
+
end
|
36
|
+
|
37
|
+
def detect_node
|
38
|
+
self.send %w(comparison expression).detect { |m| self.respond_to? m }
|
39
|
+
end
|
40
|
+
|
41
|
+
def is_node?(node_type)
|
42
|
+
detect_node.is_node?(node_type)
|
43
|
+
end
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
rule coordinator
|
48
|
+
('|' / '&') {
|
49
|
+
def to_ransack
|
50
|
+
coordinators = {'|' => 'or', '&' => 'and'}
|
51
|
+
coordinators[text_value]
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
rule comparison
|
57
|
+
variable:alphanumeric space comparator:comparator space value:text {
|
58
|
+
def to_ransack
|
59
|
+
hash = {'a' => {'0' => {'name' => self.variable.text_value.gsub('.', '_')}}, 'p' => self.comparator.to_ransack, 'v' => {'0' => {'value' => self.value.text_value}}}
|
60
|
+
hash = {'c' => [hash]} if !parent || !parent.parent || text_value == parent.parent.text_value
|
61
|
+
hash
|
62
|
+
end
|
63
|
+
|
64
|
+
def is_node?(node_type)
|
65
|
+
node_type.to_sym == :comparison
|
66
|
+
end
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
rule comparator
|
71
|
+
('=' / '!=' / '>' / '>=' / '<' / '<=' / '%')+ {
|
72
|
+
def to_ransack
|
73
|
+
comparators = {
|
74
|
+
'=' => 'eq',
|
75
|
+
'!=' => 'not_eq',
|
76
|
+
'>' => 'gt',
|
77
|
+
'>=' => 'gteq',
|
78
|
+
'<' => 'lt',
|
79
|
+
'<=' => 'lteq',
|
80
|
+
'%' => 'cont'
|
81
|
+
}
|
82
|
+
comparators[text_value]
|
83
|
+
end
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
rule text
|
88
|
+
(alphanumeric / utf8 / symbol)+
|
89
|
+
(space (alphanumeric / utf8 / symbol)+)*
|
90
|
+
end
|
91
|
+
|
92
|
+
rule alphanumeric
|
93
|
+
[a-zA-Z0-9_.]+
|
94
|
+
end
|
95
|
+
|
96
|
+
rule space
|
97
|
+
' '*
|
98
|
+
end
|
99
|
+
|
100
|
+
rule symbol
|
101
|
+
[><=+-\/\\@#$%!?:]
|
102
|
+
end
|
103
|
+
|
104
|
+
rule utf8
|
105
|
+
[\u00c1\u00c0\u00c9\u00c8\u00cd\u00cc\u00d3\u00d2\u00da\u00d9\u00dc\u00d1\u00c7\u00e1\u00e0\u00e9\u00e8\u00ed\u00ec\u00f3\u00f2\u00fa\u00f9\u00fc\u00f1\u00e7]
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
data/lib/nql/version.rb
ADDED
data/nql.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/nql/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'nql'
|
6
|
+
s.version = NQL::VERSION
|
7
|
+
s.authors = ['Gabriel Naiman']
|
8
|
+
s.email = ['gabynaiman@gmail.com']
|
9
|
+
s.description = 'Natural Query Language built on top of ActiveRecord and Ransack'
|
10
|
+
s.summary = 'Natural Query Language built on top of ActiveRecord and Ransack'
|
11
|
+
s.homepage = 'https://github.com/gabynaiman/nql'
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split($\)
|
14
|
+
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_dependency 'treetop'
|
19
|
+
s.add_dependency 'activerecord', '>= 3.2.0'
|
20
|
+
s.add_dependency 'activesupport', '>= 3.2.0'
|
21
|
+
s.add_dependency 'ransack'
|
22
|
+
|
23
|
+
s.add_development_dependency 'sqlite3'
|
24
|
+
s.add_development_dependency 'rspec'
|
25
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe NQL::SyntaxParser, '-> Comparison' do
|
4
|
+
|
5
|
+
let(:parser) { NQL::SyntaxParser.new }
|
6
|
+
|
7
|
+
context 'Structure and comparators' do
|
8
|
+
|
9
|
+
it 'Equals' do
|
10
|
+
tree = parser.parse('var = value')
|
11
|
+
|
12
|
+
tree.comparison.variable.text_value.should eq 'var'
|
13
|
+
tree.comparison.comparator.text_value.should eq '='
|
14
|
+
tree.comparison.value.text_value.should eq 'value'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'Not equals' do
|
18
|
+
tree = parser.parse('var != value')
|
19
|
+
|
20
|
+
tree.comparison.variable.text_value.should eq 'var'
|
21
|
+
tree.comparison.comparator.text_value.should eq '!='
|
22
|
+
tree.comparison.value.text_value.should eq 'value'
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'Greater than' do
|
26
|
+
tree = parser.parse('var > value')
|
27
|
+
|
28
|
+
tree.comparison.variable.text_value.should eq 'var'
|
29
|
+
tree.comparison.comparator.text_value.should eq '>'
|
30
|
+
tree.comparison.value.text_value.should eq 'value'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'Greater or equals than' do
|
34
|
+
tree = parser.parse('var >= value')
|
35
|
+
|
36
|
+
tree.comparison.variable.text_value.should eq 'var'
|
37
|
+
tree.comparison.comparator.text_value.should eq '>='
|
38
|
+
tree.comparison.value.text_value.should eq 'value'
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'Less than' do
|
42
|
+
tree = parser.parse('var < value')
|
43
|
+
|
44
|
+
tree.comparison.variable.text_value.should eq 'var'
|
45
|
+
tree.comparison.comparator.text_value.should eq '<'
|
46
|
+
tree.comparison.value.text_value.should eq 'value'
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'Less or equals than' do
|
50
|
+
tree = parser.parse('var <= value')
|
51
|
+
|
52
|
+
tree.comparison.variable.text_value.should eq 'var'
|
53
|
+
tree.comparison.comparator.text_value.should eq '<='
|
54
|
+
tree.comparison.value.text_value.should eq 'value'
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'Contains' do
|
58
|
+
tree = parser.parse('var % value')
|
59
|
+
|
60
|
+
tree.comparison.variable.text_value.should eq 'var'
|
61
|
+
tree.comparison.comparator.text_value.should eq '%'
|
62
|
+
tree.comparison.value.text_value.should eq 'value'
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'Space separators' do
|
68
|
+
|
69
|
+
it 'Without spaces' do
|
70
|
+
tree = parser.parse('var=value')
|
71
|
+
|
72
|
+
tree.comparison.variable.text_value.should eq 'var'
|
73
|
+
tree.comparison.comparator.text_value.should eq '='
|
74
|
+
tree.comparison.value.text_value.should eq 'value'
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'With many spaces' do
|
78
|
+
tree = parser.parse('var = value')
|
79
|
+
|
80
|
+
tree.comparison.variable.text_value.should eq 'var'
|
81
|
+
tree.comparison.comparator.text_value.should eq '='
|
82
|
+
tree.comparison.value.text_value.should eq 'value'
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'Variable names' do
|
88
|
+
|
89
|
+
it 'With numbers' do
|
90
|
+
tree = parser.parse('var1 = value')
|
91
|
+
tree.comparison.variable.text_value.should eq 'var1'
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'With uppercase' do
|
95
|
+
tree = parser.parse('varName = value')
|
96
|
+
tree.comparison.variable.text_value.should eq 'varName'
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'With underscore' do
|
100
|
+
tree = parser.parse('var_name = value')
|
101
|
+
tree.comparison.variable.text_value.should eq 'var_name'
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'With dot' do
|
105
|
+
tree = parser.parse('var.name = value')
|
106
|
+
tree.comparison.variable.text_value.should eq 'var.name'
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'Values' do
|
112
|
+
|
113
|
+
it 'With numbers' do
|
114
|
+
tree = parser.parse('var = value1')
|
115
|
+
tree.comparison.value.text_value.should eq 'value1'
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'With uppercase' do
|
119
|
+
tree = parser.parse('var = valueDummy')
|
120
|
+
tree.comparison.value.text_value.should eq 'valueDummy'
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'With dot' do
|
124
|
+
tree = parser.parse('var = value.dummy')
|
125
|
+
tree.comparison.value.text_value.should eq 'value.dummy'
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
it 'With utf8 chars and symbols' do
|
130
|
+
utf8_symbols = "\u00c1\u00c0\u00c9\u00c8\u00cd\u00cc\u00d3\u00d2\u00da\u00d9\u00dc\u00d1\u00c7\u00e1\u00e0\u00e9\u00e8\u00ed\u00ec\u00f3\u00f2\u00fa\u00f9\u00fc\u00f1\u00e7"
|
131
|
+
tree = parser.parse("var = .#+-#{utf8_symbols}")
|
132
|
+
tree.comparison.value.text_value.should eq ".#+-#{utf8_symbols}"
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'With spaces' do
|
136
|
+
tree = parser.parse('var = value 123')
|
137
|
+
tree.comparison.value.text_value.should eq 'value 123'
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'With comparators, symbols and spaces' do
|
141
|
+
tree = parser.parse('var = value1 > value2 ! value3')
|
142
|
+
tree.comparison.value.text_value.should eq 'value1 > value2 ! value3'
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe NQL::SyntaxParser, '-> Coordination' do
|
4
|
+
|
5
|
+
let(:parser) { NQL::SyntaxParser.new }
|
6
|
+
|
7
|
+
it 'And' do
|
8
|
+
tree = parser.parse('var1 = value1 & var2 = value2')
|
9
|
+
|
10
|
+
tree.left.text_value.strip.should eq 'var1 = value1'
|
11
|
+
tree.coordinator.text_value.should eq '&'
|
12
|
+
tree.right.text_value.strip.should eq 'var2 = value2'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'Or' do
|
16
|
+
tree = parser.parse('var1 = value1 | var2 = value2')
|
17
|
+
|
18
|
+
tree.left.text_value.strip.should eq 'var1 = value1'
|
19
|
+
tree.coordinator.text_value.should eq '|'
|
20
|
+
tree.right.text_value.strip.should eq 'var2 = value2'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'And then Or' do
|
24
|
+
tree = parser.parse('var1 = value1 & var2 = value2 | var3 = value3')
|
25
|
+
|
26
|
+
tree.left.text_value.strip.should eq 'var1 = value1'
|
27
|
+
tree.coordinator.text_value.should eq '&'
|
28
|
+
tree.right.left.text_value.strip.should eq 'var2 = value2'
|
29
|
+
tree.right.coordinator.text_value.strip.should eq '|'
|
30
|
+
tree.right.right.text_value.strip.should eq 'var3 = value3'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'With parentheses' do
|
34
|
+
tree = parser.parse('(var1 = value1 & var2 = value2) | var3 = value3')
|
35
|
+
|
36
|
+
tree.left.expression.left.text_value.strip.should eq 'var1 = value1'
|
37
|
+
tree.left.expression.coordinator.text_value.should eq '&'
|
38
|
+
tree.left.expression.right.text_value.strip.should eq 'var2 = value2'
|
39
|
+
tree.coordinator.text_value.strip.should eq '|'
|
40
|
+
tree.right.text_value.strip.should eq 'var3 = value3'
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/spec/models/city.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Ransack Query' do
|
4
|
+
|
5
|
+
let(:parser) { NQL::SyntaxParser.new }
|
6
|
+
|
7
|
+
context 'Single comparisons' do
|
8
|
+
|
9
|
+
it 'Equals' do
|
10
|
+
q = parser.parse('id = 1234').to_ransack
|
11
|
+
|
12
|
+
q['c'][0].should have_attribute 'id'
|
13
|
+
q['c'][0].should have_predicate 'eq'
|
14
|
+
q['c'][0].should have_value '1234'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'Not equals' do
|
18
|
+
q = parser.parse('id != 1234').to_ransack
|
19
|
+
|
20
|
+
q['c'][0].should have_attribute 'id'
|
21
|
+
q['c'][0].should have_predicate 'not_eq'
|
22
|
+
q['c'][0].should have_value '1234'
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'Greater than' do
|
26
|
+
q = parser.parse('id > 1234').to_ransack
|
27
|
+
|
28
|
+
q['c'][0].should have_attribute 'id'
|
29
|
+
q['c'][0].should have_predicate 'gt'
|
30
|
+
q['c'][0].should have_value '1234'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'Greater or equals than' do
|
34
|
+
q = parser.parse('id >= 1234').to_ransack
|
35
|
+
|
36
|
+
q['c'][0].should have_attribute 'id'
|
37
|
+
q['c'][0].should have_predicate 'gteq'
|
38
|
+
q['c'][0].should have_value '1234'
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'Less than' do
|
42
|
+
q = parser.parse('id < 1234').to_ransack
|
43
|
+
|
44
|
+
q['c'][0].should have_attribute 'id'
|
45
|
+
q['c'][0].should have_predicate 'lt'
|
46
|
+
q['c'][0].should have_value '1234'
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'Less or equals than' do
|
50
|
+
q = parser.parse('id <= 1234').to_ransack
|
51
|
+
|
52
|
+
q['c'][0].should have_attribute 'id'
|
53
|
+
q['c'][0].should have_predicate 'lteq'
|
54
|
+
q['c'][0].should have_value '1234'
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'Contains' do
|
58
|
+
q = parser.parse('id % 1234').to_ransack
|
59
|
+
|
60
|
+
q['c'][0].should have_attribute 'id'
|
61
|
+
q['c'][0].should have_predicate 'cont'
|
62
|
+
q['c'][0].should have_value '1234'
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'Model references' do
|
66
|
+
q = parser.parse('models.id = 1234').to_ransack
|
67
|
+
|
68
|
+
q['c'][0].should have_attribute 'models_id'
|
69
|
+
q['c'][0].should have_predicate 'eq'
|
70
|
+
q['c'][0].should have_value '1234'
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'Coordinated comparisons' do
|
76
|
+
|
77
|
+
it 'And' do
|
78
|
+
q = parser.parse('id > 1234 & name = abcd').to_ransack
|
79
|
+
|
80
|
+
q['g'][0]['m'].should eq 'and'
|
81
|
+
q['g'][0]['c'][0].should have_attribute 'id'
|
82
|
+
q['g'][0]['c'][0].should have_predicate 'gt'
|
83
|
+
q['g'][0]['c'][0].should have_value '1234'
|
84
|
+
q['g'][0]['c'][1].should have_attribute 'name'
|
85
|
+
q['g'][0]['c'][1].should have_predicate 'eq'
|
86
|
+
q['g'][0]['c'][1].should have_value 'abcd'
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'Or' do
|
90
|
+
q = parser.parse('id < 1234 | name % abcd').to_ransack
|
91
|
+
|
92
|
+
q['g'][0]['m'].should eq 'or'
|
93
|
+
q['g'][0]['c'][0].should have_attribute 'id'
|
94
|
+
q['g'][0]['c'][0].should have_predicate 'lt'
|
95
|
+
q['g'][0]['c'][0].should have_value '1234'
|
96
|
+
q['g'][0]['c'][1].should have_attribute 'name'
|
97
|
+
q['g'][0]['c'][1].should have_predicate 'cont'
|
98
|
+
q['g'][0]['c'][1].should have_value 'abcd'
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'And then Or' do
|
102
|
+
q = parser.parse('id > 1234 & name = abcd | name % efgh').to_ransack
|
103
|
+
|
104
|
+
q['g'][0]['m'].should eq 'and'
|
105
|
+
q['g'][0]['c'][0].should have_attribute 'id'
|
106
|
+
q['g'][0]['c'][0].should have_predicate 'gt'
|
107
|
+
q['g'][0]['c'][0].should have_value '1234'
|
108
|
+
q['g'][0]['g'][0]['m'].should eq 'or'
|
109
|
+
q['g'][0]['g'][0]['c'][0].should have_attribute 'name'
|
110
|
+
q['g'][0]['g'][0]['c'][0].should have_predicate 'eq'
|
111
|
+
q['g'][0]['g'][0]['c'][0].should have_value 'abcd'
|
112
|
+
q['g'][0]['g'][0]['c'][1].should have_attribute 'name'
|
113
|
+
q['g'][0]['g'][0]['c'][1].should have_predicate 'cont'
|
114
|
+
q['g'][0]['g'][0]['c'][1].should have_value 'efgh'
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'With parentheses' do
|
118
|
+
q = parser.parse('(id > 1234 & name = abcd) | name % efgh').to_ransack
|
119
|
+
|
120
|
+
q['g'][0]['g'][0]['m'].should eq 'and'
|
121
|
+
q['g'][0]['g'][0]['c'][0].should have_attribute 'id'
|
122
|
+
q['g'][0]['g'][0]['c'][0].should have_predicate 'gt'
|
123
|
+
q['g'][0]['g'][0]['c'][0].should have_value '1234'
|
124
|
+
q['g'][0]['g'][0]['c'][1].should have_attribute 'name'
|
125
|
+
q['g'][0]['g'][0]['c'][1].should have_predicate 'eq'
|
126
|
+
q['g'][0]['g'][0]['c'][1].should have_value 'abcd'
|
127
|
+
q['g'][0]['m'].should eq 'or'
|
128
|
+
q['g'][0]['c'][0].should have_attribute 'name'
|
129
|
+
q['g'][0]['c'][0].should have_predicate 'cont'
|
130
|
+
q['g'][0]['c'][0].should have_value 'efgh'
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|