logical_query_parser 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8f62acc3ca3efe725b46798c405985136dcd0702
4
- data.tar.gz: 56e9d416089645c652649782a1c3d320a6bedc05
2
+ SHA256:
3
+ metadata.gz: c2a48abdb3c711d5984f63d4ff56d8d492e5c674a306833472435c64e2180d41
4
+ data.tar.gz: 85f5476c26e88e6f0cc5465eb46bbf62a7ac0b2a9bfa84183fd2a93cee0aef9b
5
5
  SHA512:
6
- metadata.gz: 83df4ab258dce60d775a4d0548b4f20e25ac26d0f424fc734ea70cc579a9664ecaeaefcb3c593901629428d8954ce7ac60a3682bc32c523232c0c5de0f804ac1
7
- data.tar.gz: 1ac865f2c09148e70e0da1ba31c964b53ebdf6fc31dbedb5cd602c1b5dceb74f46177ee6bb9442511ffe6679696035e9ffb287e9cf354685c873be1a6b93f69c
6
+ metadata.gz: 95909ee34ce7e60993844d69a96c6ef683a470b44113cd49c4045ceb7f6cfb0d6081731c2e13e7a699c7584f2d4efbfd368cff549291481b8471bb6d594b09ff
7
+ data.tar.gz: 1319eab5cf8dd32f8760cacb5ac0211b8c3d2330e95dc3426325e9bc7f0767b9d477c0c4b75e82061e5126117bf70ffe729252dae3de06dc04e0041b051215d3
data/.gitignore CHANGED
@@ -1,10 +1,7 @@
1
1
  /.bundle/
2
- /.yardoc
3
2
  /.project
4
3
  /Gemfile.lock
5
- /_yardoc/
6
4
  /coverage/
7
- /doc/
5
+ /gemfiles/*gemfile.lock
8
6
  /pkg/
9
- /spec/reports/
10
7
  /tmp/
data/.travis.yml CHANGED
@@ -1,12 +1,21 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.4
5
- - 2.4.1
4
+ - 2.3
5
+ - 2.4
6
+ - 2.5
7
+ - 2.6
6
8
  gemfile:
7
9
  - gemfiles/rails42.gemfile
8
10
  - gemfiles/rails50.gemfile
9
11
  - gemfiles/rails51.gemfile
10
- before_install:
11
- - gem install bundler -v 1.15.3
12
- script: bundle exec rspec
12
+ - gemfiles/rails52.gemfile
13
+ - gemfiles/rails60.gemfile
14
+ matrix:
15
+ exclude:
16
+ - rvm: 2.3
17
+ gemfile: gemfiles/rails60.gemfile
18
+ - rvm: 2.4
19
+ gemfile: gemfiles/rails60.gemfile
20
+ script:
21
+ - bundle exec rspec
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # CHANGELOG
2
+
3
+ ## 0.3.0
4
+
5
+ * Support searching with associations.
6
+ * Change module name.
7
+
8
+ ## 0.2.0
9
+
10
+ * Support operators: "NOT", "&", "|".
11
+
12
+ ## 0.1.0
13
+
14
+ * First release.
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in logical_query_parser.gemspec
4
3
  gemspec
data/README.md CHANGED
@@ -79,36 +79,41 @@ parser.parse('("a a" AND NOT "b b") OR (c AND -d)')
79
79
  * ": represents beginning or end of a quoted word.
80
80
  * Space: represents a boundary between two words.
81
81
 
82
- For more information, see [grammar definition](lib/logical_query.treetop).
82
+ For more information, see [grammar definition](lib/logical_query_parser.treetop).
83
83
 
84
- ### Compile into SQL
84
+ ### Use with ActiveRecord
85
85
 
86
- You can compile a syntax tree into a SQL statement by using activerecord model. For example, the code below
86
+ You can use a syntax tree to compile a SQL statement for activerecord. For example:
87
87
 
88
88
  ```ruby
89
- class Table < ActiveRecord::Base
90
- def build_sql(str = "a AND b")
91
- parser = LogicalQueryParser.new
92
- parser.parse(str).to_sql(model: self, columns: %w(c1 c2))
93
- end
89
+ class Doc < ActiveRecord::Base
94
90
  end
95
- ```
96
-
97
- builds a SQL statement as follows (this example shows a postgresql statement):
98
91
 
99
- ```sql
100
- ("tables"."c1" ILIKE '%a%' OR "tables"."c2" ILIKE '%a%') AND ("tables"."c1" ILIKE '%b%' OR "tables"."c2" ILIKE '%b%')
92
+ LogicalQueryParser.search("a AND b", Doc.all, :c1, :c2).to_sql
93
+ # SELECT "docs".* FROM "docs"
94
+ # WHERE (("docs"."c1" LIKE '%a%' OR "docs"."c2" LIKE '%a%') AND ("docs"."c1" LIKE '%b%' OR "docs"."c2" LIKE '%b%'))
101
95
  ```
102
96
 
103
- ## Development
97
+ Use with associations:
104
98
 
105
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
99
+ ```ruby
100
+ class Doc < ActiveRecord::Base
101
+ has_many :tags
102
+ end
106
103
 
107
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
104
+ class Tag < ActiveRecord::Base
105
+ end
106
+
107
+ LogicalQueryParser.search("a AND b", Doc.all, :c1, :c2, tags: [:c3]).to_sql
108
+ # SELECT "docs".* FROM "docs"
109
+ # INNER JOIN "tags" ON "tags"."doc_id" = "docs"."id"
110
+ # WHERE ((("docs"."c1" LIKE '%a%' OR "docs"."c2" LIKE '%a%') OR "tags"."c3" LIKE '%a%') AND
111
+ # (("docs"."c1" LIKE '%b%' OR "docs"."c2" LIKE '%b%') OR "tags"."c3" LIKE '%b%'))
112
+ ```
108
113
 
109
114
  ## Contributing
110
115
 
111
- Bug reports and pull requests are welcome on GitHub at https://github.com/kanety/logical_query_parser. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
116
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kanety/logical_query_parser.
112
117
 
113
118
  ## License
114
119
 
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem "activerecord", "~> 4.2.9"
4
+ gem "sqlite3", "~> 1.3.6"
4
5
 
5
- # Specify your gem's dependencies in params_keeper_rails.gemspec
6
6
  gemspec path: "../"
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem "activerecord", "~> 5.0.5"
4
+ gem "sqlite3", "~> 1.3.6"
4
5
 
5
- # Specify your gem's dependencies in params_keeper_rails.gemspec
6
6
  gemspec path: "../"
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem "activerecord", "~> 5.1.3"
4
+ gem "sqlite3", "~> 1.3.6"
4
5
 
5
- # Specify your gem's dependencies in params_keeper_rails.gemspec
6
6
  gemspec path: "../"
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "activerecord", "~> 5.2.0"
4
+ gem "sqlite3", "~> 1.3.6"
5
+
6
+ gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "activerecord", "~> 6.0.0"
4
+
5
+ gemspec path: "../"
@@ -1,12 +1,28 @@
1
1
  require 'treetop'
2
+ Treetop.load File.expand_path("../logical_query_parser.treetop", __FILE__)
3
+
2
4
  require 'logical_query_parser/version'
5
+ require 'logical_query_parser/assoc_resolver'
3
6
  require 'logical_query_parser/nodes/base'
4
- require 'logical_query_parser/nodes/active_record' if defined? ActiveRecord::Base
5
-
6
- Treetop.load File.expand_path("../logical_query.treetop", __FILE__)
7
+ require 'logical_query_parser/nodes/active_record' if defined? ::ActiveRecord::Base
7
8
 
8
- class LogicalQueryParser < Treetop::Runtime::CompiledParser
9
+ module LogicalQueryParser
9
10
  class << self
11
+ def new
12
+ LogicalQueryParserParser.new
13
+ end
14
+
15
+ def search(query, relations, *options)
16
+ relations = relations.all if relations.respond_to?(:all)
17
+ assoc = resolve_assocs(relations.klass, options)
18
+ sql = new.parse(query).to_sql(model: relations.klass, columns: assoc.column_mapping)
19
+ relations.joins(assoc.structure).where(sql)
20
+ end
21
+
22
+ def resolve_assocs(klass, options)
23
+ AssocResolver.new(klass).run(options)
24
+ end
25
+
10
26
  def walk_tree(node, &block)
11
27
  yield node
12
28
  unless node.elements.nil?
@@ -1,4 +1,4 @@
1
- grammar LogicalQuery
1
+ grammar LogicalQueryParser
2
2
  rule exp
3
3
  sp* exp:(logic_exp / paren_exp / literal_exp / literal) sp* <ExpNode>
4
4
  end
@@ -0,0 +1,11 @@
1
+ module LogicalQueryParser
2
+ class Assoc
3
+ attr_accessor :column_mapping, :structure
4
+ attr_accessor :current
5
+
6
+ def initialize(attrs = {})
7
+ @column_mapping = {}
8
+ @structure = {}
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'assoc'
2
+
3
+ module LogicalQueryParser
4
+ class AssocResolver
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def run(*args)
10
+ Assoc.new.tap do |assoc|
11
+ assoc.current = assoc.structure
12
+ resolve_assocs(@klass, args, assoc)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def wrap_array(options)
19
+ if options.is_a?(Array)
20
+ options.flatten(1)
21
+ else
22
+ [options]
23
+ end
24
+ end
25
+
26
+ def resolve_assocs(klass, options, assoc)
27
+ options = wrap_array(options)
28
+ options.each do |option|
29
+ if option.is_a?(Hash)
30
+ resolve_assocs_for_hash(klass, option, assoc)
31
+ else
32
+ assoc.column_mapping[klass] ||= []
33
+ assoc.column_mapping[klass] << option
34
+ end
35
+ end
36
+ end
37
+
38
+ def resolve_assocs_for_hash(klass, hash, assoc)
39
+ hash.each do |assoc_name, options|
40
+ if (reflection = klass.reflect_on_association(assoc_name))
41
+ assoc.current[assoc_name] = {}
42
+ assoc.current = assoc.current[assoc_name]
43
+ resolve_assocs(reflection.klass, options, assoc)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,81 +1,109 @@
1
- module LogicalQuery
1
+ module LogicalQueryParser
2
2
  module ExpNode
3
- def to_sql(opts = {}, sql = '')
4
- exp.to_sql(opts, sql)
3
+ def to_sql(params = {})
4
+ params[:sql] ||= ''
5
+ exp.to_sql(params)
5
6
  end
6
7
  end
7
8
 
8
9
  module ParenExpNode
9
- def to_sql(opts, sql = '')
10
+ def to_sql(params)
10
11
  if negative.elements.size > 0
11
- negative.elements[0].to_sql(opts, sql)
12
+ negative.elements[0].to_sql(params)
12
13
  end
13
- lparen.to_sql(opts, sql)
14
- exp.to_sql(opts, sql)
15
- rparen.to_sql(opts, sql)
14
+ lparen.to_sql(params)
15
+ exp.to_sql(params)
16
+ rparen.to_sql(params)
16
17
  if rexp.elements.size > 0
17
- sql += ' AND '
18
- rexp.elements[0].to_sql(opts, sql)
18
+ params[:sql] += ' AND '
19
+ rexp.elements[0].to_sql(params)
19
20
  end
20
- sql
21
+ params[:sql]
21
22
  end
22
23
  end
23
24
 
24
25
  module LogicExpNode
25
- def to_sql(opts, sql = '')
26
- lexp.to_sql(opts, sql)
27
- logic.to_sql(opts, sql)
28
- rexp.to_sql(opts, sql)
26
+ def to_sql(params)
27
+ lexp.to_sql(params)
28
+ logic.to_sql(params)
29
+ rexp.to_sql(params)
29
30
  end
30
31
  end
31
32
 
32
33
  module LiteralExpNode
33
- def to_sql(opts, sql = '')
34
- literal.to_sql(opts, sql)
35
- sql << ' AND '
36
- exp.to_sql(opts, sql)
34
+ def to_sql(params)
35
+ literal.to_sql(params)
36
+ params[:sql] << ' AND '
37
+ exp.to_sql(params)
37
38
  end
38
39
  end
39
40
 
40
41
  module LParenNode
41
- def to_sql(opts, sql = '')
42
- sql << '('
42
+ def to_sql(params)
43
+ params[:sql] << '('
43
44
  end
44
45
  end
45
46
 
46
47
  module RParenNode
47
- def to_sql(opts, sql = '')
48
- sql << ')'
48
+ def to_sql(params)
49
+ params[:sql] << ')'
49
50
  end
50
51
  end
51
52
 
52
53
  module AndNode
53
- def to_sql(opts, sql = '')
54
- sql << ' AND '
54
+ def to_sql(params)
55
+ params[:sql] << ' AND '
55
56
  end
56
57
  end
57
58
 
58
59
  module OrNode
59
- def to_sql(opts, sql = '')
60
- sql << ' OR '
60
+ def to_sql(params)
61
+ params[:sql] << ' OR '
61
62
  end
62
63
  end
63
64
 
64
65
  module NotNode
65
- def to_sql(opts, sql = '')
66
- sql << 'NOT '
66
+ def to_sql(params)
67
+ params[:sql] << 'NOT '
67
68
  end
68
69
  end
69
70
 
70
71
  module LiteralNode
71
- def to_sql(opts, sql = '')
72
- operator, logic = negative.elements.size > 0 ? [:does_not_match, :and] : [:matches, :or]
73
- unquoted = LogicalQuery.unquote(word.text_value)
72
+ def to_sql(params)
73
+ operator, logic = operator_and_logic
74
+ text = LogicalQueryParser.unquote(word.text_value)
75
+
76
+ sql = build_arel(params, operator, text).reduce(logic).to_sql
77
+ sql = "(#{sql})" if sql[0] != '(' && sql[-1] != ')'
78
+ params[:sql] << sql
79
+ end
80
+
81
+ private
82
+
83
+ def operator_and_logic
84
+ if negative.elements.size > 0
85
+ return :does_not_match, :and
86
+ else
87
+ return :matches, :or
88
+ end
89
+ end
74
90
 
75
- arel = opts[:model].arel_table
76
- ss = opts[:columns].map { |c| arel[c].send(operator, "%#{unquoted}%") }.reduce(logic).to_sql
77
- ss = "(#{ss})" if ss[0] != '(' && ss[-1] != ')'
78
- sql << ss
91
+ def build_arel(params, operator, text)
92
+ if params[:columns].is_a?(Hash)
93
+ build_arel_from_hash(params[:model], params[:columns], operator, text)
94
+ else
95
+ build_arel_from_columns(params[:model], params[:columns], operator, text)
96
+ end
97
+ end
98
+
99
+ def build_arel_from_columns(klass, columns, operator, text)
100
+ columns.map { |column| klass.arel_table[column].send(operator, "%#{text}%") }
101
+ end
102
+
103
+ def build_arel_from_hash(klass, hash, operator, text)
104
+ hash.flat_map do |klass, columns|
105
+ build_arel_from_columns(klass, columns, operator, text)
106
+ end
79
107
  end
80
108
  end
81
109
 
@@ -1,4 +1,4 @@
1
- module LogicalQuery
1
+ module LogicalQueryParser
2
2
  module ExpNode
3
3
  end
4
4
 
@@ -1,3 +1,3 @@
1
- module LogicalQuery
2
- VERSION = "0.2.0"
1
+ module LogicalQueryParser
2
+ VERSION = "0.3.0"
3
3
  end
@@ -5,7 +5,7 @@ require 'logical_query_parser/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "logical_query_parser"
8
- spec.version = LogicalQuery::VERSION
8
+ spec.version = LogicalQueryParser::VERSION
9
9
  spec.authors = ["Yoshikazu Kaneta"]
10
10
  spec.email = ["kaneta@sitebridge.co.jp"]
11
11
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logical_query_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoshikazu Kaneta
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-10-07 00:00:00.000000000 Z
11
+ date: 2019-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: treetop
@@ -119,6 +119,7 @@ files:
119
119
  - ".gitignore"
120
120
  - ".rspec"
121
121
  - ".travis.yml"
122
+ - CHANGELOG.md
122
123
  - CODE_OF_CONDUCT.md
123
124
  - Gemfile
124
125
  - LICENSE.txt
@@ -129,8 +130,12 @@ files:
129
130
  - gemfiles/rails42.gemfile
130
131
  - gemfiles/rails50.gemfile
131
132
  - gemfiles/rails51.gemfile
132
- - lib/logical_query.treetop
133
+ - gemfiles/rails52.gemfile
134
+ - gemfiles/rails60.gemfile
133
135
  - lib/logical_query_parser.rb
136
+ - lib/logical_query_parser.treetop
137
+ - lib/logical_query_parser/assoc.rb
138
+ - lib/logical_query_parser/assoc_resolver.rb
134
139
  - lib/logical_query_parser/nodes/active_record.rb
135
140
  - lib/logical_query_parser/nodes/base.rb
136
141
  - lib/logical_query_parser/version.rb
@@ -154,8 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
154
159
  - !ruby/object:Gem::Version
155
160
  version: '0'
156
161
  requirements: []
157
- rubyforge_project:
158
- rubygems_version: 2.6.12
162
+ rubygems_version: 3.0.3
159
163
  signing_key:
160
164
  specification_version: 4
161
165
  summary: A parser for a logical query string.