live_fixtures 1.0.1 → 2.0.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: c62185d78a809ced340e94cea4d55cf2149ffe83f3d01629db553df1b37dd10e
4
- data.tar.gz: cea4216a648c7483612dc608ae00d72c0a78bce29b98e53a78534f47506d5f06
3
+ metadata.gz: 4555eba9da64e35ef61d763132a1d2e5a280c785d0b2583fcb9055e6800eae74
4
+ data.tar.gz: fb54c037ff212fecb9bdbe2fd0c961df6d0acba9a0ed6dab7579c5e487656b0f
5
5
  SHA512:
6
- metadata.gz: 679e1f0bf3bc5d4d423258ab29b049f81d468889fb17f58de07a3d3bc0a3755466c09b88a853c9dd86b90e475f933c9c102f186fd8405cda0cebec598a6ebae0
7
- data.tar.gz: 5417590a883f45a64ce359a79553a9cf1e826696e7b65f5c9ff0d15d3d28b3a05807578e8dad6672a91daaf3b2f53e38bd28a8dc842f28237c8ee044f01cf970
6
+ metadata.gz: 6f0af7f78d0638a22f2dc607b6a21b0cf24b95497fe974c4f0c1b1d7c4b895048deac44ec55576391ccf67bc297e8aa318e841ed1ef1669eb286b8cdeba3f932
7
+ data.tar.gz: 12a8488d4e93c45462e7362926f057571f54f4afe2640ff94dc422b8c3bbc9a4ff0a6b4a064962e15ca13b75c2abb60e1f016495189a28e3d14b627504152c32
@@ -2,6 +2,13 @@
2
2
  All notable changes to this project will be documented in this file.
3
3
  This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
+ ## [2.0.0] - 2020-11-11
6
+ ### Breaking changes
7
+ This is a breaking change because LiveFixtures::Import.new now needs to receive the class_names Hash to be able to correctly compute the insert_order in case there are some unconventional class names associations. And the class_names argument is removed from import_all. But this is the only change.
8
+
9
+ ### Added
10
+ - compute insert_order automatically (#33)
11
+
5
12
  ## [1.0.1] - 2019-04-10
6
13
  ### Fixed
7
14
  - fixed incompatibility with mysql
data/README.md CHANGED
@@ -76,7 +76,7 @@ The `LiveFixtures::Export` module is meant to be included into your export class
76
76
 
77
77
  ### Importing
78
78
 
79
- The `LiveFixtures::Import` class allows you to specify the location of your fixtures and the order in which to import them. Once you've done that, you can import them directly to your database.
79
+ The `LiveFixtures::Import` class allows you to specify the location of your fixtures and, optionally, the order in which to import them. If you don't specify an order, the order will be computed from the ActiveRecord models associations. Once you've done that, you can import them directly to your database.
80
80
 
81
81
 
82
82
  module Seed::User
@@ -1,9 +1,11 @@
1
1
  require "live_fixtures/version"
2
2
  require "live_fixtures/import"
3
3
  require "live_fixtures/import/fixtures"
4
+ require "live_fixtures/import/insertion_order_computer"
4
5
  require "live_fixtures/export"
5
6
  require "live_fixtures/export/fixture"
6
7
  require "ruby-progressbar"
8
+ require "yaml"
7
9
 
8
10
  module LiveFixtures
9
11
  module_function
@@ -2,35 +2,48 @@
2
2
  class LiveFixtures::Import
3
3
  NO_LABEL = nil
4
4
 
5
+ # Returns the insert order that was specified in the constructor or
6
+ # the inferred one if none was specified.
7
+ attr_reader :insert_order
8
+
5
9
  # Instantiate a new Import with the directory containing your fixtures, and
6
10
  # the order in which to import them. The order should ensure fixtures
7
11
  # containing references to another fixture are imported AFTER the referenced
8
12
  # fixture.
9
13
  # @raise [ArgumentError] raises an argument error if not every element in the insert_order has a corresponding yml file.
10
14
  # @param root_path [String] path to the directory containing the yml files to import.
11
- # @param insert_order [Array<String>] a list of yml files (without .yml extension) in the order they should be imported.
15
+ # @param insert_order [Array<String> | Nil] a list of yml files (without .yml extension) in the order they should be imported, or `nil` if these order is to be inferred by this class.
16
+ # @param class_names [Hash{Symbol => String}] a mapping table name => Model class, for any that don't follow convention.
12
17
  # @param [Hash] opts export configuration options
13
18
  # @option opts [Boolean] show_progress whether or not to show the progress bar
14
19
  # @option opts [Boolean] skip_missing_tables when false, an error will be raised if a yaml file isn't found for each table in insert_order
15
20
  # @option opts [Boolean] skip_missing_refs when false, an error will be raised if an ID isn't found for a label.
16
21
  # @return [LiveFixtures::Import] an importer
17
22
  # @see LiveFixtures::Export::Reference
18
- def initialize(root_path, insert_order, **opts)
23
+ def initialize(root_path, insert_order = nil, class_names = {}, **opts)
19
24
  defaut_options = { show_progress: true, skip_missing_tables: false, skip_missing_refs: false }
20
25
  @options = defaut_options.merge(opts)
21
26
  @root_path = root_path
22
27
  @table_names = Dir.glob(File.join(@root_path, '{*,**}/*.yml')).map do |filepath|
23
28
  File.basename filepath, ".yml"
24
29
  end
25
- @table_names = insert_order.select {|table_name| @table_names.include? table_name}
26
- if @table_names.size < insert_order.size && !@options[:skip_missing_tables]
27
- raise ArgumentError, "table(s) mentioned in `insert_order` which has no yml file to import: #{insert_order - @table_names}"
30
+
31
+ @class_names = class_names
32
+ @table_names.each { |n|
33
+ @class_names[n.tr('/', '_').to_sym] ||= n.classify if n.include?('/')
34
+ }
35
+
36
+ @insert_order = insert_order
37
+ @insert_order ||= InsertionOrderComputer.compute(@table_names, @class_names, compute_polymorphic_associations)
38
+
39
+ @table_names = @insert_order.select {|table_name| @table_names.include? table_name}
40
+ if @table_names.size < @insert_order.size && !@options[:skip_missing_tables]
41
+ raise ArgumentError, "table(s) mentioned in `insert_order` which has no yml file to import: #{@insert_order - @table_names}"
28
42
  end
29
43
  @label_to_id = {}
30
44
  end
31
45
 
32
46
  # Within a transaction, import all the fixtures into the database.
33
- # @param class_names [Hash{Symbol => String}] a mapping table name => Model class, for any that don't follow convention.
34
47
  #
35
48
  # The very similar method: ActiveRecord::FixtureSet.create_fixtures has the
36
49
  # unfortunate side effect of truncating each table!!
@@ -39,11 +52,7 @@ class LiveFixtures::Import
39
52
  # with calling {LiveFixtures::Import::Fixtures#each_table_row_with_label} instead of
40
53
  # `AR::Fixtures#table_rows`, and using those labels to populate `@label_to_id`.
41
54
  # @see https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/fixtures.rb#L496
42
- def import_all(class_names = {})
43
- @table_names.each { |n|
44
- class_names[n.tr('/', '_').to_sym] ||= n.classify if n.include?('/')
45
- }
46
-
55
+ def import_all
47
56
  connection = ActiveRecord::Base.connection
48
57
 
49
58
  files_to_read = @table_names
@@ -52,7 +61,7 @@ class LiveFixtures::Import
52
61
  connection.transaction(requires_new: true) do
53
62
  files_to_read.each do |path|
54
63
  table_name = path.tr '/', '_'
55
- class_name = class_names[table_name.to_sym] || table_name.classify
64
+ class_name = @class_names[table_name.to_sym] || table_name.classify
56
65
 
57
66
  ff = Fixtures.new(connection,
58
67
  table_name,
@@ -72,6 +81,52 @@ class LiveFixtures::Import
72
81
  end
73
82
  end
74
83
 
84
+ private
85
+
86
+ # Here we go through each of the fixture YAML files to see what polymorphic
87
+ # dependencies exist for each of the models.
88
+ # We do this by inspecting the value of any field that ends with `_type`,
89
+ # for example `author_type`, `assignment_type`, etc.
90
+ # Becuase we can't know all the possible types of a polymorphic association
91
+ # we compute them from the YAML file contents.
92
+ # Returns a Hash[Class => Set[Class]]
93
+ def compute_polymorphic_associations
94
+ polymorphic_associations = Hash.new { |h, k| h[k] = Set.new }
95
+
96
+ connection = ActiveRecord::Base.connection
97
+ files_to_read = @table_names
98
+
99
+ files_to_read.each do |path|
100
+ table_name = path.tr '/', '_'
101
+ class_name = @class_names[table_name.to_sym] || table_name.classify
102
+
103
+ # Here we use the yaml file and YAML.load instead of ActiveRecord::FixtureSet.new
104
+ # because it's faster and we can also check whether we actually need to
105
+ # load the file: only if it includes "_type" in it, otherwise there will be
106
+ # no polymorphic types in there.
107
+
108
+ filename = ::File.join(@root_path, "#{path}.yml")
109
+ file = File.read(filename)
110
+ next unless file =~ /_type/
111
+
112
+ yaml = YAML.load(file)
113
+ yaml.each do |key, object|
114
+ object.each do |field, value|
115
+ next unless field.ends_with?("_type")
116
+
117
+ begin
118
+ polymorphic_associations[class_name.constantize] << value.constantize
119
+ rescue NameError
120
+ # It might be the case that the `..._type` field doesn't actually
121
+ # refer to a type name, so we just ignore it.
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ polymorphic_associations
128
+ end
129
+
75
130
  class ProgressBarIterator
76
131
  def initialize(ff)
77
132
  @ff = ff
@@ -0,0 +1,127 @@
1
+ class LiveFixtures::Import
2
+ # :nodoc:
3
+ class InsertionOrderComputer
4
+ # :nodoc:
5
+ class Node
6
+ attr_reader :path
7
+ attr_reader :class_name
8
+ attr_reader :klass
9
+
10
+ # The classes this node depends on
11
+ attr_reader :dependencies
12
+
13
+ def initialize(path, class_name, klass)
14
+ @path = path
15
+ @class_name = class_name
16
+ @klass = klass
17
+ @dependencies = Set.new
18
+ end
19
+ end
20
+
21
+ def self.compute(table_names, class_names = {}, polymorphic_associations = {})
22
+ new(table_names, class_names, polymorphic_associations).compute
23
+ end
24
+
25
+ def initialize(table_names, class_names = {}, polymorphic_associations = {})
26
+ @table_names = table_names
27
+ @class_names = class_names
28
+ @polymorphic_associations = polymorphic_associations
29
+ end
30
+
31
+ def compute
32
+ nodes = build_nodes
33
+ compute_insert_order(nodes)
34
+ end
35
+
36
+ private
37
+
38
+ # Builds an Array of Nodes, each containing dependencies to other nodes
39
+ # using their class names.
40
+ def build_nodes
41
+ # Create a Hash[Class => Node] for each table/class
42
+ nodes = {}
43
+ @table_names.each do |path|
44
+ table_name = path.tr "/", "_"
45
+ class_name = @class_names[table_name.to_sym] || table_name.classify
46
+ klass = class_name.constantize
47
+ nodes[klass] = Node.new(path, class_name, klass)
48
+ end
49
+
50
+ # First iniitalize dependencies from polymorphic associations that we
51
+ # explicitly found in the yaml files.
52
+ @polymorphic_associations.each do |klass, associations|
53
+ associations.each do |association|
54
+ node = nodes[klass]
55
+ next unless node
56
+ next unless nodes.key?(association)
57
+
58
+ node.dependencies << association
59
+ end
60
+ end
61
+
62
+ # Compute dependencies between nodes/classes by reflecting on their
63
+ # ActiveRecord associations.
64
+ nodes.each do |_, node|
65
+ klass = node.klass
66
+ klass.reflect_on_all_associations.each do |assoc|
67
+ # We can't handle polymorphic associations, but the concrete types
68
+ # should have been deduced from the yaml files contents
69
+ next if assoc.polymorphic?
70
+
71
+ # Don't add a dependency if the class is not in the given table names
72
+ next unless nodes.key?(assoc.klass)
73
+
74
+ # A class might depend on itself, but we don't add it as a dependency
75
+ # because otherwise we'll never make it (the class can probably be created
76
+ # just fine and these dependencies are optional/nilable)
77
+ next if klass == assoc.klass
78
+
79
+ case assoc.macro
80
+ when :belongs_to
81
+ node.dependencies << assoc.klass
82
+ when :has_one, :has_many
83
+ # Skip `through` association becuase it will be already computed
84
+ # for the related `has_one`/`has_many` association
85
+ next if assoc.options[:through]
86
+
87
+ nodes[assoc.klass].dependencies << klass
88
+ end
89
+ end
90
+ end
91
+
92
+ # Finally sort all values by name for consistent results
93
+ nodes.values.sort_by { |node| node.klass.name }
94
+ end
95
+
96
+ def compute_insert_order(nodes)
97
+ insert_order = []
98
+
99
+ until nodes.empty?
100
+ # Pick a node that has no dependencies
101
+ free_node = nodes.find { |node| node.dependencies.empty? }
102
+
103
+ if free_node.nil?
104
+ msg = "Can't compute an insert order.\n\n"
105
+ msg << "These models seem to depend on each other:\n"
106
+ nodes.each do |node|
107
+ msg << " #{node.klass.name}\n"
108
+ msg << " - depends on: #{node.dependencies.map(&:name).join(", ")}\n"
109
+ end
110
+ raise msg
111
+ end
112
+
113
+ insert_order << free_node.path
114
+
115
+ # Delete this node from the other nodes' dependencies
116
+ nodes.each do |node|
117
+ node.dependencies.delete(free_node.klass)
118
+ end
119
+
120
+ # And delete this node because we are done with it
121
+ nodes.delete(free_node)
122
+ end
123
+
124
+ insert_order
125
+ end
126
+ end
127
+ end
@@ -1,3 +1,3 @@
1
1
  module LiveFixtures
2
- VERSION = "1.0.1"
2
+ VERSION = "2.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: live_fixtures
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - jleven
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-04-10 00:00:00.000000000 Z
11
+ date: 2020-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -178,7 +178,7 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
- description:
181
+ description:
182
182
  email:
183
183
  - josh@noredink.com
184
184
  executables: []
@@ -193,12 +193,13 @@ files:
193
193
  - lib/live_fixtures/export/fixture.rb
194
194
  - lib/live_fixtures/import.rb
195
195
  - lib/live_fixtures/import/fixtures.rb
196
+ - lib/live_fixtures/import/insertion_order_computer.rb
196
197
  - lib/live_fixtures/version.rb
197
- homepage:
198
+ homepage:
198
199
  licenses:
199
200
  - MIT
200
201
  metadata: {}
201
- post_install_message:
202
+ post_install_message:
202
203
  rdoc_options: []
203
204
  require_paths:
204
205
  - lib
@@ -214,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
214
215
  version: '0'
215
216
  requirements: []
216
217
  rubygems_version: 3.0.3
217
- signing_key:
218
+ signing_key:
218
219
  specification_version: 4
219
220
  summary: Tools for exporting and importing between databases managed by ActiveRecord.
220
221
  test_files: []