pgrel 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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