pgrel 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
2
  SHA256:
3
- metadata.gz: 43d4c6d3c7078b0823c3da86d1749db92e7b86f9759fb17326b3065780290649
4
- data.tar.gz: ebf600d3ccb22d4bed4dc4f6a479c7f3c1aa65e300edebbcaaded0da9e8698cc
3
+ metadata.gz: 62a79ba274061f7a36972f8817c5ee5b61242af5d92d8d7ca77f776cfb1ee802
4
+ data.tar.gz: f8c8da31f5ce40f8c1e3400dd154aac9f1a8309768586d7f1049e733a1e5650b
5
5
  SHA512:
6
- metadata.gz: c7a7c10c700279fbc1943d44e478ddbc096fe35af24d1733b4cc95bf564322580c177abbc56c3e76f2ffcd69053229badab34b5e9cc53b496e1d8a638118f662
7
- data.tar.gz: b8381c2b638a70e58c9159b3a8ab2a3ecd2261a305d376fa2346f91dc7130ff163caafbe237bfe4f0c3c503f51c28be15dbb95e7e20421478187f4bc29c4792e
6
+ metadata.gz: 34d502c9098aa129c3a64acd5d48d722e31d38b3ececf9d930d9629da226e73aa9077643882341e6a01a9cf1ae95158348347a3b98f2ee92319b553aede53c8e
7
+ data.tar.gz: e7b38cbd1eaa94fffbecaa093caa87ebf4ae73aa128139db1efd8d022047d6775dbbf4452cb2e5ff94ccc8fc6dbc1cf0d93b29ee762230b8c2aafc9f6f450549
data/.gitignore CHANGED
@@ -33,4 +33,5 @@ spec/dummy/log/*.log
33
33
  spec/dummy/tmp/
34
34
  spec/dummy/.sass-cache
35
35
  Gemfile.local
36
- Gemfile.lock
36
+ Gemfile.lock
37
+ .rspec_status
@@ -10,7 +10,9 @@ before_script:
10
10
 
11
11
  matrix:
12
12
  include:
13
- - rvm: 2.5.0
13
+ - rvm: 2.6.0
14
+ gemfile: gemfiles/railsmaster.gemfile
15
+ - rvm: 2.6.0
14
16
  gemfile: gemfiles/rails5.gemfile
15
17
  - rvm: 2.4.3
16
18
  gemfile: gemfiles/rails5.gemfile
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.3.0 (2019-28-01)
6
+
7
+ - Rename `#value` method to `#overlap_values`.
8
+ - Rename `#values` method to `#contains_values`.
9
+ - Improve `#contains_values` method:
10
+ eliminate multiple `avals` calls for Hstore and multiple `array_agg` calls for Jsonb.
11
+
12
+ See https://github.com/palkan/pgrel/pull/9. ([@StanisLove][])
13
+
14
+ - Quote store name in queries. ([@palkan][])
15
+
16
+ Previously, we didn't quote store name in queries which could led
17
+ to ambiguity conflicts when joining tables.
18
+ Now it fixed.
19
+
5
20
  ## 0.2.0 (2018-06-15)
6
21
 
7
22
  - Add `#update_store` methods. ([@StanisLove][])
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ActiveRecord extension for querying hstore, array and jsonb.
6
6
 
7
- Compatible with **Rails** >= 4.2.
7
+ Compatible with **Rails** >= 4.2 (including **Rails 6**).
8
8
 
9
9
  #### Install
10
10
 
@@ -57,11 +57,11 @@ Values existence:
57
57
 
58
58
  ```ruby
59
59
  # Retrieve items that have value '1' OR '2'
60
- Hstore.where.store(:tags).value(1, 2)
61
- #=> select * from hstores where (avals(tags) @> ARRAY['1'] OR avals(tags) @> ARRAY['2'] )
60
+ Hstore.where.store(:tags).overlap_values(1, 2)
61
+ #=> select * from hstores where (avals(tags) && ARRAY['1', '2'])
62
62
 
63
63
  # Retrieve items that have values '1' AND '2'
64
- Hstore.where.store(:tags).values(1, 2)
64
+ Hstore.where.store(:tags).contains_values(1, 2)
65
65
  #=> select * from hstores where (avals(tags) @> ARRAY['1', '2'])
66
66
  ```
67
67
 
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rails', github: 'rails/rails'
4
+
5
+ gemspec path: '..'
@@ -6,10 +6,13 @@ module ActiveRecord
6
6
  # Provides _containment_ queries methods.
7
7
  # Provides basic methods.
8
8
  class StoreChain
9
+ attr_reader :store_name, :quoted_store_name
10
+
9
11
  def initialize(scope, store_name)
10
12
  @scope = scope
11
13
  @store_name = store_name
12
14
  @inverted = false
15
+ @quoted_store_name = "#{@scope.klass.quoted_table_name}.#{@scope.klass.connection.quote_column_name(store_name)}"
13
16
  end
14
17
 
15
18
  # Whether the store contains provided store
@@ -33,7 +36,7 @@ module ActiveRecord
33
36
  # data = {b: 1, c: 2}
34
37
  # Model.store(:store).contains(data).all #=> [Model(name: 'first', ...)]
35
38
  def contained(opts)
36
- update_scope "#{@store_name} <@ #{type_cast(opts)}"
39
+ update_scope "#{quoted_store_name} <@ #{type_cast(opts)}"
37
40
  end
38
41
 
39
42
  # Add negation to condition.
@@ -135,7 +138,7 @@ module ActiveRecord
135
138
  private
136
139
 
137
140
  def contains_clause(opts)
138
- "#{@store_name} @> #{type_cast(opts)}"
141
+ "#{quoted_store_name} @> #{type_cast(opts)}"
139
142
  end
140
143
 
141
144
  def build_or_contains(k, vals)
@@ -154,7 +157,7 @@ module ActiveRecord
154
157
  # # Get all records which have key 'a' in store 'store'
155
158
  # Model.store(:store).key('a').all #=> [Model(name: 'first', ...)]
156
159
  def key(key)
157
- update_scope "#{@store_name} ? :key", key: key.to_s
160
+ update_scope "#{quoted_store_name} ? :key", key: key.to_s
158
161
  end
159
162
 
160
163
  # Several keys existence
@@ -166,7 +169,7 @@ module ActiveRecord
166
169
  # Model.store(:store).keys('a','b').all #=> [Model(name: 'first', ...)]
167
170
  def keys(*keys)
168
171
  update_scope(
169
- "#{@store_name} ?& ARRAY[:keys]",
172
+ "#{quoted_store_name} ?& ARRAY[:keys]",
170
173
  keys: keys.flatten.map(&:to_s)
171
174
  )
172
175
  end
@@ -180,7 +183,7 @@ module ActiveRecord
180
183
  # Model.store(:store).keys('a','b').count #=> 2
181
184
  def any(*keys)
182
185
  update_scope(
183
- "#{@store_name} ?| ARRAY[:keys]",
186
+ "#{quoted_store_name} ?| ARRAY[:keys]",
184
187
  keys: keys.flatten.map(&:to_s)
185
188
  )
186
189
  end
@@ -13,7 +13,7 @@ module ActiveRecord
13
13
  # Model.store(:store).overlap('c').all #=> [Model(name: 'first', ...)]
14
14
  # Model.store(:store).overlap(['b']).size #=> 2
15
15
  def overlap(*vals)
16
- update_scope "#{@store_name} && #{type_cast(vals.flatten)}"
16
+ update_scope "#{quoted_store_name} && #{type_cast(vals.flatten)}"
17
17
  end
18
18
  end
19
19
  end
@@ -4,32 +4,27 @@ module ActiveRecord
4
4
  module QueryMethods
5
5
  # Store chain for hstore columns.
6
6
  class HstoreChain < KeyStoreChain
7
- # Value existence
7
+ # Overlap values
8
8
  #
9
9
  # Example
10
10
  # Model.create!(name: 'first', store: {a: 1, b: 2})
11
11
  # Model.create!(name: 'second', store: {b: 1, c: 3})
12
12
  #
13
- # Model.store(:store).value(1, 2).all
13
+ # Model.store(:store).overlap_values(1, 2).all
14
14
  # #=>[Model(name: 'first', ...), Model(name: 'second')]
15
- def value(*values)
16
- query = String.new
17
- values.length.times do |n|
18
- query.concat("avals(#{@store_name}) @> ARRAY[?]")
19
- query.concat(' OR ') if n < values.length - 1
20
- end
21
- update_scope(query, *values.map(&:to_s))
15
+ def overlap_values(*values)
16
+ update_scope("avals(#{quoted_store_name}) && ARRAY[?]", values.map(&:to_s))
22
17
  end
23
18
 
24
- # Values existence
19
+ # Contains values
25
20
  #
26
21
  # Example
27
22
  # Model.create!(name: 'first', store: {a: 1, b: 2})
28
23
  # Model.create!(name: 'second', store: {b: 1, c: 3})
29
24
  #
30
- # Model.store(:store).values(1, 2).all #=> [Model(name: 'first', ...)]
31
- def values(*values)
32
- update_scope("avals(#{@store_name}) @> ARRAY[?]", values.map(&:to_s))
25
+ # Model.store(:store).contains_values(1, 2).all #=> [Model(name: 'first', ...)]
26
+ def contains_values(*values)
27
+ update_scope("avals(#{quoted_store_name}) @> ARRAY[?]", values.map(&:to_s))
33
28
  end
34
29
  end
35
30
  end
@@ -4,6 +4,8 @@ module ActiveRecord
4
4
  module QueryMethods
5
5
  # Store chain for jsonb columns.
6
6
  class JsonbChain < KeyStoreChain
7
+ OPERATORS = { contains: '@>', overlap: '&&' }.freeze
8
+
7
9
  # Query by value in path.
8
10
  #
9
11
  # Example:
@@ -30,52 +32,30 @@ module ActiveRecord
30
32
  val = val.to_s
31
33
  end
32
34
 
33
- where_with_prefix "#{@store_name}#{op}", path => val
35
+ where_with_prefix "#{quoted_store_name}#{op}", path => val
34
36
  end
35
37
 
36
- # Value existence
38
+ # Overlap values
37
39
  #
38
40
  # Example
39
41
  # Model.create!(name: 'first', store: {a: 1, b: 2})
40
42
  # Model.create!(name: 'second', store: {b: 1, c: 3})
41
43
  #
42
- # Model.store(:store).values(1, 2).all
44
+ # Model.store(:store).overlap_values(1, 2).all
43
45
  # #=>[Model(name: 'first', ...), Model(name: 'second')]
44
- def value(*values)
45
- query = String.new
46
- values = values.map do |v|
47
- case v
48
- when Hash, Array, String
49
- v.to_json
50
- else
51
- v.to_s
52
- end
53
- end
54
-
55
- values.length.times do |n|
56
- query.concat(value_existence_query)
57
- query.concat(' OR ') if n < values.length - 1
58
- end
59
- update_scope(query, *values)
46
+ def overlap_values(*values)
47
+ update_scope(value_query(:overlap), cast_values(values))
60
48
  end
61
49
 
62
- # Values existence
50
+ # Contains values
63
51
  #
64
52
  # Example
65
53
  # Model.create!(name: 'first', store: {a: 1, b: 2})
66
54
  # Model.create!(name: 'second', store: {b: 1, c: 3})
67
55
  #
68
- # Model.store(:store).values(1, 2).all #=> [Model(name: 'first', ...)]
69
- def values(*values)
70
- values = values.map do |v|
71
- case v
72
- when Hash, Array, String
73
- v.to_json
74
- else
75
- v.to_s
76
- end
77
- end
78
- update_scope(value_existence_query, values)
56
+ # Model.store(:store).contains_values(1, 2).all #=> [Model(name: 'first', ...)]
57
+ def contains_values(*values)
58
+ update_scope(value_query(:contains), cast_values(values))
79
59
  end
80
60
 
81
61
  private
@@ -91,8 +71,20 @@ module ActiveRecord
91
71
  end
92
72
  end
93
73
 
94
- def value_existence_query
95
- "(SELECT array_agg(value) FROM jsonb_each(#{@store_name})) @> ARRAY[?]::jsonb[]"
74
+ def value_query(operator)
75
+ oper = OPERATORS[operator]
76
+ "(SELECT array_agg(value) FROM jsonb_each(#{quoted_store_name})) #{oper} ARRAY[?]::jsonb[]"
77
+ end
78
+
79
+ def cast_values(values)
80
+ values.map do |v|
81
+ case v
82
+ when Hash, Array, String
83
+ v.to_json
84
+ else
85
+ v.to_s
86
+ end
87
+ end
96
88
  end
97
89
  end
98
90
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgrel # :nodoc:
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe ArrayStore do
4
- before do
4
+ before(:all) do
5
5
  @connection = ActiveRecord::Base.connection
6
6
 
7
7
  @connection.transaction do
@@ -14,7 +14,9 @@ describe ArrayStore do
14
14
  ArrayStore.reset_column_information
15
15
  end
16
16
 
17
- after do
17
+ after { ArrayStore.delete_all }
18
+
19
+ after(:all) do
18
20
  @connection.drop_table 'array_stores', if_exists: true
19
21
  end
20
22
 
@@ -68,4 +70,18 @@ describe ArrayStore do
68
70
  expect(ArrayStore.where.store(:tags).not.overlap('b', 2).size).to eq 1
69
71
  end
70
72
  end
73
+
74
+ context "joins" do
75
+ before do
76
+ User.create!(name: "x", array_store: ArrayStore.find_by!(name: "a"))
77
+ User.create!(name: "y", array_store: ArrayStore.find_by!(name: "b"))
78
+ User.create!(name: "z", array_store: ArrayStore.find_by!(name: "c"))
79
+ end
80
+
81
+ it "works" do
82
+ users = User.joins(:array_store).merge(ArrayStore.where.store(:tags).overlap(2))
83
+ expect(users.size).to eq 2
84
+ expect(users.map(&:name)).to match_array(["x", "y"])
85
+ end
86
+ end
71
87
  end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Hstore do
4
- before do
4
+ before(:all) do
5
5
  @connection = ActiveRecord::Base.connection
6
6
 
7
7
  @connection.transaction do
@@ -14,7 +14,9 @@ describe Hstore do
14
14
  Hstore.reset_column_information
15
15
  end
16
16
 
17
- after do
17
+ after { Hstore.delete_all }
18
+
19
+ after(:all) do
18
20
  @connection.drop_table 'hstores', if_exists: true
19
21
  end
20
22
 
@@ -99,23 +101,30 @@ describe Hstore do
99
101
  expect(records.first.name).to eq 'e'
100
102
  end
101
103
 
102
- it '#value' do
103
- records = Hstore.where.store(:tags).value(1, false, [1, 2, { a: 1 }])
104
- expect(records.size).to eq 3
104
+ describe '#overlap_values' do
105
+ let(:records) { Hstore.where.store(:tags).overlap_values(1, false, [1, 2, { a: 1 }]) }
106
+
107
+ it 'returns records with overlapping values' do
108
+ expect(records.size).to eq 3
109
+ end
110
+
111
+ it 'calls avals function only once' do
112
+ expect(records.to_sql.scan(/avals/).count).to eq 1
113
+ end
105
114
  end
106
115
 
107
- it '#values' do
108
- records = Hstore.where.store(:tags).values(1)
116
+ it '#contains_values' do
117
+ records = Hstore.where.store(:tags).contains_values(1)
109
118
  expect(records.size).to eq 1
110
119
  expect(records.first.name).to eq 'a'
111
120
 
112
- records = Hstore.where.store(:tags).values('2', 'b')
121
+ records = Hstore.where.store(:tags).contains_values('2', 'b')
113
122
  expect(records.size).to eq 2
114
123
 
115
- records = Hstore.where.store(:tags).values(true)
124
+ records = Hstore.where.store(:tags).contains_values(true)
116
125
  expect(records.size).to eq 2
117
126
 
118
- records = Hstore.where.store(:tags).values([1, 2, { a: 1 }], { a: 1, b: [1], f: false })
127
+ records = Hstore.where.store(:tags).contains_values([1, 2, { a: 1 }], { a: 1, b: [1], f: false })
119
128
  expect(records.size).to eq 1
120
129
  end
121
130
 
@@ -205,4 +214,24 @@ describe Hstore do
205
214
  expect(Hstore.where.store(store, a: 2)).to exist
206
215
  end
207
216
  end
217
+
218
+ context "joins" do
219
+ before do
220
+ User.create!(name: "x", hstore: Hstore.find_by!(name: "a"))
221
+ User.create!(name: "y", hstore: Hstore.find_by!(name: "b"))
222
+ User.create!(name: "z", hstore: Hstore.find_by!(name: "c"))
223
+ end
224
+
225
+ it "works" do
226
+ users = User.joins(:hstore).merge(Hstore.where.store(:tags).key(:a))
227
+ expect(users.size).to eq 2
228
+ expect(users.map(&:name)).to match_array(["x", "y"])
229
+ end
230
+
231
+ it "works with #contains_values" do
232
+ users = User.joins(:hstore).merge(Hstore.where.store(:tags).contains_values(1))
233
+ expect(users.size).to eq 1
234
+ expect(users.map(&:name)).to match_array(["x"])
235
+ end
236
+ end
208
237
  end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Jsonb do
4
- before do
4
+ before(:all) do
5
5
  @connection = ActiveRecord::Base.connection
6
6
 
7
7
  @connection.transaction do
@@ -14,7 +14,9 @@ describe Jsonb do
14
14
  Jsonb.reset_column_information
15
15
  end
16
16
 
17
- after do
17
+ after { Jsonb.delete_all }
18
+
19
+ after(:all) do
18
20
  @connection.drop_table 'jsonbs', if_exists: true
19
21
  end
20
22
 
@@ -108,23 +110,30 @@ describe Jsonb do
108
110
  expect(records.first.name).to eq 'e'
109
111
  end
110
112
 
111
- it '#value' do
112
- records = Jsonb.where.store(:tags).value(1, false, { e: 2 })
113
- expect(records.size).to eq 3
113
+ describe '#overlap_values' do
114
+ let(:records) { Jsonb.where.store(:tags).overlap_values(1, false, { e: 2 }) }
115
+
116
+ it 'returns records with overlapping values' do
117
+ expect(records.size).to eq 3
118
+ end
119
+
120
+ it 'calls array_agg function only once' do
121
+ expect(records.to_sql.scan(/array_agg/).count).to eq 1
122
+ end
114
123
  end
115
124
 
116
- it '#values' do
117
- records = Jsonb.where.store(:tags).values(1)
125
+ it '#contains_values' do
126
+ records = Jsonb.where.store(:tags).contains_values(1)
118
127
  expect(records.size).to eq 2
119
128
 
120
- records = Jsonb.where.store(:tags).values(2, 'e')
129
+ records = Jsonb.where.store(:tags).contains_values(2, 'e')
121
130
  expect(records.size).to eq 1
122
131
  expect(records.first.name).to eq 'e'
123
132
 
124
- records = Jsonb.where.store(:tags).values(e: 1, f: { h: { k: 'a', s: 2 } })
133
+ records = Jsonb.where.store(:tags).contains_values(e: 1, f: { h: { k: 'a', s: 2 } })
125
134
  expect(records.size).to eq 1
126
135
 
127
- records = Jsonb.where.store(:tags).values(false, { a: 1, b: '1' }, [1, '1'])
136
+ records = Jsonb.where.store(:tags).contains_values(false, { a: 1, b: '1' }, [1, '1'])
128
137
  expect(records.size).to eq 1
129
138
  end
130
139
 
@@ -201,4 +210,30 @@ describe Jsonb do
201
210
  expect(Jsonb.where.store(store, d: { e: 2 })).to_not exist
202
211
  end
203
212
  end
213
+
214
+ context "joins" do
215
+ before do
216
+ User.create!(name: "x", jsonb: Jsonb.find_by!(name: "a"))
217
+ User.create!(name: "y", jsonb: Jsonb.find_by!(name: "b"))
218
+ User.create!(name: "z", jsonb: Jsonb.find_by!(name: "c"))
219
+ end
220
+
221
+ it "works" do
222
+ users = User.joins(:jsonb).merge(Jsonb.where.store(:tags).key(:a))
223
+ expect(users.size).to eq 2
224
+ expect(users.map(&:name)).to match_array(["y", "z"])
225
+ end
226
+
227
+ it "works with #path" do
228
+ users = User.joins(:jsonb).merge(Jsonb.where.store(:tags).path(:a, 2))
229
+ expect(users.size).to eq 1
230
+ expect(users.map(&:name)).to match_array(["z"])
231
+ end
232
+
233
+ it "works with #contains_values" do
234
+ users = User.joins(:jsonb).merge(Jsonb.where.store(:tags).contains_values(1))
235
+ expect(users.size).to eq 1
236
+ expect(users.map(&:name)).to match_array(["y"])
237
+ end
238
+ end
204
239
  end
@@ -30,5 +30,15 @@ connection.reconnect!
30
30
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
31
31
 
32
32
  RSpec.configure do |config|
33
- config.mock_with :rspec
33
+ config.example_status_persistence_file_path = '.rspec_status'
34
+ config.filter_run focus: true
35
+ config.run_all_when_everything_filtered = true
36
+
37
+ config.order = :random
38
+
39
+ config.expect_with :rspec do |c|
40
+ c.syntax = :expect
41
+ end
42
+
43
+ config.after(:each) { User.delete_all }
34
44
  end
@@ -0,0 +1,16 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :users, force: true do |t|
3
+ t.string :name
4
+ t.string :tags
5
+ t.integer :jsonb_id
6
+ t.integer :array_store_id
7
+ t.integer :hstore_id
8
+ t.timestamps null: true
9
+ end
10
+ end
11
+
12
+ class User < ActiveRecord::Base
13
+ belongs_to :jsonb
14
+ belongs_to :array_store
15
+ belongs_to :hstore
16
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgrel
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
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-15 00:00:00.000000000 Z
11
+ date: 2019-01-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -111,6 +111,7 @@ files:
111
111
  - Rakefile
112
112
  - gemfiles/rails42.gemfile
113
113
  - gemfiles/rails5.gemfile
114
+ - gemfiles/railsmaster.gemfile
114
115
  - lib/pgrel.rb
115
116
  - lib/pgrel/active_record.rb
116
117
  - lib/pgrel/active_record/query_methods.rb
@@ -132,6 +133,7 @@ files:
132
133
  - spec/support/array_store.rb
133
134
  - spec/support/hstore.rb
134
135
  - spec/support/jsonb.rb
136
+ - spec/support/user.rb
135
137
  homepage: http://github.com/palkan/pgrel
136
138
  licenses:
137
139
  - MIT