partial_ks 0.1.1 → 0.2.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
  SHA1:
3
- metadata.gz: 0c44b4f31d3904ec2fa73af0a8b4c1ef4194f416
4
- data.tar.gz: d4e1e4117d37b38c02ab1ec3f8704074564e5606
3
+ metadata.gz: 073f6737128fd06c8791fb210f72d3a657a459de
4
+ data.tar.gz: 4ea3290c59333b803b506ad70d7f81480513737d
5
5
  SHA512:
6
- metadata.gz: 0c0ab0cfe27acd0a8ea5c6fcc8d5eb28e48c3b0cccc66c63d990c0c5528d697ef8c2ad29a0865c86ac6c7e6adb1a23642c545c570188b6cf5e449cd62b873f67
7
- data.tar.gz: 13bb499ff85fbe75a4d60a83790bca00433d1d91441e29915152063340b9bf92fb8aafed499f8ab0e671935e64ff57a91e158acd4cb93215a31d0551eb19c21a
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
- - ConfigurationGenerator
3
- - Runner
4
- - runs
5
- - reports (mostly for debugging)
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
- TODO :
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
- class PartialKs::FilteredTable
2
- attr_reader :table, :parent_model, :custom_filter_relation
3
- delegate :table_name, :to => :table
1
+ module PartialKs
2
+ class FilteredTable
3
+ attr_reader :table, :parent_model, :custom_filter_relation
4
+ delegate :table_name, :to => :table
4
5
 
5
- def initialize(table, parent_model, custom_filter_relation: nil)
6
- @table = table
7
- @parent_model = parent_model
8
- @custom_filter_relation = custom_filter_relation
9
- end
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
- if !filter_condition.nil?
15
- if filter_condition.is_a?(ActiveRecord::Relation) || filter_condition.respond_to?(:where_sql)
16
- only_filter = filter_condition.where_sql.to_s.sub("WHERE", "")
17
- elsif filter_condition.is_a?(String)
18
- only_filter = filter_condition
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
- # this only supports parents where it's a belongs_to
21
- # TODO we can make it work with has_many
22
- # e.g. SomeModel.reflect_on_association(:elses)
23
- association = table.model.reflect_on_all_associations(:belongs_to).find {|assoc| assoc.class_name == filter_condition.name}
24
- raise "#{filter_condition.name} not found in #{table.model.name} associations" if association.nil?
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
- only_filter = "#{association.foreign_key} IN (#{[0, *filter_condition.pluck(:id)].join(',')})"
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
- {"only" => only_filter}
35
+ def where_regexp
36
+ /\A.*WHERE\s*/i
30
37
  end
31
38
  end
32
39
  end
@@ -1,19 +1,21 @@
1
- class PartialKs::ParentInferrer
2
- CannotInfer = Class.new(StandardError)
1
+ module PartialKs
2
+ class ParentInferrer
3
+ CannotInfer = Class.new(StandardError)
3
4
 
4
- attr_reader :table
5
+ attr_reader :table
5
6
 
6
- def initialize(table)
7
- @table = table
8
- end
7
+ def initialize(table)
8
+ @table = table
9
+ end
9
10
 
10
- def inferred_parent_class
11
- if table.top_level_table?
12
- nil
13
- elsif table.candidate_parent_classes.size == 1
14
- table.candidate_parent_classes.first
15
- else
16
- raise CannotInfer, "table has multiple candidates for parents"
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
@@ -1,73 +1,75 @@
1
- class PartialKs::Runner
2
- attr_reader :table_graphs
1
+ module PartialKs
2
+ class Runner
3
+ attr_reader :table_graphs
3
4
 
4
- def initialize(table_graphs)
5
- @table_graphs = table_graphs
6
- end
5
+ def initialize(table_graphs)
6
+ @table_graphs = table_graphs
7
+ end
7
8
 
8
- def run!
9
- each_generation do |generation|
10
- tables_to_filter = {}
11
- table_names = []
9
+ def run!
10
+ each_generation do |generation|
11
+ tables_to_filter = {}
12
+ table_names = []
12
13
 
13
- generation.each do |table|
14
- table_names << table.table_name
15
- filter_config = table.kitchen_sync_filter
14
+ generation.each do |table|
15
+ table_names << table.table_name
16
+ filter_config = table.kitchen_sync_filter
16
17
 
17
- if !filter_config.nil?
18
- tables_to_filter[table.table_name] = filter_config
18
+ if !filter_config.nil?
19
+ tables_to_filter[table.table_name] = filter_config
20
+ end
19
21
  end
20
- end
21
22
 
22
- # TODO output only tables_to_filter, depending on how KS handles filters
23
- yield tables_to_filter, table_names
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
- def report
28
- result = []
29
- each_generation.with_index do |generation, depth|
30
- generation.each do |table|
31
- result << [table.table_name, table.parent_model, table.custom_filter_relation, depth]
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
- def each_generation
38
- return enum_for(:each_generation) unless block_given?
38
+ def each_generation
39
+ return enum_for(:each_generation) unless block_given?
39
40
 
40
- generations.each do |generation|
41
- yield generation
41
+ generations.each do |generation|
42
+ yield generation
43
+ end
42
44
  end
43
- end
44
45
 
45
- protected
46
- def generations
47
- return @generations if defined?(@generations)
46
+ protected
47
+ def generations
48
+ return @generations if defined?(@generations)
48
49
 
49
- @generations = []
50
- table_graphs.each do |table_graph|
51
- q = []
50
+ @generations = []
51
+ table_graphs.each do |filtered_tables|
52
+ q = []
52
53
 
53
- table_graph.each do |table|
54
- q << table if table.parent_model.nil?
55
- end
54
+ filtered_tables.each do |filtered_table|
55
+ q << filtered_table if filtered_table.parent_model.nil?
56
+ end
56
57
 
57
- until q.empty?
58
- @generations << q
58
+ until q.empty?
59
+ @generations << q
59
60
 
60
- next_generation = []
61
- q.each do |table|
62
- table_graph.each do |child_table|
63
- next_generation << child_table if child_table.parent_model && child_table.parent_model.table_name == table.table_name
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
- q = next_generation
68
+ q = next_generation
69
+ end
68
70
  end
69
- end
70
71
 
71
- @generations
72
+ @generations
73
+ end
72
74
  end
73
75
  end
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module PartialKs
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/partial_ks.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  require 'active_record'
2
2
 
3
- module PartialKs
4
- end
5
-
6
3
  require_relative 'partial_ks/all_rails_models'
7
4
  require_relative 'partial_ks/filtered_table'
8
5
  require_relative 'partial_ks/parent_inferrer'
@@ -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
- filtered_table.kitchen_sync_filter.must_equal({"only" => 'tag_id IN (0)'})
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" => ' "post_tags"."id" IN (1, 2)'})
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.1.1
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: 2016-12-12 00:00:00.000000000 Z
11
+ date: 2017-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord