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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22d02f1282d55df340c385cd13c61186f4230f22f8282c7fd67b781dacd2fbd0
4
- data.tar.gz: b952657d8f60d182c037c308005b5e6fca626b2a607fbc6d8892b93b48e74daa
3
+ metadata.gz: 50d9e7d0d793b457dd2561160c3fc0e2a0611d7425c671a624c19c5e7395fd92
4
+ data.tar.gz: cc8b9d94d8c13c5764e82b64be14d6b68e050f8d7fe9a3b53c624aa973dbb949
5
5
  SHA512:
6
- metadata.gz: 4eba4f1502637981543337180b179dbd03ee0cb7eabc4215cf256a3f1654bb3bbd2db9b4690e4dc9c1f81f4a3fa2a7bdae3da3b9c75700bcd693f6b9c877ebaa
7
- data.tar.gz: 256ad90335de5fe846cd6b38620b7dbca8f480c1a3783141adcb53691f28cd6172df75774c51eabca395562b340c493992c2935504b00cc32a7886b8dac5013f
6
+ metadata.gz: fd0590d955cba3c8b10d2441e62d720131bffb46ecbfbbf5df3ed49fc1e27aeb1a81a28e2ae18d3cdd50e553a0802a24b66b3c5d77b15395391bae7a18e095e4
7
+ data.tar.gz: 678c2067c0dae4587dde4158d251ccb7643479af9ff0c894be7c3410a02c2970a0e83ea8275a300d0ed2b89f08aaa98ddabd0332dca40e62033d00d65c76f208
@@ -4,7 +4,7 @@ on: [push, pull_request]
4
4
 
5
5
  jobs:
6
6
  test:
7
- runs-on: ubuntu-22.04
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@v4
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
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.6.0
4
+
5
+ * Support multiple associations and alias table names for `search` method.
6
+ * Change join method from `joins` to `left_joins` for `search` method.
7
+
3
8
  ## 0.5.0
4
9
 
5
10
  * Drop support for ruby <= 2.7, rails <= 6.1.
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
- # INNER JOIN "tags" ON "tags"."doc_id" = "docs"."id"
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 'assoc'
3
+ require_relative 'assoc_node'
4
4
 
5
5
  module LogicalQueryParser
6
6
  class AssocResolver
7
- def initialize(klass)
8
- @klass = klass
7
+ def initialize(relation, *options)
8
+ @relation = relation
9
+ @options = options.flatten(1)
9
10
  end
10
11
 
11
- def run(*args)
12
- Assoc.new.tap do |assoc|
13
- assoc.current = assoc.structure
14
- resolve_assocs(@klass, args, assoc)
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(klass, options, assoc)
29
- options = wrap_array(options)
30
- options.each do |option|
31
- if option.is_a?(Hash)
32
- resolve_assocs_for_hash(klass, option, assoc)
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
- assoc.column_mapping[klass] ||= []
35
- assoc.column_mapping[klass] << option
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(params, operator, text)
94
- if params[:columns].is_a?(Hash)
95
- build_arel_from_hash(params[:model], params[:columns], operator, text)
96
- else
97
- build_arel_from_columns(params[:model], params[:columns], operator, text)
98
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogicalQueryParser
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -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
- assoc = resolve_assocs(relations.klass, *options)
20
- sql = new.parse(query).to_sql(model: relations.klass, columns: assoc.column_mapping)
21
- relations.joins(assoc.structure).where(sql)
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(klass, *options)
25
- AssocResolver.new(klass).run(*options)
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.5.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/assoc.rb
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
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LogicalQueryParser
4
- class Assoc
5
- attr_accessor :column_mapping, :structure
6
- attr_accessor :current
7
-
8
- def initialize(attrs = {})
9
- @column_mapping = {}
10
- @structure = {}
11
- end
12
- end
13
- end