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 +5 -5
- data/.gitignore +1 -4
- data/.travis.yml +14 -5
- data/CHANGELOG.md +14 -0
- data/Gemfile +0 -1
- data/README.md +22 -17
- data/gemfiles/rails42.gemfile +1 -1
- data/gemfiles/rails50.gemfile +1 -1
- data/gemfiles/rails51.gemfile +1 -1
- data/gemfiles/rails52.gemfile +6 -0
- data/gemfiles/rails60.gemfile +5 -0
- data/lib/logical_query_parser.rb +20 -4
- data/lib/{logical_query.treetop → logical_query_parser.treetop} +1 -1
- data/lib/logical_query_parser/assoc.rb +11 -0
- data/lib/logical_query_parser/assoc_resolver.rb +48 -0
- data/lib/logical_query_parser/nodes/active_record.rb +64 -36
- data/lib/logical_query_parser/nodes/base.rb +1 -1
- data/lib/logical_query_parser/version.rb +2 -2
- data/logical_query_parser.gemspec +1 -1
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c2a48abdb3c711d5984f63d4ff56d8d492e5c674a306833472435c64e2180d41
|
4
|
+
data.tar.gz: 85f5476c26e88e6f0cc5465eb46bbf62a7ac0b2a9bfa84183fd2a93cee0aef9b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95909ee34ce7e60993844d69a96c6ef683a470b44113cd49c4045ceb7f6cfb0d6081731c2e13e7a699c7584f2d4efbfd368cff549291481b8471bb6d594b09ff
|
7
|
+
data.tar.gz: 1319eab5cf8dd32f8760cacb5ac0211b8c3d2330e95dc3426325e9bc7f0767b9d477c0c4b75e82061e5126117bf70ffe729252dae3de06dc04e0041b051215d3
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,12 +1,21 @@
|
|
1
1
|
sudo: false
|
2
2
|
language: ruby
|
3
3
|
rvm:
|
4
|
-
- 2.3
|
5
|
-
- 2.4
|
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
|
-
|
11
|
-
-
|
12
|
-
|
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
data/Gemfile
CHANGED
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/
|
82
|
+
For more information, see [grammar definition](lib/logical_query_parser.treetop).
|
83
83
|
|
84
|
-
###
|
84
|
+
### Use with ActiveRecord
|
85
85
|
|
86
|
-
You can
|
86
|
+
You can use a syntax tree to compile a SQL statement for activerecord. For example:
|
87
87
|
|
88
88
|
```ruby
|
89
|
-
class
|
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
|
-
|
100
|
-
|
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
|
-
|
97
|
+
Use with associations:
|
104
98
|
|
105
|
-
|
99
|
+
```ruby
|
100
|
+
class Doc < ActiveRecord::Base
|
101
|
+
has_many :tags
|
102
|
+
end
|
106
103
|
|
107
|
-
|
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.
|
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
|
|
data/gemfiles/rails42.gemfile
CHANGED
data/gemfiles/rails50.gemfile
CHANGED
data/gemfiles/rails51.gemfile
CHANGED
data/lib/logical_query_parser.rb
CHANGED
@@ -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
|
-
|
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?
|
@@ -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
|
1
|
+
module LogicalQueryParser
|
2
2
|
module ExpNode
|
3
|
-
def to_sql(
|
4
|
-
|
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(
|
10
|
+
def to_sql(params)
|
10
11
|
if negative.elements.size > 0
|
11
|
-
negative.elements[0].to_sql(
|
12
|
+
negative.elements[0].to_sql(params)
|
12
13
|
end
|
13
|
-
lparen.to_sql(
|
14
|
-
exp.to_sql(
|
15
|
-
rparen.to_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(
|
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(
|
26
|
-
lexp.to_sql(
|
27
|
-
logic.to_sql(
|
28
|
-
rexp.to_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(
|
34
|
-
literal.to_sql(
|
35
|
-
sql << ' AND '
|
36
|
-
exp.to_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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
72
|
-
operator, logic =
|
73
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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,3 +1,3 @@
|
|
1
|
-
module
|
2
|
-
VERSION = "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 =
|
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.
|
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:
|
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
|
-
-
|
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
|
-
|
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.
|