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 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