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.
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ script: bundle exec rspec spec
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # NQL
2
2
 
3
+ [![Build Status](https://travis-ci.org/gabynaiman/nql.png)](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
- q = '(name: arg | name: br) & region = south'
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
- q = 'cities.name: buenos'
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/invalid_expression_error'
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'
@@ -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
@@ -0,0 +1,9 @@
1
+ class Hash
2
+ def deep_symbolize_keys
3
+ inject({}) { |result, (key, value)|
4
+ value = value.deep_symbolize_keys if value.is_a?(Hash)
5
+ result[(key.to_sym rescue key) || key] = value
6
+ result
7
+ }
8
+ end unless Hash.method_defined?(:deep_symbolize_keys)
9
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module NQL
2
- VERSION = '0.0.5'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -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'
@@ -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
@@ -12,42 +12,42 @@ describe 'SQL generation' do
12
12
 
13
13
  it 'Equals' do
14
14
  q = 'name = abcd'
15
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE \"countries\".\"name\" = 'abcd'"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" != 'abcd')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" > 'abcd')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" >= 'abcd')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" < 'abcd')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" <= 'abcd')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" LIKE '%abcd%')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" LIKE 'abcd')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" > 1234 AND \"countries\".\"name\" = 'abcd'))"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" < 1234 OR \"countries\".\"name\" LIKE '%abcd%'))"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" > 1234 AND (\"countries\".\"name\" = 'abcd' OR \"countries\".\"name\" LIKE '%efgh%')))"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"name\" LIKE '%efgh%' OR (\"countries\".\"id\" > 1234 AND \"countries\".\"name\" = 'abcd')))"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"cities\".* FROM \"cities\" LEFT OUTER JOIN \"countries\" ON \"countries\".\"id\" = \"cities\".\"country_id\" WHERE (\"countries\".\"name\" LIKE '%abcd%')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" LEFT OUTER JOIN \"cities\" ON \"cities\".\"country_id\" = \"countries\".\"id\" WHERE (\"cities\".\"name\" LIKE '%abcd%')"
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.search(NQL.to_ransack(q)).result(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%')"
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\""
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\""
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.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\""
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
- expect { Country.search(NQL.to_ransack(q)).result }.to raise_exception NQL::InvalidExpressionError
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.5
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: 2012-11-15 00:00:00.000000000 Z
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: '0'
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: '0'
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/invalid_expression_error.rb
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:
@@ -1,7 +0,0 @@
1
- module NQL
2
- class InvalidExpressionError < StandardError
3
- def initialize(message)
4
- super
5
- end
6
- end
7
- end