logical_query_parser 0.5.0 → 0.6.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 +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/CHANGELOG.md +5 -0
- data/README.md +21 -1
- data/lib/logical_query_parser/assoc_node.rb +40 -0
- data/lib/logical_query_parser/assoc_resolver.rb +27 -24
- data/lib/logical_query_parser/nodes/active_record.rb +7 -16
- data/lib/logical_query_parser/version.rb +1 -1
- data/lib/logical_query_parser.rb +6 -5
- data/logical_query_parser.gemspec +2 -0
- metadata +30 -2
- data/lib/logical_query_parser/assoc.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50d9e7d0d793b457dd2561160c3fc0e2a0611d7425c671a624c19c5e7395fd92
|
|
4
|
+
data.tar.gz: cc8b9d94d8c13c5764e82b64be14d6b68e050f8d7fe9a3b53c624aa973dbb949
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd0590d955cba3c8b10d2441e62d720131bffb46ecbfbbf5df3ed49fc1e27aeb1a81a28e2ae18d3cdd50e553a0802a24b66b3c5d77b15395391bae7a18e095e4
|
|
7
|
+
data.tar.gz: 678c2067c0dae4587dde4158d251ccb7643479af9ff0c894be7c3410a02c2970a0e83ea8275a300d0ed2b89f08aaa98ddabd0332dca40e62033d00d65c76f208
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -4,7 +4,7 @@ on: [push, pull_request]
|
|
|
4
4
|
|
|
5
5
|
jobs:
|
|
6
6
|
test:
|
|
7
|
-
runs-on: ubuntu-
|
|
7
|
+
runs-on: ubuntu-24.04
|
|
8
8
|
strategy:
|
|
9
9
|
fail-fast: false
|
|
10
10
|
matrix:
|
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
|
29
29
|
|
|
30
30
|
steps:
|
|
31
|
-
- uses: actions/checkout@
|
|
31
|
+
- uses: actions/checkout@v5
|
|
32
32
|
- uses: ruby/setup-ruby@v1
|
|
33
33
|
with:
|
|
34
34
|
ruby-version: ${{ matrix.ruby }}
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -99,16 +99,36 @@ Use with associations:
|
|
|
99
99
|
```ruby
|
|
100
100
|
class Doc < ActiveRecord::Base
|
|
101
101
|
has_many :tags
|
|
102
|
+
has_many :flags
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
class Tag < ActiveRecord::Base
|
|
106
|
+
belongs_to :doc
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class Flag < ActiveRecord::Base
|
|
110
|
+
belongs_to :doc
|
|
105
111
|
end
|
|
106
112
|
|
|
107
113
|
LogicalQueryParser.search("a AND b", Doc.all, :c1, :c2, tags: [:c3]).to_sql
|
|
108
114
|
# SELECT "docs".* FROM "docs"
|
|
109
|
-
#
|
|
115
|
+
# LEFT OUTER JOIN "tags" ON "tags"."doc_id" = "docs"."id"
|
|
110
116
|
# WHERE ((("docs"."c1" LIKE '%a%' OR "docs"."c2" LIKE '%a%') OR "tags"."c3" LIKE '%a%') AND
|
|
111
117
|
# (("docs"."c1" LIKE '%b%' OR "docs"."c2" LIKE '%b%') OR "tags"."c3" LIKE '%b%'))
|
|
118
|
+
|
|
119
|
+
LogicalQueryParser.search("a AND b", Doc.all, :c1, :c2, tags: [:c3], flags: [:c4]).to_sql
|
|
120
|
+
# SELECT "docs".* FROM "docs"
|
|
121
|
+
# LEFT OUTER JOIN "tags" ON "tags"."doc_id" = "docs"."id"
|
|
122
|
+
# LEFT OUTER JOIN "flags" ON "flags"."doc_id" = "docs"."id"
|
|
123
|
+
# WHERE (((("docs"."c1" LIKE '%a%' OR "docs"."c2" LIKE '%a%') OR "tags"."c3" LIKE '%a%') OR "flags"."c4" LIKE '%a%') AND
|
|
124
|
+
# ((("docs"."c1" LIKE '%b%' OR "docs"."c2" LIKE '%b%') OR "tags"."c3" LIKE '%b%') OR "flags"."c4" LIKE '%b%'))
|
|
125
|
+
|
|
126
|
+
LogicalQueryParser.search("a AND b", Doc.all, :c1, tags: [:c2, doc: [:c3]]).to_sql
|
|
127
|
+
# SELECT "docs".* FROM "docs"
|
|
128
|
+
# LEFT OUTER JOIN "tags" ON "tags"."doc_id" = "docs"."id"
|
|
129
|
+
# LEFT OUTER JOIN "docs" AS "docs_tags" ON "docs_tags"."id" = "tags"."doc_id"
|
|
130
|
+
# WHERE ((("docs"."c1" LIKE '%a%' OR "tags"."c2" LIKE '%a%') OR "docs_tags"."c3" LIKE '%a%') AND
|
|
131
|
+
# (("docs"."c1" LIKE '%b%' OR "tags"."c2" LIKE '%b%') OR "docs_tags"."c3" LIKE '%b%'))
|
|
112
132
|
```
|
|
113
133
|
|
|
114
134
|
## Contributing
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogicalQueryParser
|
|
4
|
+
class AssocNode
|
|
5
|
+
attr_accessor :klass, :assoc_name, :table_name, :columns, :parent, :children
|
|
6
|
+
|
|
7
|
+
def initialize(options = {})
|
|
8
|
+
options.each do |key, value|
|
|
9
|
+
send("#{key}=", value)
|
|
10
|
+
end
|
|
11
|
+
@columns ||= []
|
|
12
|
+
@children ||= []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def model=(model)
|
|
16
|
+
@klass = model
|
|
17
|
+
@table_name = model.table_name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def descendants
|
|
21
|
+
children.flat_map do |child|
|
|
22
|
+
[child] + child.descendants
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def arel_table
|
|
27
|
+
Arel::Table.new(table_name, as: table_name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def join_structure
|
|
31
|
+
if children.empty?
|
|
32
|
+
{}
|
|
33
|
+
else
|
|
34
|
+
children.each_with_object({}) do |child, hash|
|
|
35
|
+
hash[child.assoc_name] = child.join_structure
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'assoc_node'
|
|
4
4
|
|
|
5
5
|
module LogicalQueryParser
|
|
6
6
|
class AssocResolver
|
|
7
|
-
def initialize(
|
|
8
|
-
@
|
|
7
|
+
def initialize(relation, *options)
|
|
8
|
+
@relation = relation
|
|
9
|
+
@options = options.flatten(1)
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
def call
|
|
13
|
+
root_node = AssocNode.new(klass: @relation.klass, table_name: @relation.table_name)
|
|
14
|
+
resolve_assocs(@relation.klass, root_node, @options)
|
|
15
|
+
|
|
16
|
+
join_relation = @relation.klass.unscoped.left_joins(root_node.join_structure)
|
|
17
|
+
root_node.descendants.each_with_index do |node, i|
|
|
18
|
+
join_source = join_relation.arel.join_sources[i]
|
|
19
|
+
node.table_name = join_source&.left&.name || node.klass.table_name
|
|
15
20
|
end
|
|
21
|
+
|
|
22
|
+
root_node
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
private
|
|
@@ -25,24 +32,20 @@ module LogicalQueryParser
|
|
|
25
32
|
end
|
|
26
33
|
end
|
|
27
34
|
|
|
28
|
-
def resolve_assocs(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
def resolve_assocs(current_klass, node, options)
|
|
36
|
+
wrap_array(options).each do |column_or_assoc_hash|
|
|
37
|
+
if column_or_assoc_hash.is_a?(Hash)
|
|
38
|
+
column_or_assoc_hash.each do |assoc_name, nested_column_or_assoc_hash|
|
|
39
|
+
if (reflection = current_klass.reflect_on_association(assoc_name))
|
|
40
|
+
child = AssocNode.new(klass: reflection.klass, assoc_name: assoc_name, parent: node)
|
|
41
|
+
node.children ||= []
|
|
42
|
+
node.children << child
|
|
43
|
+
resolve_assocs(reflection.klass, child, nested_column_or_assoc_hash)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
33
46
|
else
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def resolve_assocs_for_hash(klass, hash, assoc)
|
|
41
|
-
hash.each do |assoc_name, options|
|
|
42
|
-
if (reflection = klass.reflect_on_association(assoc_name))
|
|
43
|
-
assoc.current[assoc_name] = {}
|
|
44
|
-
assoc.current = assoc.current[assoc_name]
|
|
45
|
-
resolve_assocs(reflection.klass, options, assoc)
|
|
47
|
+
node.columns ||= []
|
|
48
|
+
node.columns << column_or_assoc_hash
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
end
|
|
@@ -75,7 +75,7 @@ module LogicalQueryParser
|
|
|
75
75
|
operator, logic = operator_and_logic
|
|
76
76
|
text = LogicalQueryParser.unquote(word.text_value)
|
|
77
77
|
|
|
78
|
-
sql = build_arel(params, operator, text).reduce(logic).to_sql
|
|
78
|
+
sql = build_arel(params[:root], operator, text).reduce(logic).to_sql
|
|
79
79
|
sql = "(#{sql})" if sql[0] != '(' && sql[-1] != ')'
|
|
80
80
|
params[:_sql] << sql
|
|
81
81
|
end
|
|
@@ -90,21 +90,12 @@ module LogicalQueryParser
|
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
def build_arel(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def build_arel_from_columns(klass, columns, operator, text)
|
|
102
|
-
columns.map { |column| klass.arel_table[column].send(operator, Arel.sql(klass.connection.quote("%#{text}%"))) }
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def build_arel_from_hash(klass, hash, operator, text)
|
|
106
|
-
hash.flat_map do |klass, columns|
|
|
107
|
-
build_arel_from_columns(klass, columns, operator, text)
|
|
93
|
+
def build_arel(root, operator, text)
|
|
94
|
+
assocs = [root] + root.descendants
|
|
95
|
+
assocs.flat_map do |assoc|
|
|
96
|
+
assoc.columns.map do |column|
|
|
97
|
+
assoc.arel_table[column].send(operator, Arel.sql(assoc.klass.connection.quote("%#{text}%")))
|
|
98
|
+
end
|
|
108
99
|
end
|
|
109
100
|
end
|
|
110
101
|
end
|
data/lib/logical_query_parser.rb
CHANGED
|
@@ -16,13 +16,14 @@ module LogicalQueryParser
|
|
|
16
16
|
|
|
17
17
|
def search(query, relations, *options)
|
|
18
18
|
relations = relations.all if relations.respond_to?(:all)
|
|
19
|
-
|
|
20
|
-
sql = new.parse(query).to_sql(
|
|
21
|
-
relations.
|
|
19
|
+
root = resolve_assocs(relations, *options)
|
|
20
|
+
sql = new.parse(query).to_sql(root: root)
|
|
21
|
+
relations = relations.left_joins(root.join_structure) unless root.join_structure.empty?
|
|
22
|
+
relations.where(sql)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
def resolve_assocs(
|
|
25
|
-
AssocResolver.new(
|
|
25
|
+
def resolve_assocs(relation, *options)
|
|
26
|
+
AssocResolver.new(relation, *options).call
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def walk_tree(node, &block)
|
|
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
|
|
24
24
|
spec.add_dependency "treetop", "~> 1.6.8"
|
|
25
25
|
|
|
26
|
+
spec.add_development_dependency "activerecord", ">= 7.0"
|
|
27
|
+
spec.add_development_dependency "sqlite3"
|
|
26
28
|
spec.add_development_dependency "bundler"
|
|
27
29
|
spec.add_development_dependency "irb"
|
|
28
30
|
spec.add_development_dependency "rake"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yoshikazu Kaneta
|
|
@@ -23,6 +23,34 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 1.6.8
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: sqlite3
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
26
54
|
- !ruby/object:Gem::Dependency
|
|
27
55
|
name: bundler
|
|
28
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -119,7 +147,7 @@ files:
|
|
|
119
147
|
- gemfiles/rails81.gemfile
|
|
120
148
|
- lib/logical_query_parser.rb
|
|
121
149
|
- lib/logical_query_parser.treetop
|
|
122
|
-
- lib/logical_query_parser/
|
|
150
|
+
- lib/logical_query_parser/assoc_node.rb
|
|
123
151
|
- lib/logical_query_parser/assoc_resolver.rb
|
|
124
152
|
- lib/logical_query_parser/nodes/active_record.rb
|
|
125
153
|
- lib/logical_query_parser/nodes/base.rb
|