nql 0.0.5 → 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.
- data/.travis.yml +5 -0
- data/README.md +19 -4
- data/lib/nql.rb +4 -18
- data/lib/nql/error.rb +26 -0
- data/lib/nql/extension/active_record.rb +19 -0
- data/lib/nql/extension/hash.rb +9 -0
- data/lib/nql/query.rb +62 -0
- data/lib/nql/version.rb +1 -1
- data/nql.gemspec +2 -2
- data/spec/query_spec.rb +48 -0
- data/spec/sql_spec.rb +19 -19
- metadata +18 -11
- data/lib/nql/invalid_expression_error.rb +0 -7
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# NQL
|
2
2
|
|
3
|
+
[](https://travis-ci.org/gabynaiman/nql)
|
4
|
+
|
3
5
|
Natural Query Language built on top of ActiveRecord and Ransack
|
4
6
|
|
5
7
|
## Installation
|
@@ -36,16 +38,29 @@ Or install it yourself as:
|
|
36
38
|
|
37
39
|
Converts from natural language to query expression
|
38
40
|
|
39
|
-
|
40
|
-
Country.search(NQL.to_ransack(q)).result.to_sql
|
41
|
+
Country.nql('(name: arg | name: br) & region = south').to_sql
|
41
42
|
=> "SELECT coutries.* FROM countries WHERE (countries.name LIKE '%arg%' OR countries.name LIKE '%br%') AND region = 'south'"
|
42
43
|
|
43
44
|
### Joins support
|
44
45
|
|
45
|
-
|
46
|
-
Country.search(NQL.to_ransack(q)).result.to_sql
|
46
|
+
Country.nql('cities.name: buenos').to_sql
|
47
47
|
=> "SELECT countries.* FROM countries LEFT OUTER JOIN cities ON countries.id = cities.country_id WHERE cities.name LIKE '%buenos%'"
|
48
48
|
|
49
|
+
### Invalid expressions handling
|
50
|
+
|
51
|
+
Safe query
|
52
|
+
|
53
|
+
Country.nql('xyz').to_sql
|
54
|
+
=> "SELECT coutries.* FROM countries WHERE (1=2)
|
55
|
+
|
56
|
+
Raising exceptions
|
57
|
+
|
58
|
+
Country.nql!('xyz') => raise NQL::SyntaxError
|
59
|
+
|
60
|
+
Country.nql!('xyz: arg') => raise NQL::AttributesNotFoundError
|
61
|
+
|
62
|
+
Country.nql!(1234) => raise NQL::DataTypeError
|
63
|
+
|
49
64
|
## Contributing
|
50
65
|
|
51
66
|
1. Fork it
|
data/lib/nql.rb
CHANGED
@@ -4,22 +4,8 @@ require 'active_support/all'
|
|
4
4
|
require 'ransack'
|
5
5
|
|
6
6
|
require 'nql/version'
|
7
|
+
require 'nql/extension/hash'
|
8
|
+
require 'nql/extension/active_record'
|
7
9
|
require 'nql/grammar'
|
8
|
-
require 'nql/
|
9
|
-
|
10
|
-
module NQL
|
11
|
-
|
12
|
-
def self.to_ransack(query)
|
13
|
-
return nil if query.nil? || query.strip.empty?
|
14
|
-
expression = parser.parse(query)
|
15
|
-
raise InvalidExpressionError.new(parser.failure_reason) unless expression
|
16
|
-
expression.to_ransack
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def self.parser
|
22
|
-
@@parser ||= SyntaxParser.new
|
23
|
-
end
|
24
|
-
|
25
|
-
end
|
10
|
+
require 'nql/query'
|
11
|
+
require 'nql/error'
|
data/lib/nql/error.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module NQL
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class SyntaxError < Error
|
6
|
+
end
|
7
|
+
|
8
|
+
class DataTypeError < Error
|
9
|
+
def initialize(text)
|
10
|
+
super "#{text} must be a String"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class InvalidModelError < Error
|
15
|
+
def initialize(model)
|
16
|
+
super "#{model} must be subclass of ActiveRecord::Base"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class AttributesNotFoundError < Error
|
21
|
+
def initialize(model, attributes)
|
22
|
+
super "#{model} does not contains the attributes #{attributes}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
|
4
|
+
def self.nql(query, options={})
|
5
|
+
nql! query, options
|
6
|
+
rescue NQL::Error
|
7
|
+
self.where('1=2')
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.nql!(query, options={})
|
11
|
+
nql_search(query).result(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.nql_search(query)
|
15
|
+
NQL::Query.new(self, query).ransack_search
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/nql/query.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module NQL
|
2
|
+
class Query
|
3
|
+
|
4
|
+
attr_reader :model
|
5
|
+
attr_reader :text
|
6
|
+
attr_reader :expression
|
7
|
+
|
8
|
+
def initialize(model, text)
|
9
|
+
raise InvalidModelError.new model unless model.ancestors.include? ::ActiveRecord::Base
|
10
|
+
raise DataTypeError.new text if text && !text.is_a?(String)
|
11
|
+
@model = model
|
12
|
+
@text = text
|
13
|
+
evaluate
|
14
|
+
end
|
15
|
+
|
16
|
+
def ransack_search
|
17
|
+
if expression
|
18
|
+
model.search(expression.to_ransack)
|
19
|
+
else
|
20
|
+
model.search
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def evaluate
|
27
|
+
if text.nil? || text.strip.empty?
|
28
|
+
@expression = nil
|
29
|
+
else
|
30
|
+
parser = SyntaxParser.new
|
31
|
+
@expression = parser.parse(text)
|
32
|
+
raise SyntaxError.new(parser.failure_reason) unless expression
|
33
|
+
validate_attributes!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_attributes!
|
38
|
+
return unless expression
|
39
|
+
extended_attributes = model.column_names | model.reflections.flat_map { |k, v| v.klass.column_names.map { |c| "#{k}_#{c}" } }
|
40
|
+
invalid_attributes(expression.to_ransack, extended_attributes).tap do |attributes|
|
41
|
+
raise AttributesNotFoundError.new model, attributes if attributes.any?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def invalid_attributes(node, valid_attributes)
|
46
|
+
return [] unless node
|
47
|
+
|
48
|
+
node.deep_symbolize_keys.flat_map do |k, v|
|
49
|
+
if k == :a
|
50
|
+
[v['0'.to_sym][:name]] unless valid_attributes.include?(v['0'.to_sym][:name])
|
51
|
+
else
|
52
|
+
if v.is_a?(Hash)
|
53
|
+
invalid_attributes(v, valid_attributes)
|
54
|
+
elsif v.is_a?(Array)
|
55
|
+
v.select { |e| e.is_a?(Hash) }.flat_map { |e| invalid_attributes(e, valid_attributes) }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end.compact
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
data/lib/nql/version.rb
CHANGED
data/nql.gemspec
CHANGED
@@ -15,10 +15,10 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
16
16
|
s.require_paths = ["lib"]
|
17
17
|
|
18
|
-
s.add_dependency 'treetop'
|
18
|
+
s.add_dependency 'treetop', '~> 1.4'
|
19
19
|
s.add_dependency 'activerecord', '>= 3.2.0'
|
20
20
|
s.add_dependency 'activesupport', '>= 3.2.0'
|
21
|
-
s.add_dependency 'ransack'
|
21
|
+
s.add_dependency 'ransack', '~> 0.7'
|
22
22
|
|
23
23
|
s.add_development_dependency 'sqlite3'
|
24
24
|
s.add_development_dependency 'rspec'
|
data/spec/query_spec.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe NQL::Query do
|
4
|
+
|
5
|
+
before :all do
|
6
|
+
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ":memory:"
|
7
|
+
ActiveRecord::Base.connection
|
8
|
+
ActiveRecord::Migrator.migrate ActiveRecord::Migrator.migrations_path
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'Valid expression' do
|
12
|
+
query = NQL::Query.new Country, 'name: Argentina'
|
13
|
+
|
14
|
+
query.text.should eq 'name: Argentina'
|
15
|
+
query.expression.should be_a Treetop::Runtime::SyntaxNode
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'Valid with empty expression' do
|
19
|
+
query = NQL::Query.new Country, ''
|
20
|
+
|
21
|
+
query.text.should eq ''
|
22
|
+
query.expression.should be_nil
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'Valid with nil expression' do
|
26
|
+
query = NQL::Query.new Country, nil
|
27
|
+
|
28
|
+
query.text.should be_nil
|
29
|
+
query.expression.should be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'Invalid model type' do
|
33
|
+
expect { NQL::Query.new Object, nil }.to raise_error NQL::InvalidModelError
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'Invalid expression type' do
|
37
|
+
expect { NQL::Query.new Country, Object.new }.to raise_error NQL::DataTypeError
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'Invalid expression syntax' do
|
41
|
+
expect { NQL::Query.new Country, 'xyz1234' }.to raise_error NQL::SyntaxError
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'Invalid fields' do
|
45
|
+
expect { NQL::Query.new Country, 'name: Argentina | xyz: 1234 | abc: 0000'}.to raise_error NQL::AttributesNotFoundError
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/spec/sql_spec.rb
CHANGED
@@ -12,42 +12,42 @@ describe 'SQL generation' do
|
|
12
12
|
|
13
13
|
it 'Equals' do
|
14
14
|
q = 'name = abcd'
|
15
|
-
Country.
|
15
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE \"countries\".\"name\" = 'abcd'"
|
16
16
|
end
|
17
17
|
|
18
18
|
it 'Not equals' do
|
19
19
|
q = 'name != abcd'
|
20
|
-
Country.
|
20
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" != 'abcd')"
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'Greater than' do
|
24
24
|
q = 'name > abcd'
|
25
|
-
Country.
|
25
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" > 'abcd')"
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'Greater or equals than' do
|
29
29
|
q = 'name >= abcd'
|
30
|
-
Country.
|
30
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" >= 'abcd')"
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'Less than' do
|
34
34
|
q = 'name < abcd'
|
35
|
-
Country.
|
35
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" < 'abcd')"
|
36
36
|
end
|
37
37
|
|
38
38
|
it 'Less or equals than' do
|
39
39
|
q = 'name <= abcd'
|
40
|
-
Country.
|
40
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" <= 'abcd')"
|
41
41
|
end
|
42
42
|
|
43
43
|
it 'Contains' do
|
44
44
|
q = 'name : abcd'
|
45
|
-
Country.
|
45
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" LIKE '%abcd%')"
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'Matches' do
|
49
49
|
q = 'name ~ abcd'
|
50
|
-
Country.
|
50
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" LIKE 'abcd')"
|
51
51
|
end
|
52
52
|
|
53
53
|
end
|
@@ -56,22 +56,22 @@ describe 'SQL generation' do
|
|
56
56
|
|
57
57
|
it 'And' do
|
58
58
|
q = 'id > 1234 & name = abcd'
|
59
|
-
Country.
|
59
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" > 1234 AND \"countries\".\"name\" = 'abcd'))"
|
60
60
|
end
|
61
61
|
|
62
62
|
it 'Or' do
|
63
63
|
q = 'id < 1234 | name : abcd'
|
64
|
-
Country.
|
64
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" < 1234 OR \"countries\".\"name\" LIKE '%abcd%'))"
|
65
65
|
end
|
66
66
|
|
67
67
|
it 'And then Or' do
|
68
68
|
q = 'id > 1234 & name = abcd | name : efgh'
|
69
|
-
Country.
|
69
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" > 1234 AND (\"countries\".\"name\" = 'abcd' OR \"countries\".\"name\" LIKE '%efgh%')))"
|
70
70
|
end
|
71
71
|
|
72
72
|
it 'With parentheses' do
|
73
73
|
q = '(id > 1234 & name = abcd) | name : efgh'
|
74
|
-
Country.
|
74
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"name\" LIKE '%efgh%' OR (\"countries\".\"id\" > 1234 AND \"countries\".\"name\" = 'abcd')))"
|
75
75
|
end
|
76
76
|
|
77
77
|
end
|
@@ -80,17 +80,17 @@ describe 'SQL generation' do
|
|
80
80
|
|
81
81
|
it 'Parent join' do
|
82
82
|
q = 'country.name : abcd'
|
83
|
-
City.
|
83
|
+
City.nql(q).should produce_sql "SELECT \"cities\".* FROM \"cities\" LEFT OUTER JOIN \"countries\" ON \"countries\".\"id\" = \"cities\".\"country_id\" WHERE (\"countries\".\"name\" LIKE '%abcd%')"
|
84
84
|
end
|
85
85
|
|
86
86
|
it 'Children join' do
|
87
87
|
q = 'cities.name : abcd'
|
88
|
-
Country.
|
88
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" LEFT OUTER JOIN \"cities\" ON \"cities\".\"country_id\" = \"countries\".\"id\" WHERE (\"cities\".\"name\" LIKE '%abcd%')"
|
89
89
|
end
|
90
90
|
|
91
91
|
it 'Children join distinct' do
|
92
92
|
q = 'cities.name : abcd'
|
93
|
-
Country.
|
93
|
+
Country.nql(q, distinct: true).should produce_sql "SELECT DISTINCT \"countries\".* FROM \"countries\" LEFT OUTER JOIN \"cities\" ON \"cities\".\"country_id\" = \"countries\".\"id\" WHERE (\"cities\".\"name\" LIKE '%abcd%')"
|
94
94
|
end
|
95
95
|
|
96
96
|
end
|
@@ -99,22 +99,22 @@ describe 'SQL generation' do
|
|
99
99
|
|
100
100
|
it 'Nil' do
|
101
101
|
q = nil
|
102
|
-
Country.
|
102
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\""
|
103
103
|
end
|
104
104
|
|
105
105
|
it 'Empty' do
|
106
106
|
q = ''
|
107
|
-
Country.
|
107
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\""
|
108
108
|
end
|
109
109
|
|
110
110
|
it 'Empty with spaces' do
|
111
111
|
q = ' '
|
112
|
-
Country.
|
112
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\""
|
113
113
|
end
|
114
114
|
|
115
115
|
it 'Partial expression' do
|
116
116
|
q = 'id ='
|
117
|
-
|
117
|
+
Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (1=2)"
|
118
118
|
end
|
119
119
|
|
120
120
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,24 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: treetop
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
|
-
- -
|
19
|
+
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: '
|
21
|
+
version: '1.4'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
26
26
|
requirements:
|
27
|
-
- -
|
27
|
+
- - ~>
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
29
|
+
version: '1.4'
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
31
|
name: activerecord
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -64,17 +64,17 @@ dependencies:
|
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
65
65
|
none: false
|
66
66
|
requirements:
|
67
|
-
- -
|
67
|
+
- - ~>
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '0'
|
69
|
+
version: '0.7'
|
70
70
|
type: :runtime
|
71
71
|
prerelease: false
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
73
73
|
none: false
|
74
74
|
requirements:
|
75
|
-
- -
|
75
|
+
- - ~>
|
76
76
|
- !ruby/object:Gem::Version
|
77
|
-
version: '0'
|
77
|
+
version: '0.7'
|
78
78
|
- !ruby/object:Gem::Dependency
|
79
79
|
name: sqlite3
|
80
80
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,14 +115,18 @@ extensions: []
|
|
115
115
|
extra_rdoc_files: []
|
116
116
|
files:
|
117
117
|
- .gitignore
|
118
|
+
- .travis.yml
|
118
119
|
- Gemfile
|
119
120
|
- LICENSE
|
120
121
|
- README.md
|
121
122
|
- Rakefile
|
122
123
|
- lib/nql.rb
|
124
|
+
- lib/nql/error.rb
|
125
|
+
- lib/nql/extension/active_record.rb
|
126
|
+
- lib/nql/extension/hash.rb
|
123
127
|
- lib/nql/grammar.rb
|
124
128
|
- lib/nql/grammar.treetop
|
125
|
-
- lib/nql/
|
129
|
+
- lib/nql/query.rb
|
126
130
|
- lib/nql/version.rb
|
127
131
|
- nql.gemspec
|
128
132
|
- spec/comparison_parser_spec.rb
|
@@ -131,6 +135,7 @@ files:
|
|
131
135
|
- spec/migrations/20121108154508_create_cities.rb
|
132
136
|
- spec/models/city.rb
|
133
137
|
- spec/models/country.rb
|
138
|
+
- spec/query_spec.rb
|
134
139
|
- spec/ransack_spec.rb
|
135
140
|
- spec/spec_helper.rb
|
136
141
|
- spec/sql_spec.rb
|
@@ -165,6 +170,8 @@ test_files:
|
|
165
170
|
- spec/migrations/20121108154508_create_cities.rb
|
166
171
|
- spec/models/city.rb
|
167
172
|
- spec/models/country.rb
|
173
|
+
- spec/query_spec.rb
|
168
174
|
- spec/ransack_spec.rb
|
169
175
|
- spec/spec_helper.rb
|
170
176
|
- spec/sql_spec.rb
|
177
|
+
has_rdoc:
|