partial_ks 0.1.1 → 0.2.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/CHANGES.md +23 -0
- data/README.md +26 -9
- data/lib/partial_ks/filtered_table.rb +30 -23
- data/lib/partial_ks/parent_inferrer.rb +15 -13
- data/lib/partial_ks/runner.rb +51 -49
- data/lib/partial_ks/table.rb +14 -0
- data/lib/partial_ks/version.rb +1 -1
- data/lib/partial_ks.rb +0 -3
- data/test/filtered_table_test.rb +23 -12
- data/test/setup_models.rb +3 -0
- data/test/table_test.rb +35 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 073f6737128fd06c8791fb210f72d3a657a459de
|
4
|
+
data.tar.gz: 4ea3290c59333b803b506ad70d7f81480513737d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99dcd5e6f8a03017b98ef20099683108fd9173962865324bcb3166fc80bbf07ff5c36e7c5668ea138558c22a61a0278489570b5f5454bb1c461578f6c48ac7d0
|
7
|
+
data.tar.gz: 4f29d6e17164c9d25ff214c210b4ca5355560c898e3d3230ff59aa086f9d444eb9529c8422236179784439004db0eea55ca02ab9502f63804f5680986cc215cc
|
data/CHANGES.md
CHANGED
@@ -1,5 +1,28 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
# 0.2.0
|
4
|
+
|
5
|
+
* Support filtering via a has_many association
|
6
|
+
|
7
|
+
You can now skip manually specifying the resultant filter on a has_many "parent". So previously:
|
8
|
+
|
9
|
+
```
|
10
|
+
[
|
11
|
+
[User, Tags, User.where(:id => Tags.pluck(:id))]
|
12
|
+
]
|
13
|
+
```
|
14
|
+
|
15
|
+
can now just be :
|
16
|
+
|
17
|
+
```
|
18
|
+
[
|
19
|
+
[User, Tags]
|
20
|
+
]
|
21
|
+
```
|
22
|
+
|
23
|
+
* Can now specify a full SQL statement in the manual specification
|
24
|
+
|
25
|
+
|
3
26
|
# 0.1.1
|
4
27
|
|
5
28
|
Fix minor regression with Runner#report
|
data/README.md
CHANGED
@@ -1,17 +1,19 @@
|
|
1
|
-
PartialKs
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
# PartialKs
|
2
|
+
|
3
|
+
A library to use kitchen-sync to sync a subset of your database
|
4
|
+
|
5
|
+
# Usage
|
6
6
|
|
7
7
|
So how does it work ?
|
8
8
|
|
9
|
-
`brew install kitchen-sync`
|
10
9
|
|
11
10
|
```
|
12
11
|
manual_configuration = []
|
13
12
|
config = PartialKs::ConfigurationGenerator.new(manual_configuration).call
|
14
|
-
PartialKs::Runner.new([config]).run!
|
13
|
+
PartialKs::Runner.new([config]).run! do |tables_to_filter, tables|
|
14
|
+
puts tables_to_filter.inspect
|
15
|
+
puts tables.inspect
|
16
|
+
end
|
15
17
|
```
|
16
18
|
|
17
19
|
You can specify manual configurations if needed.
|
@@ -23,11 +25,26 @@ manual_configuration = [
|
|
23
25
|
]
|
24
26
|
```
|
25
27
|
|
26
|
-
|
28
|
+
NB: The first use case for this gem is to be run in conjuction with [Kitchen Sync](https://github.com/willbryant/kitchen_sync). On OSX, one can install Kitchen Sync using `brew install kitchen-sync`
|
29
|
+
|
30
|
+
*TODO*
|
31
|
+
|
32
|
+
* Provide a way for users to pass in manual configurations
|
33
|
+
* Tool to run report using bundle exec
|
34
|
+
|
35
|
+
# Public API
|
36
|
+
|
37
|
+
It currently consists of :
|
38
|
+
|
39
|
+
- ConfigurationGenerator
|
40
|
+
- Runner
|
41
|
+
- runs
|
42
|
+
- reports (mostly for debugging)
|
43
|
+
|
44
|
+
*TODO*
|
27
45
|
|
28
46
|
* Rename PartialKs::ConfigurationGenerator#call to something better
|
29
47
|
* Minimize Public API
|
30
|
-
* Tool to run report using bundle exec
|
31
48
|
|
32
49
|
# Not supported
|
33
50
|
|
@@ -1,32 +1,39 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module PartialKs
|
2
|
+
class FilteredTable
|
3
|
+
attr_reader :table, :parent_model, :custom_filter_relation
|
4
|
+
delegate :table_name, :to => :table
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def kitchen_sync_filter
|
12
|
-
filter_condition = custom_filter_relation || parent_model
|
6
|
+
def initialize(table, parent_model, custom_filter_relation: nil)
|
7
|
+
@table = table
|
8
|
+
@parent_model = parent_model
|
9
|
+
@custom_filter_relation = custom_filter_relation
|
10
|
+
end
|
13
11
|
|
14
|
-
|
15
|
-
if
|
16
|
-
|
17
|
-
elsif
|
18
|
-
|
12
|
+
def kitchen_sync_filter
|
13
|
+
if custom_filter_relation
|
14
|
+
{"only" => filter_based_on_custom_filter_relation}
|
15
|
+
elsif parent_model
|
16
|
+
{"only" => filter_based_on_parent_model}
|
19
17
|
else
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
def filter_based_on_parent_model
|
24
|
+
table.relation_for_associated_model(parent_model).where_sql.to_s.sub(where_regexp, "")
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
+
def filter_based_on_custom_filter_relation
|
28
|
+
if custom_filter_relation.is_a?(ActiveRecord::Relation) || custom_filter_relation.respond_to?(:where_sql)
|
29
|
+
only_filter = custom_filter_relation.where_sql.to_s.sub(where_regexp, "")
|
30
|
+
elsif custom_filter_relation.is_a?(String)
|
31
|
+
only_filter = custom_filter_relation.sub(where_regexp, "")
|
27
32
|
end
|
33
|
+
end
|
28
34
|
|
29
|
-
|
35
|
+
def where_regexp
|
36
|
+
/\A.*WHERE\s*/i
|
30
37
|
end
|
31
38
|
end
|
32
39
|
end
|
@@ -1,19 +1,21 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module PartialKs
|
2
|
+
class ParentInferrer
|
3
|
+
CannotInfer = Class.new(StandardError)
|
3
4
|
|
4
|
-
|
5
|
+
attr_reader :table
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
def initialize(table)
|
8
|
+
@table = table
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
def inferred_parent_class
|
12
|
+
if table.top_level_table?
|
13
|
+
nil
|
14
|
+
elsif table.candidate_parent_classes.size == 1
|
15
|
+
table.candidate_parent_classes.first
|
16
|
+
else
|
17
|
+
raise CannotInfer, "table has multiple candidates for parents"
|
18
|
+
end
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
data/lib/partial_ks/runner.rb
CHANGED
@@ -1,73 +1,75 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module PartialKs
|
2
|
+
class Runner
|
3
|
+
attr_reader :table_graphs
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
5
|
+
def initialize(table_graphs)
|
6
|
+
@table_graphs = table_graphs
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
def run!
|
10
|
+
each_generation do |generation|
|
11
|
+
tables_to_filter = {}
|
12
|
+
table_names = []
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
generation.each do |table|
|
15
|
+
table_names << table.table_name
|
16
|
+
filter_config = table.kitchen_sync_filter
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
if !filter_config.nil?
|
19
|
+
tables_to_filter[table.table_name] = filter_config
|
20
|
+
end
|
19
21
|
end
|
20
|
-
end
|
21
22
|
|
22
|
-
|
23
|
-
|
23
|
+
# TODO output only tables_to_filter, depending on how KS handles filters
|
24
|
+
yield tables_to_filter, table_names
|
25
|
+
end
|
24
26
|
end
|
25
|
-
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
def report
|
29
|
+
result = []
|
30
|
+
each_generation.with_index do |generation, depth|
|
31
|
+
generation.each do |table|
|
32
|
+
result << [table.table_name, table.parent_model, table.custom_filter_relation, depth]
|
33
|
+
end
|
32
34
|
end
|
35
|
+
result
|
33
36
|
end
|
34
|
-
result
|
35
|
-
end
|
36
37
|
|
37
|
-
|
38
|
-
|
38
|
+
def each_generation
|
39
|
+
return enum_for(:each_generation) unless block_given?
|
39
40
|
|
40
|
-
|
41
|
-
|
41
|
+
generations.each do |generation|
|
42
|
+
yield generation
|
43
|
+
end
|
42
44
|
end
|
43
|
-
end
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
protected
|
47
|
+
def generations
|
48
|
+
return @generations if defined?(@generations)
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
50
|
+
@generations = []
|
51
|
+
table_graphs.each do |filtered_tables|
|
52
|
+
q = []
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
54
|
+
filtered_tables.each do |filtered_table|
|
55
|
+
q << filtered_table if filtered_table.parent_model.nil?
|
56
|
+
end
|
56
57
|
|
57
|
-
|
58
|
-
|
58
|
+
until q.empty?
|
59
|
+
@generations << q
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
61
|
+
next_generation = []
|
62
|
+
q.each do |table|
|
63
|
+
filtered_tables.each do |child_table|
|
64
|
+
next_generation << child_table if child_table.parent_model && child_table.parent_model.table_name == table.table_name
|
65
|
+
end
|
64
66
|
end
|
65
|
-
end
|
66
67
|
|
67
|
-
|
68
|
+
q = next_generation
|
69
|
+
end
|
68
70
|
end
|
69
|
-
end
|
70
71
|
|
71
|
-
|
72
|
+
@generations
|
73
|
+
end
|
72
74
|
end
|
73
75
|
end
|
data/lib/partial_ks/table.rb
CHANGED
@@ -27,6 +27,20 @@ module PartialKs
|
|
27
27
|
belongs_to_reflections.map(&:table_name)
|
28
28
|
end
|
29
29
|
|
30
|
+
def relation_for_associated_model(klass)
|
31
|
+
association = model.reflect_on_all_associations.find {|assoc| assoc.class_name == klass.name}
|
32
|
+
raise "#{filter_condition.name} not found in #{model.name} associations" if association.nil?
|
33
|
+
|
34
|
+
case association.macro
|
35
|
+
when :belongs_to
|
36
|
+
model.where(association.foreign_key => [0, *klass.pluck(:id)])
|
37
|
+
when :has_many
|
38
|
+
model.where(model.primary_key => [0, *klass.pluck(association.foreign_key)])
|
39
|
+
else
|
40
|
+
raise "Unknown macro"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
30
44
|
private
|
31
45
|
def belongs_to_reflections
|
32
46
|
@belongs_to_reflections ||= model.reflect_on_all_associations(:belongs_to)
|
data/lib/partial_ks/version.rb
CHANGED
data/lib/partial_ks.rb
CHANGED
data/test/filtered_table_test.rb
CHANGED
@@ -2,17 +2,36 @@ require 'test_helper'
|
|
2
2
|
|
3
3
|
describe "kitchen sync filter" do
|
4
4
|
let(:table) { PartialKs::Table.new(PostTag) }
|
5
|
+
let(:parent) { Minitest::Mock.new }
|
6
|
+
|
7
|
+
it "proxies to Table if there's parent only" do
|
8
|
+
table_parent_relation_method = :relation_for_associated_model
|
9
|
+
relation_mock = Minitest::Mock.new
|
10
|
+
relation_mock.expect :where_sql, "WHERE tag_id IN (0)"
|
5
11
|
|
6
|
-
it "uses parent as the filter" do
|
7
|
-
parent = Tag
|
8
12
|
filtered_table = PartialKs::FilteredTable.new(table, parent)
|
9
|
-
|
13
|
+
table.stub table_parent_relation_method, relation_mock do
|
14
|
+
filtered_table.kitchen_sync_filter.must_equal({"only" => 'tag_id IN (0)'})
|
15
|
+
end
|
10
16
|
end
|
11
17
|
|
12
18
|
it "uses the custom filter if provided" do
|
13
19
|
filter = PostTag.where(:id => [1, 2])
|
14
20
|
filtered_table = PartialKs::FilteredTable.new(table, nil, custom_filter_relation: filter)
|
15
|
-
filtered_table.kitchen_sync_filter.must_equal({"only" => '
|
21
|
+
filtered_table.kitchen_sync_filter.must_equal({"only" => '"post_tags"."id" IN (1, 2)'})
|
22
|
+
end
|
23
|
+
|
24
|
+
it "uses a SQL where fragment as a filter if provided" do
|
25
|
+
string_filter = "1=0"
|
26
|
+
filtered_table = PartialKs::FilteredTable.new(table, nil, custom_filter_relation: string_filter)
|
27
|
+
filtered_table.kitchen_sync_filter.must_equal({"only" => string_filter})
|
28
|
+
end
|
29
|
+
|
30
|
+
it "uses a SQL statement as a filter if provided" do
|
31
|
+
string_filter = "1=0"
|
32
|
+
sql_statement = "select * from #{table.table_name} where #{string_filter}"
|
33
|
+
filtered_table = PartialKs::FilteredTable.new(table, nil, custom_filter_relation: sql_statement)
|
34
|
+
filtered_table.kitchen_sync_filter.must_equal({"only" => string_filter})
|
16
35
|
end
|
17
36
|
|
18
37
|
it "returns nil if parent is nil" do
|
@@ -20,12 +39,4 @@ describe "kitchen sync filter" do
|
|
20
39
|
filtered_table.kitchen_sync_filter.must_be_nil
|
21
40
|
end
|
22
41
|
|
23
|
-
describe "table with different :foreign_key" do
|
24
|
-
let(:table) { PartialKs::Table.new(OldTag) }
|
25
|
-
|
26
|
-
it "uses the foreign key that's present in the table" do
|
27
|
-
filtered_table = PartialKs::FilteredTable.new(table, OldEntry)
|
28
|
-
filtered_table.kitchen_sync_filter.must_equal({"only" => 'blog_post_id IN (0)'})
|
29
|
-
end
|
30
|
-
end
|
31
42
|
end
|
data/test/setup_models.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
class User < ActiveRecord::Base
|
2
|
+
has_many :blog_posts
|
2
3
|
end
|
3
4
|
|
4
5
|
class BlogPost < ActiveRecord::Base
|
5
6
|
belongs_to :user
|
7
|
+
has_many :post_tags
|
6
8
|
end
|
7
9
|
|
8
10
|
class Tag < ActiveRecord::Base
|
11
|
+
has_many :post_tags
|
9
12
|
end
|
10
13
|
|
11
14
|
class PostTag < ActiveRecord::Base
|
data/test/table_test.rb
CHANGED
@@ -15,4 +15,39 @@ describe PartialKs::Table do
|
|
15
15
|
table.candidate_parent_classes.must_equal []
|
16
16
|
end
|
17
17
|
end
|
18
|
+
|
19
|
+
describe "relation for association" do
|
20
|
+
let(:table) { PartialKs::Table.new(BlogPost) }
|
21
|
+
|
22
|
+
it "emits the correct foreign key for the table for belongs_to" do
|
23
|
+
table.relation_for_associated_model(User).must_equal BlogPost.where(:user_id => 0)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "use results from pluck for belongs_to" do
|
27
|
+
user_ids = [1,2,3]
|
28
|
+
User.stub :pluck, user_ids do
|
29
|
+
table.relation_for_associated_model(User).must_equal BlogPost.where(:user_id => [0] + user_ids)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "emits the id condition for has_many" do
|
34
|
+
table.relation_for_associated_model(PostTag).must_equal BlogPost.where(:id => 0)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "uses results from pluck for has_many" do
|
38
|
+
id = 111
|
39
|
+
|
40
|
+
PostTag.stub :pluck, [id] do
|
41
|
+
table.relation_for_associated_model(PostTag).must_equal BlogPost.where(:id => [0] + [id])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "table with non-conventional :foreign_key" do
|
46
|
+
let(:table) { PartialKs::Table.new(OldTag) }
|
47
|
+
|
48
|
+
it "uses the foreign key that's present in the table" do
|
49
|
+
table.relation_for_associated_model(OldEntry).must_equal OldTag.where(:blog_post_id => 0)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
18
53
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: partial_ks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thong Kuah
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-02-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|