query_dam 0.1.0 → 0.1.1
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/.travis.yml +4 -0
- data/README.md +7 -6
- data/Rakefile +5 -1
- data/lib/query_dam/concerns/trackable.rb +53 -30
- data/lib/query_dam/version.rb +1 -1
- data/lib/query_dam.rb +14 -11
- data/query_dam.gemspec +2 -0
- data/spec/query_dam_spec.rb +89 -36
- data/spec/spec_helper.rb +5 -0
- metadata +31 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 801eba4331fd983c0db84d269e8d327a82ebd5d1
|
4
|
+
data.tar.gz: 32988056b1e8a66e56b277ed380f13d338595186
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f6d274864e35f8692ced8092c5907624dcd9ffe26578db66559c4618e4067cb2ff2c5f2cd1afaaacbd447f8d191db889a05fcc8be87b639d0dd078e15456cc1
|
7
|
+
data.tar.gz: 83c8ed5b184a71a35eaf20b0362afa26de28affddcd6a8e5918a72d06c90fc02b50273396a8c9bc605b3d96e1e6da5dd9614758f80a2f7f4db3e0f66e7e0d2a0
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# QueryDam
|
2
|
+
[](https://travis-ci.org/romankuznietsov/query_dam)
|
2
3
|
|
3
4
|
QueryDam is a tool for observing changes in ActiveRecord relation results.
|
4
5
|
|
@@ -10,7 +11,7 @@ Add it to your gemfile Gemfile:
|
|
10
11
|
|
11
12
|
## Usage
|
12
13
|
|
13
|
-
First, include `QueryDam::Trackable` in every model you
|
14
|
+
First, include `QueryDam::Trackable` in every model you want to track.
|
14
15
|
|
15
16
|
~~~ruby
|
16
17
|
class Item < ActiveRecord::Base
|
@@ -18,7 +19,7 @@ class Item < ActiveRecord::Base
|
|
18
19
|
end
|
19
20
|
~~~
|
20
21
|
|
21
|
-
Then start watching updates of relation:
|
22
|
+
Then start watching updates of the relation:
|
22
23
|
|
23
24
|
~~~ruby
|
24
25
|
relation = Item.where(name: 'pencil')
|
@@ -27,9 +28,9 @@ tracking_key = QueryDam.watch_relation(relation)
|
|
27
28
|
|
28
29
|
To get updates, call `QueryDam.get_updates(tracking_key)`.
|
29
30
|
The result is a hash with 3 keys:
|
30
|
-
`:model` - the model of the relation,
|
31
|
-
`:updates` - relation containing records that were updated or entered the scope of the relation
|
32
|
-
`:exclusions` - array of ids of records that left the scope of the relation
|
31
|
+
* `:model` - the model of the relation,
|
32
|
+
* `:updates` - relation containing records that were updated or entered the scope of the relation
|
33
|
+
* `:exclusions` - array of ids of records that left the scope of the relation
|
33
34
|
|
34
35
|
The call to `get_updates` clears updates/exclusions.
|
35
36
|
|
@@ -47,7 +48,7 @@ QueryDam.get_updates(tracking_key)
|
|
47
48
|
# :updates=>#<ActiveRecord::Relation []>,
|
48
49
|
# :exclusions=>[]}
|
49
50
|
|
50
|
-
Item.first.
|
51
|
+
Item.first.destroy
|
51
52
|
|
52
53
|
QueryDam.get_updates(tracking_key)
|
53
54
|
# =>
|
data/Rakefile
CHANGED
@@ -1,51 +1,74 @@
|
|
1
|
+
require 'pry'
|
2
|
+
|
1
3
|
module QueryDam
|
2
4
|
module Trackable
|
3
5
|
extend ActiveSupport::Concern
|
4
6
|
|
5
7
|
included do
|
6
|
-
before_save :
|
7
|
-
before_destroy :
|
8
|
-
after_commit :
|
8
|
+
before_save :get_prior_results
|
9
|
+
before_destroy :get_prior_results
|
10
|
+
after_commit :update_queries
|
9
11
|
end
|
10
12
|
|
11
|
-
def
|
12
|
-
|
13
|
+
def get_prior_results
|
14
|
+
@_prior_results = {}
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
queries_set.members.each do |key|
|
17
|
-
query_hash = Redis::HashKey.new(key)
|
16
|
+
queries.each do |key, query_hash|
|
17
|
+
query = query_hash[:query]
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
@_prior_results[key] =
|
20
|
+
self.class.connection.select_values(query).map(&:to_i)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_queries
|
25
|
+
queries.each do |key, query_hash|
|
26
|
+
query = query_hash[:query]
|
27
|
+
result = self.class.connection.select_values(query).map(&:to_i)
|
28
|
+
added = result - @_prior_results[key]
|
29
|
+
removed = @_prior_results[key] - result
|
30
|
+
|
31
|
+
if result.include?(id)
|
32
|
+
QueryDam.updates_set(key) << id
|
33
|
+
end
|
34
|
+
|
35
|
+
additions_set = QueryDam.additions_set(key)
|
36
|
+
updates_set = QueryDam.updates_set(key)
|
37
|
+
removals_set = QueryDam.removals_set(key)
|
38
|
+
|
39
|
+
if result.include?(id)
|
40
|
+
updates_set.add(id)
|
22
41
|
end
|
23
42
|
|
24
|
-
|
43
|
+
added.each do |record_id|
|
44
|
+
if removals_set.member?(record_id)
|
45
|
+
removals_set.delete(record_id)
|
46
|
+
else
|
47
|
+
additions_set.add(record_id)
|
48
|
+
end
|
49
|
+
end
|
25
50
|
|
26
|
-
|
27
|
-
|
51
|
+
removed.each do |record_id|
|
52
|
+
updates_set.delete(record_id)
|
53
|
+
|
54
|
+
if additions_set.member?(record_id)
|
55
|
+
additions_set.delete(record_id)
|
56
|
+
else
|
57
|
+
removals_set.add(record_id)
|
58
|
+
end
|
59
|
+
end
|
28
60
|
end
|
29
61
|
end
|
30
62
|
|
31
|
-
def
|
63
|
+
def queries
|
32
64
|
queries_set = Redis::Set.new(self.class.name)
|
33
|
-
queries_set.members.
|
65
|
+
queries_set.members.reduce({}) do |acc, key|
|
34
66
|
query_hash = Redis::HashKey.new(key)
|
35
|
-
|
36
|
-
|
67
|
+
if query_hash.exists?
|
68
|
+
acc.merge(key => query_hash)
|
69
|
+
else
|
37
70
|
queries_set.delete(key)
|
38
|
-
|
39
|
-
end
|
40
|
-
|
41
|
-
query = query_hash[:query].gsub(key, id.to_s)
|
42
|
-
|
43
|
-
if self.class.find_by_sql(query).any?
|
44
|
-
QueryDam.updates_set(key) << id
|
45
|
-
QueryDam.exclusions_set(key).delete id
|
46
|
-
elsif @_query_dam_previous_status[key]
|
47
|
-
QueryDam.updates_set(key).delete id
|
48
|
-
QueryDam.exclusions_set(key) << id
|
71
|
+
acc
|
49
72
|
end
|
50
73
|
end
|
51
74
|
end
|
data/lib/query_dam/version.rb
CHANGED
data/lib/query_dam.rb
CHANGED
@@ -12,10 +12,10 @@ module QueryDam
|
|
12
12
|
class << self
|
13
13
|
def watch_relation(relation)
|
14
14
|
key = SecureRandom.uuid
|
15
|
-
query = relation.
|
15
|
+
query = relation.select("`#{relation.model.table_name}`.`id`").to_sql
|
16
16
|
query_hash = Redis::HashKey.new(key)
|
17
17
|
query_hash.bulk_set(model: relation.model.name, query: query)
|
18
|
-
query_hash.expire(QUERY_EXPIRE)
|
18
|
+
# query_hash.expire(QUERY_EXPIRE)
|
19
19
|
Redis::Set.new(relation.model.name) << key
|
20
20
|
key
|
21
21
|
end
|
@@ -26,23 +26,26 @@ module QueryDam
|
|
26
26
|
|
27
27
|
model = query_hash[:model].constantize
|
28
28
|
|
29
|
+
updated_ids = (additions_set(key).members +
|
30
|
+
updates_set(key).members).uniq
|
31
|
+
removed_ids = removals_set(key).members
|
32
|
+
|
29
33
|
result = {
|
30
34
|
model: model,
|
31
|
-
updates: model.where(id:
|
32
|
-
exclusions:
|
35
|
+
updates: model.where(id: updated_ids),
|
36
|
+
exclusions: removed_ids.map(&:to_i) }
|
33
37
|
|
38
|
+
additions_set(key).clear
|
34
39
|
updates_set(key).clear
|
35
|
-
|
40
|
+
removals_set(key).clear
|
36
41
|
|
37
42
|
result
|
38
43
|
end
|
39
44
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
def exclusions_set(key)
|
45
|
-
Redis::Set.new("#{key}-exclusions")
|
45
|
+
%w[additions updates removals].each do |set_type|
|
46
|
+
define_method "#{set_type}_set" do |key|
|
47
|
+
Redis::Set.new("#{key} #{set_type}")
|
48
|
+
end
|
46
49
|
end
|
47
50
|
end
|
48
51
|
end
|
data/query_dam.gemspec
CHANGED
@@ -26,4 +26,6 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency 'rake'
|
27
27
|
spec.add_development_dependency 'rspec', '~> 3.0.0'
|
28
28
|
spec.add_development_dependency 'sqlite3'
|
29
|
+
spec.add_development_dependency 'fakeredis'
|
30
|
+
spec.add_development_dependency 'pry'
|
29
31
|
end
|
data/spec/query_dam_spec.rb
CHANGED
@@ -1,53 +1,106 @@
|
|
1
1
|
describe QueryDam do
|
2
|
-
|
3
|
-
|
2
|
+
describe 'simple query' do
|
3
|
+
let!(:relation) { Item.where(name: 'pencil') }
|
4
|
+
let!(:key) { QueryDam.watch_relation(relation) }
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
specify 'item enters scope on creation' do
|
7
|
+
item = Item.create(name: 'pencil')
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
expect(QueryDam.get_updates(key)).to eq(
|
10
|
+
{model: Item, updates: [item], exclusions: []})
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
expect(QueryDam.get_updates(key)).to eq(
|
13
|
+
{model: Item, updates: [], exclusions: []})
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
specify 'item enters scope on update' do
|
17
|
+
item = Item.create
|
17
18
|
|
18
|
-
|
19
|
-
|
19
|
+
expect(QueryDam.get_updates(key)).to eq(
|
20
|
+
{model: Item, updates: [], exclusions: []})
|
20
21
|
|
21
|
-
|
22
|
+
item.update_attributes!(name: 'pencil')
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
expect(QueryDam.get_updates(key)).to eq(
|
25
|
+
{model: Item, updates: [item], exclusions: []})
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
specify 'item leaves scope on update' do
|
29
|
+
item = Item.create(name: 'pencil')
|
30
|
+
QueryDam.get_updates(key)
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
item.update_attributes!(name: 'pen')
|
33
|
+
expect(QueryDam.get_updates(key)).to eq(
|
34
|
+
{model: Item, updates: [], exclusions: [item.id]})
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
specify 'item leaves scope on deletion' do
|
38
|
+
item = Item.create(name: 'pencil')
|
39
|
+
QueryDam.get_updates(key)
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
item.destroy
|
42
|
+
expect(QueryDam.get_updates(key)).to eq(
|
43
|
+
{model: Item, updates: [], exclusions: [item.id]})
|
44
|
+
end
|
45
|
+
|
46
|
+
specify 'items gets updated' do
|
47
|
+
item = Item.create(name: 'pencil')
|
48
|
+
QueryDam.get_updates(key)
|
49
|
+
|
50
|
+
item.update_attributes!(quantity: 1)
|
51
|
+
expect(QueryDam.get_updates(key)).to eq(
|
52
|
+
{model: Item, updates: [item], exclusions: []})
|
53
|
+
end
|
43
54
|
end
|
44
55
|
|
45
|
-
|
46
|
-
|
47
|
-
|
56
|
+
describe 'query with limit/offset' do
|
57
|
+
let!(:item_a) { Item.create!(name: 'a') }
|
58
|
+
let!(:item_b) { Item.create!(name: 'b') }
|
59
|
+
let!(:item_c) { Item.create!(name: 'c') }
|
60
|
+
let!(:item_d) { Item.create!(name: 'd') }
|
61
|
+
|
62
|
+
let!(:relation) { Item.order('name asc').limit(2).offset(2) }
|
63
|
+
let!(:key) { QueryDam.watch_relation(relation) }
|
64
|
+
|
65
|
+
specify 'item is not on page, but shifts it' do
|
66
|
+
Item.create!(name: 'a1')
|
67
|
+
|
68
|
+
expect(QueryDam.get_updates(key)).to eq (
|
69
|
+
{model: Item, updates: [item_b], exclusions: [item_d.id]})
|
70
|
+
end
|
71
|
+
|
72
|
+
specify 'item appears on page' do
|
73
|
+
item_b1 = Item.create(name: 'b1')
|
74
|
+
expect(QueryDam.get_updates(key)).to eq (
|
75
|
+
{model: Item, updates: [item_b1], exclusions: [item_d.id]})
|
76
|
+
end
|
77
|
+
|
78
|
+
specify 'item on page is updated' do
|
79
|
+
item_c.update_attributes(quantity: 1)
|
80
|
+
expect(QueryDam.get_updates(key)).to eq (
|
81
|
+
{model: Item, updates: [item_c], exclusions: []})
|
82
|
+
end
|
83
|
+
|
84
|
+
specify 'two updates lead to no changes' do
|
85
|
+
item_c1 = Item.create(name: 'c1')
|
86
|
+
item_c1.destroy
|
87
|
+
expect(QueryDam.get_updates(key)).to eq (
|
88
|
+
{model: Item, updates: [], exclusions: []})
|
89
|
+
end
|
90
|
+
|
91
|
+
specify 'item is updated and removed' do
|
92
|
+
item_d.update_attributes!(quantity: 1)
|
93
|
+
item_d.destroy
|
94
|
+
expect(QueryDam.get_updates(key)).to eq (
|
95
|
+
{model: Item, updates: [], exclusions: [item_d.id]})
|
96
|
+
end
|
48
97
|
|
49
|
-
item
|
50
|
-
|
51
|
-
|
98
|
+
specify 'item is added, updated and removed' do
|
99
|
+
item_c1 = Item.create(name: 'c1')
|
100
|
+
item_c1.update_attributes(quantity: 1)
|
101
|
+
item_c1.destroy
|
102
|
+
expect(QueryDam.get_updates(key)).to eq (
|
103
|
+
{model: Item, updates: [], exclusions: []})
|
104
|
+
end
|
52
105
|
end
|
53
106
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'query_dam'
|
2
2
|
require 'active_record'
|
3
3
|
require 'models/item'
|
4
|
+
require 'fakeredis/rspec'
|
4
5
|
|
5
6
|
RSpec.configure do |config|
|
6
7
|
config.order = :random
|
@@ -13,6 +14,10 @@ RSpec.configure do |config|
|
|
13
14
|
mocks.syntax = :expect
|
14
15
|
mocks.verify_partial_doubles = true
|
15
16
|
end
|
17
|
+
|
18
|
+
config.after(:each) do
|
19
|
+
Item.destroy_all
|
20
|
+
end
|
16
21
|
end
|
17
22
|
|
18
23
|
ActiveRecord::Base.establish_connection(
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: query_dam
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Kuznietsov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -108,6 +108,34 @@ dependencies:
|
|
108
108
|
- - '>='
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: fakeredis
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
111
139
|
description: ''
|
112
140
|
email:
|
113
141
|
- roman.kuznietsov@gmail.com
|
@@ -117,6 +145,7 @@ extra_rdoc_files: []
|
|
117
145
|
files:
|
118
146
|
- .gitignore
|
119
147
|
- .rspec
|
148
|
+
- .travis.yml
|
120
149
|
- Gemfile
|
121
150
|
- LICENSE.txt
|
122
151
|
- README.md
|