evil-seed 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +25 -7
- data/CHANGELOG.md +68 -0
- data/Gemfile +7 -2
- data/README.md +19 -3
- data/evil-seed.gemspec +1 -1
- data/lib/evil_seed/configuration/root.rb +97 -13
- data/lib/evil_seed/configuration.rb +1 -1
- data/lib/evil_seed/dumper.rb +2 -1
- data/lib/evil_seed/record_dumper.rb +12 -7
- data/lib/evil_seed/relation_dumper.rb +59 -27
- data/lib/evil_seed/root_dumper.rb +3 -2
- data/lib/evil_seed/version.rb +1 -1
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 757a2cee2b774b29a9d248270642db41faa431fdfd1b738aed0b8ea275fa358a
|
4
|
+
data.tar.gz: 8dc1887a03f1169aa332f9ef345ea666d5582e578ec6c8c97c6041b6f281dff4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aea3be4c8d73fc5ec6e04457d6322306d12a70d44a4792eb1dc855162b28b3b325617815a8a5898733cdc31067e8c51fa3714909c123fe890d54e21123121f7f
|
7
|
+
data.tar.gz: 98ed722479590c6615d87dfa3a0262d7012f008f0c9be2c4ddb3066a01ef2959febaf3ecea60c25e362417e9eba986f7ada50fad9ce06355fb8b5dfbc75e664c
|
data/.github/workflows/test.yml
CHANGED
@@ -19,22 +19,40 @@ jobs:
|
|
19
19
|
- ruby: "head"
|
20
20
|
activerecord: "head"
|
21
21
|
database: sqlite
|
22
|
+
- ruby: "3.4"
|
23
|
+
activerecord: "8.0"
|
24
|
+
database: postgresql
|
25
|
+
- ruby: "3.4"
|
26
|
+
activerecord: "8.0"
|
27
|
+
database: mysql
|
28
|
+
- ruby: "3.4"
|
29
|
+
activerecord: "8.0"
|
30
|
+
database: sqlite
|
22
31
|
- ruby: "3.3"
|
23
|
-
activerecord: "7.
|
32
|
+
activerecord: "7.2"
|
24
33
|
database: postgresql
|
25
34
|
- ruby: "3.3"
|
26
|
-
activerecord: "7.
|
35
|
+
activerecord: "7.2"
|
27
36
|
database: mysql
|
28
37
|
- ruby: "3.3"
|
29
|
-
activerecord: "7.
|
38
|
+
activerecord: "7.2"
|
30
39
|
database: sqlite
|
31
40
|
- ruby: "3.2"
|
32
|
-
activerecord: "7.
|
41
|
+
activerecord: "7.1"
|
42
|
+
database: postgresql
|
43
|
+
- ruby: "3.2"
|
44
|
+
activerecord: "7.1"
|
45
|
+
database: mysql
|
46
|
+
- ruby: "3.2"
|
47
|
+
activerecord: "7.1"
|
33
48
|
database: sqlite
|
34
49
|
- ruby: "3.1"
|
35
|
-
activerecord: "
|
50
|
+
activerecord: "7.0"
|
36
51
|
database: sqlite
|
37
52
|
- ruby: "3.0"
|
53
|
+
activerecord: "6.1"
|
54
|
+
database: sqlite
|
55
|
+
- ruby: "2.7"
|
38
56
|
activerecord: "6.0"
|
39
57
|
database: sqlite
|
40
58
|
|
@@ -42,7 +60,7 @@ jobs:
|
|
42
60
|
|
43
61
|
services:
|
44
62
|
postgres:
|
45
|
-
image: postgres:
|
63
|
+
image: ${{ (matrix.database == 'postgresql') && 'postgres:17' || '' }}
|
46
64
|
env:
|
47
65
|
POSTGRES_USER: postgres
|
48
66
|
POSTGRES_PASSWORD: postgres
|
@@ -54,7 +72,7 @@ jobs:
|
|
54
72
|
--health-timeout 5s
|
55
73
|
--health-retries 5
|
56
74
|
mysql:
|
57
|
-
image: mysql:
|
75
|
+
image: ${{ (matrix.database == 'mysql') && 'mysql:9' || '' }}
|
58
76
|
env:
|
59
77
|
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
60
78
|
MYSQL_DATABASE: evil_seed_test
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.7.0] - 2025-02-07
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Options to exclude all `has_many` and `has_one` or optional `belongs_to` associations by default. [@Envek]
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
root.exclude_has_relations
|
18
|
+
root.exclude_optional_belongs_to
|
19
|
+
```
|
20
|
+
|
21
|
+
Excluded associations can be re-included by `include` with matching pattern.
|
22
|
+
|
23
|
+
- Exclusion and inclusion patterns can be specified as hashes and/or arrays. [@Envek]
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
config.root('Forum', featured: true) do |forum|
|
27
|
+
forum.include(parent: {questions: %i[answers votes]})
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
Which is equivalent to:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
config.root('Forum', featured: true) do |forum|
|
35
|
+
forum.include(/\Aforum(\.parent(\.questions(\.answers))?)?)?\z/)
|
36
|
+
forum.include(/\Aforum(\.parent(\.questions(\.votes))?)?)?\z/)
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
- Association limits also can be specified as hashes and/or arrays. [@Envek]
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
config.root('Forum', featured: true) do |forum|
|
44
|
+
forum.limit_associations_size(15, questions: %i[answers votes])
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
Which is equivalent to:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
config.root('Forum', featured: true) do |forum|
|
52
|
+
forum.limit_associations_size(15, 'forum.questions.answers')
|
53
|
+
forum.limit_associations_size(15, 'forum.questions.votes')
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
|
58
|
+
- Print reason of association exclusion or inclusion in verbose mode. [@Envek]
|
59
|
+
|
60
|
+
- Allow to apply custom scoping to included associations. [@Envek]
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
config.root('Forum', featured: true) do |forum|
|
64
|
+
forum.include('questions.answers') do
|
65
|
+
order(created_at: :desc)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
### Fixed
|
71
|
+
|
72
|
+
- Error when dumping single-column tables. [@lsantobuono]
|
73
|
+
- Bug with null foreign key to back to auxiliary `has_one` association with not matching names. E.g. user has many profiles and has one default profile, profile belongs to user.
|
74
|
+
- Ignored columns handling.
|
75
|
+
- Dump relations for records that were dumped earlier with relations excluded.
|
76
|
+
|
10
77
|
## [0.6.0] - 2024-06-18
|
11
78
|
|
12
79
|
### Added
|
@@ -72,3 +139,4 @@ Initial release. [@palkan], [@Envek]
|
|
72
139
|
[@cmer]: https://github.com/cmer "Carl Mercier"
|
73
140
|
[@nhocki]: https://github.com/nhocki "Nicolás Hock-Isaza"
|
74
141
|
[@gazay]: https://github.com/gazay "Alex Gaziev"
|
142
|
+
[@lsantobuono]: https://github.com/lsantobuono ""
|
data/Gemfile
CHANGED
@@ -5,7 +5,7 @@ source 'https://rubygems.org'
|
|
5
5
|
# Specify your gem's dependencies in evil-seed.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
-
activerecord_version = ENV.fetch("ACTIVERECORD_VERSION", "~>
|
8
|
+
activerecord_version = ENV.fetch("ACTIVERECORD_VERSION", "~> 8.0")
|
9
9
|
case activerecord_version.upcase
|
10
10
|
when "HEAD"
|
11
11
|
git "https://github.com/rails/rails.git" do
|
@@ -15,5 +15,10 @@ when "HEAD"
|
|
15
15
|
else
|
16
16
|
activerecord_version = "~> #{activerecord_version}.0" if activerecord_version.match?(/^\d+\.\d+$/)
|
17
17
|
gem "activerecord", activerecord_version
|
18
|
-
|
18
|
+
if Gem::Version.new("7.2") > Gem::Version.new(activerecord_version.scan(/\d+\.\d+/).first)
|
19
|
+
gem "sqlite3", "~> 1.4"
|
20
|
+
gem "concurrent-ruby", "< 1.3.5"
|
21
|
+
end
|
19
22
|
end
|
23
|
+
|
24
|
+
gem "debug"
|
data/README.md
CHANGED
@@ -48,21 +48,37 @@ EvilSeed.configure do |config|
|
|
48
48
|
# First, you should specify +root models+ and their +constraints+ to limit the number of dumped records:
|
49
49
|
# This is like Forum.where(featured: true).all
|
50
50
|
config.root('Forum', featured: true) do |root|
|
51
|
+
# You can limit number of records to be dumped
|
52
|
+
root.limit(100)
|
53
|
+
# Specify order for records to be selected for dump
|
54
|
+
root.order(created_at: :desc)
|
55
|
+
|
51
56
|
# It's possible to remove some associations from dumping with pattern of association path to exclude
|
52
57
|
#
|
53
58
|
# Association path is a dot-delimited string of association chain starting from model itself:
|
54
59
|
# example: "forum.users.questions"
|
55
60
|
root.exclude(/\btracking_pixels\b/, 'forum.popular_questions', /\Aforum\.parent\b/)
|
56
61
|
|
57
|
-
# Include back only certain
|
58
|
-
root.include(
|
62
|
+
# Include back only certain association chains
|
63
|
+
root.include(parent: {questions: %i[answers votes]})
|
64
|
+
# which is the same as
|
65
|
+
root.include(/\Aforum(\.parent(\.questions(\.(answers|votes))?)?)?\z/)
|
66
|
+
|
67
|
+
# You can also specify custom scoping for associations
|
68
|
+
root.include(questions: { answers: :reactions }) do
|
69
|
+
order(created_at: :desc) # Any ActiveRecord query method is allowed
|
70
|
+
end
|
59
71
|
|
60
72
|
# It's possible to limit the number of included into dump has_many and has_one records for every association
|
61
73
|
# Note that belongs_to records for all not excluded associations are always dumped to keep referential integrity.
|
62
74
|
root.limit_associations_size(100)
|
63
75
|
|
64
76
|
# Or for certain association only
|
65
|
-
root.limit_associations_size(
|
77
|
+
root.limit_associations_size(5, 'forum.questions')
|
78
|
+
root.limit_associations_size(15, 'forum.questions.answers')
|
79
|
+
# or
|
80
|
+
root.limit_associations_size(5, :questions)
|
81
|
+
root.limit_associations_size(15, questions: :answers)
|
66
82
|
|
67
83
|
# Limit the depth of associations to be dumped from the root level
|
68
84
|
# All traverses through has_many, belongs_to, etc are counted
|
data/evil-seed.gemspec
CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
|
30
30
|
spec.add_dependency 'activerecord', '>= 5.0'
|
31
31
|
|
32
|
-
spec.add_development_dependency 'rake', '~>
|
32
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
33
33
|
spec.add_development_dependency 'minitest', '~> 5.0'
|
34
34
|
spec.add_development_dependency 'pg', '>= 0.20'
|
35
35
|
spec.add_development_dependency 'mysql2'
|
@@ -4,7 +4,7 @@ module EvilSeed
|
|
4
4
|
class Configuration
|
5
5
|
# Configuration for dumping some root model and its associations
|
6
6
|
class Root
|
7
|
-
attr_reader :model, :constraints
|
7
|
+
attr_reader :model, :constraints, :limit, :order
|
8
8
|
attr_reader :total_limit, :association_limits, :deep_limit, :dont_nullify
|
9
9
|
attr_reader :exclusions, :inclusions
|
10
10
|
|
@@ -14,7 +14,7 @@ module EvilSeed
|
|
14
14
|
@model = model
|
15
15
|
@constraints = constraints
|
16
16
|
@exclusions = []
|
17
|
-
@inclusions =
|
17
|
+
@inclusions = {}
|
18
18
|
@association_limits = {}
|
19
19
|
@deep_limit = nil
|
20
20
|
@dont_nullify = dont_nullify
|
@@ -23,23 +23,69 @@ module EvilSeed
|
|
23
23
|
# Exclude some of associations from the dump
|
24
24
|
# @param association_patterns Array<String, Regex> Patterns to exclude associated models from dump
|
25
25
|
def exclude(*association_patterns)
|
26
|
-
|
26
|
+
association_patterns.each do |pattern|
|
27
|
+
case pattern
|
28
|
+
when String, Regexp
|
29
|
+
@exclusions << pattern
|
30
|
+
else
|
31
|
+
path_prefix = model.constantize.model_name.singular
|
32
|
+
@exclusions += compile_patterns(pattern, prefix: path_prefix).map { |p| Regexp.new(/\A#{p}\z/) }
|
33
|
+
end
|
34
|
+
end
|
27
35
|
end
|
28
36
|
|
29
37
|
# Include some excluded associations back to the dump
|
30
38
|
# @param association_patterns Array<String, Regex> Patterns to exclude associated models from dump
|
31
|
-
def include(*association_patterns)
|
32
|
-
|
39
|
+
def include(*association_patterns, &block)
|
40
|
+
association_patterns.each do |pattern|
|
41
|
+
case pattern
|
42
|
+
when String, Regexp
|
43
|
+
@inclusions[pattern] = block
|
44
|
+
else
|
45
|
+
path_prefix = model.constantize.model_name.singular
|
46
|
+
compile_patterns(pattern, prefix: path_prefix).map do |p|
|
47
|
+
@inclusions[Regexp.new(/\A#{p}\z/)] = block
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def exclude_has_relations
|
54
|
+
@excluded_has_relations = :exclude_has_relations
|
55
|
+
end
|
56
|
+
|
57
|
+
def exclude_optional_belongs_to
|
58
|
+
@excluded_optional_belongs_to = :exclude_optional_belongs_to
|
59
|
+
end
|
60
|
+
|
61
|
+
def limit(limit = nil)
|
62
|
+
return @limit if limit.nil?
|
63
|
+
|
64
|
+
@limit = limit
|
65
|
+
end
|
66
|
+
|
67
|
+
def order(order = nil)
|
68
|
+
return @order if order.nil?
|
69
|
+
|
70
|
+
@order = order
|
33
71
|
end
|
34
72
|
|
35
73
|
# Limit number of records in all (if pattern is not provided) or given associations to include into dump
|
36
74
|
# @param limit [Integer] Maximum number of records in associations to include into dump
|
37
|
-
# @param association_pattern
|
38
|
-
def limit_associations_size(limit,
|
39
|
-
if
|
40
|
-
|
41
|
-
|
42
|
-
|
75
|
+
# @param association_pattern Array<String, Regex, Hash> Pattern to limit number of records for certain associated models
|
76
|
+
def limit_associations_size(limit, *association_patterns)
|
77
|
+
return @total_limit = limit if association_patterns.empty?
|
78
|
+
|
79
|
+
association_patterns.each do |pattern|
|
80
|
+
case pattern
|
81
|
+
when String, Regexp
|
82
|
+
@association_limits[pattern] = limit
|
83
|
+
else
|
84
|
+
path_prefix = model.constantize.model_name.singular
|
85
|
+
compile_patterns(pattern, prefix: path_prefix, partial: false).map do |p|
|
86
|
+
@association_limits[Regexp.new(/\A#{p}\z/)] = limit
|
87
|
+
end
|
88
|
+
end
|
43
89
|
end
|
44
90
|
end
|
45
91
|
|
@@ -54,11 +100,49 @@ module EvilSeed
|
|
54
100
|
end
|
55
101
|
|
56
102
|
def excluded?(association_path)
|
57
|
-
exclusions.
|
103
|
+
exclusions.find { |exclusion| association_path.match(exclusion) } #.match(association_path) }
|
58
104
|
end
|
59
105
|
|
60
106
|
def included?(association_path)
|
61
|
-
inclusions.
|
107
|
+
inclusions.find { |inclusion, _block| association_path.match(inclusion) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def excluded_has_relations?
|
111
|
+
@excluded_has_relations
|
112
|
+
end
|
113
|
+
|
114
|
+
def excluded_optional_belongs_to?
|
115
|
+
@excluded_optional_belongs_to
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def compile_patterns(pattern, prefix: "", partial: true)
|
121
|
+
wrap = -> (p) { partial ? "(?:\\.#{p})?" : "\\.#{p}" }
|
122
|
+
case pattern
|
123
|
+
when String, Symbol
|
124
|
+
["#{prefix}#{wrap.(pattern.to_s)}"]
|
125
|
+
when Regexp
|
126
|
+
["#{prefix}#{wrap.("(?:#{pattern.source})")}"]
|
127
|
+
when Array
|
128
|
+
pattern.map { |p| compile_patterns(p, prefix: prefix, partial: partial) }.flatten
|
129
|
+
when Hash
|
130
|
+
pattern.map do |k, v|
|
131
|
+
next nil unless v
|
132
|
+
subpatterns = compile_patterns(v, partial: partial)
|
133
|
+
next "#{prefix}#{wrap.(k)}" if subpatterns.empty?
|
134
|
+
|
135
|
+
subpatterns.map do |p|
|
136
|
+
"#{prefix}#{wrap.("#{k}#{p}")}"
|
137
|
+
end
|
138
|
+
end.compact.flatten
|
139
|
+
when false, nil
|
140
|
+
nil
|
141
|
+
when true
|
142
|
+
[prefix]
|
143
|
+
else
|
144
|
+
raise ArgumentError, "Unknown pattern type: #{pattern.class} for #{pattern.inspect}"
|
145
|
+
end
|
62
146
|
end
|
63
147
|
end
|
64
148
|
end
|
data/lib/evil_seed/dumper.rb
CHANGED
@@ -7,7 +7,7 @@ module EvilSeed
|
|
7
7
|
# This class initiates dump creation for every root model of configuration
|
8
8
|
# and then concatenates dumps from all roots into one single IO.
|
9
9
|
class Dumper
|
10
|
-
attr_reader :configuration, :loaded_map
|
10
|
+
attr_reader :configuration, :loaded_map, :to_load_map
|
11
11
|
|
12
12
|
# @param configuration [Configuration]
|
13
13
|
def initialize(configuration)
|
@@ -18,6 +18,7 @@ module EvilSeed
|
|
18
18
|
# @param output [IO] Stream to write SQL dump into
|
19
19
|
def call(output)
|
20
20
|
@loaded_map = Hash.new { |h, k| h[k] = Set.new } # stores primary keys of already dumped records for every table
|
21
|
+
@to_load_map = Hash.new { |h, k| h[k] = Set.new } # stores primary keys of records we're going to dump to avoid cycles
|
21
22
|
@output = output
|
22
23
|
configuration.roots.each do |root|
|
23
24
|
table_outputs = RootDumper.new(root, self).call
|
@@ -9,7 +9,7 @@ module EvilSeed
|
|
9
9
|
|
10
10
|
attr_reader :model_class, :configuration, :relation_dumper
|
11
11
|
|
12
|
-
delegate :loaded_map, to: :relation_dumper
|
12
|
+
delegate :loaded_map, :to_load_map, to: :relation_dumper
|
13
13
|
|
14
14
|
def initialize(model_class, configuration, relation_dumper)
|
15
15
|
@model_class = model_class
|
@@ -38,8 +38,10 @@ module EvilSeed
|
|
38
38
|
|
39
39
|
def loaded!(attributes)
|
40
40
|
id = model_class.primary_key && attributes[model_class.primary_key] || attributes
|
41
|
-
|
42
|
-
|
41
|
+
!loaded_map[model_class.table_name].include?(id).tap do
|
42
|
+
loaded_map[model_class.table_name] << id
|
43
|
+
to_load_map[model_class.table_name].delete(id)
|
44
|
+
end
|
43
45
|
end
|
44
46
|
|
45
47
|
def transform_and_anonymize(attributes)
|
@@ -51,9 +53,10 @@ module EvilSeed
|
|
51
53
|
end
|
52
54
|
|
53
55
|
def insertable_column_names
|
54
|
-
|
55
|
-
|
56
|
-
|
56
|
+
@insertable_column_names ||=
|
57
|
+
model_class.columns_hash.reject do |_k, v|
|
58
|
+
v.respond_to?(:virtual?) ? v.virtual? : false
|
59
|
+
end.keys - configuration.ignored_columns_for(model_class.to_s)
|
57
60
|
end
|
58
61
|
|
59
62
|
def insert_statement
|
@@ -81,9 +84,11 @@ module EvilSeed
|
|
81
84
|
end
|
82
85
|
|
83
86
|
def finalize!
|
84
|
-
return
|
87
|
+
return true if @finalized
|
88
|
+
return false unless @header_written && @tuples_written > 0
|
85
89
|
@output.write(";\n\n")
|
86
90
|
@tuples_written = 0
|
91
|
+
@finalized = true
|
87
92
|
end
|
88
93
|
|
89
94
|
def prepare(attributes)
|
@@ -24,24 +24,27 @@ module EvilSeed
|
|
24
24
|
MAX_IDENTIFIERS_IN_IN_STMT = 1_000
|
25
25
|
|
26
26
|
attr_reader :relation, :root_dumper, :model_class, :association_path, :search_key, :identifiers, :nullify_columns,
|
27
|
-
:belongs_to_reflections, :has_many_reflections, :foreign_keys, :loaded_ids, :
|
28
|
-
:record_dumper, :inverse_reflection, :table_names, :options,
|
29
|
-
:current_deep, :verbose
|
27
|
+
:belongs_to_reflections, :has_many_reflections, :foreign_keys, :loaded_ids, :local_load_map,
|
28
|
+
:records, :record_dumper, :inverse_reflection, :table_names, :options,
|
29
|
+
:current_deep, :verbose, :custom_scope
|
30
30
|
|
31
|
-
delegate :root, :configuration, :dont_nullify, :total_limit, :deep_limit, :loaded_map, to: :root_dumper
|
31
|
+
delegate :root, :configuration, :dont_nullify, :total_limit, :deep_limit, :loaded_map, :to_load_map, to: :root_dumper
|
32
32
|
|
33
33
|
def initialize(relation, root_dumper, association_path, **options)
|
34
|
+
puts("- #{association_path}") if root_dumper.configuration.verbose
|
35
|
+
|
34
36
|
@relation = relation
|
35
37
|
@root_dumper = root_dumper
|
36
38
|
@verbose = configuration.verbose
|
37
39
|
@identifiers = options[:identifiers]
|
38
|
-
@
|
40
|
+
@local_load_map = Hash.new { |h, k| h[k] = [] }
|
39
41
|
@foreign_keys = Hash.new { |h, k| h[k] = [] }
|
40
42
|
@loaded_ids = []
|
41
43
|
@model_class = relation.klass
|
42
44
|
@search_key = options[:search_key] || model_class.primary_key
|
43
45
|
@association_path = association_path
|
44
46
|
@inverse_reflection = options[:inverse_of]
|
47
|
+
@records = []
|
45
48
|
@record_dumper = configuration.record_dumper_class.new(model_class, configuration, self)
|
46
49
|
@nullify_columns = []
|
47
50
|
@table_names = {}
|
@@ -50,8 +53,7 @@ module EvilSeed
|
|
50
53
|
@options = options
|
51
54
|
@current_deep = association_path.split('.').size
|
52
55
|
@dont_nullify = dont_nullify
|
53
|
-
|
54
|
-
puts("- #{association_path}") if verbose
|
56
|
+
@custom_scope = options[:custom_scope]
|
55
57
|
end
|
56
58
|
|
57
59
|
# Generate dump and write it into +io+
|
@@ -59,11 +61,13 @@ module EvilSeed
|
|
59
61
|
def call
|
60
62
|
dump!
|
61
63
|
if deep_limit and current_deep > deep_limit
|
62
|
-
[
|
64
|
+
[dump_records!].flatten.compact
|
63
65
|
else
|
64
|
-
|
65
|
-
|
66
|
-
|
66
|
+
[
|
67
|
+
dump_belongs_to_associations!,
|
68
|
+
dump_records!,
|
69
|
+
dump_has_many_associations!,
|
70
|
+
].flatten.compact
|
67
71
|
end
|
68
72
|
end
|
69
73
|
|
@@ -73,7 +77,15 @@ module EvilSeed
|
|
73
77
|
original_ignored_columns = model_class.ignored_columns
|
74
78
|
model_class.ignored_columns += Array(configuration.ignored_columns_for(model_class.sti_name))
|
75
79
|
model_class.send(:reload_schema_from_cache) if ActiveRecord.version < Gem::Version.new("6.1.0.rc1") # See https://github.com/rails/rails/pull/37581
|
76
|
-
if
|
80
|
+
if custom_scope
|
81
|
+
puts(" # #{search_key} (with scope)") if verbose
|
82
|
+
attrs = fetch_attributes(relation)
|
83
|
+
puts(" -- dumped #{attrs.size}") if verbose
|
84
|
+
attrs.each do |attributes|
|
85
|
+
next unless check_limits!
|
86
|
+
dump_record!(attributes)
|
87
|
+
end
|
88
|
+
elsif identifiers.present?
|
77
89
|
puts(" # #{search_key} => #{identifiers}") if verbose
|
78
90
|
# Don't use AR::Base#find_each as we will get error on Oracle if we will have more than 1000 ids in IN statement
|
79
91
|
identifiers.in_groups_of(MAX_IDENTIFIERS_IN_IN_STMT).each do |ids|
|
@@ -105,40 +117,49 @@ module EvilSeed
|
|
105
117
|
attributes[nullify_column] = nil
|
106
118
|
end
|
107
119
|
end
|
108
|
-
|
120
|
+
records << attributes
|
109
121
|
foreign_keys.each do |reflection_name, fk_column|
|
110
122
|
foreign_key = attributes[fk_column]
|
111
|
-
next if foreign_key.nil? || loaded_map[table_names[reflection_name]].include?(foreign_key)
|
112
|
-
|
123
|
+
next if foreign_key.nil? || loaded_map[table_names[reflection_name]].include?(foreign_key) || to_load_map[table_names[reflection_name]].include?(foreign_key)
|
124
|
+
local_load_map[reflection_name] << foreign_key
|
125
|
+
to_load_map[table_names[reflection_name]] << foreign_key
|
113
126
|
end
|
114
127
|
loaded_ids << attributes[model_class.primary_key]
|
115
128
|
end
|
116
129
|
|
130
|
+
def dump_records!
|
131
|
+
records.each do |attributes|
|
132
|
+
record_dumper.call(attributes)
|
133
|
+
end
|
134
|
+
record_dumper.result
|
135
|
+
end
|
136
|
+
|
117
137
|
def dump_belongs_to_associations!
|
118
138
|
belongs_to_reflections.map do |reflection|
|
119
|
-
next if
|
139
|
+
next if local_load_map[reflection.name].empty?
|
120
140
|
RelationDumper.new(
|
121
141
|
build_relation(reflection),
|
122
142
|
root_dumper,
|
123
143
|
"#{association_path}.#{reflection.name}",
|
124
144
|
search_key: reflection.association_primary_key,
|
125
|
-
identifiers:
|
145
|
+
identifiers: local_load_map[reflection.name],
|
126
146
|
limitable: false,
|
127
147
|
).call
|
128
148
|
end
|
129
149
|
end
|
130
150
|
|
131
151
|
def dump_has_many_associations!
|
132
|
-
has_many_reflections.map do |reflection|
|
152
|
+
has_many_reflections.map do |reflection, custom_scope|
|
133
153
|
next if loaded_ids.empty? || total_limit.try(:zero?)
|
134
154
|
RelationDumper.new(
|
135
|
-
build_relation(reflection),
|
155
|
+
build_relation(reflection, custom_scope),
|
136
156
|
root_dumper,
|
137
157
|
"#{association_path}.#{reflection.name}",
|
138
158
|
search_key: reflection.foreign_key,
|
139
|
-
identifiers: loaded_ids,
|
159
|
+
identifiers: loaded_ids - local_load_map[reflection.name],
|
140
160
|
inverse_of: reflection.inverse_of.try(:name),
|
141
161
|
limitable: true,
|
162
|
+
custom_scope: custom_scope,
|
142
163
|
).call
|
143
164
|
end
|
144
165
|
end
|
@@ -148,7 +169,7 @@ module EvilSeed
|
|
148
169
|
# @return [Array<Hash{String => String, Integer, Float, Boolean, nil}>]
|
149
170
|
def fetch_attributes(relation)
|
150
171
|
relation.pluck(*model_class.column_names).map do |row|
|
151
|
-
Hash[model_class.column_names.zip(row)]
|
172
|
+
Hash[model_class.column_names.zip(Array(row))]
|
152
173
|
end
|
153
174
|
end
|
154
175
|
|
@@ -157,13 +178,14 @@ module EvilSeed
|
|
157
178
|
root_dumper.check_limits!(association_path)
|
158
179
|
end
|
159
180
|
|
160
|
-
def build_relation(reflection)
|
181
|
+
def build_relation(reflection, custom_scope = nil)
|
161
182
|
if configuration.unscoped
|
162
183
|
relation = reflection.klass.unscoped
|
163
184
|
else
|
164
185
|
relation = reflection.klass.all
|
165
186
|
end
|
166
187
|
relation = relation.instance_eval(&reflection.scope) if reflection.scope
|
188
|
+
relation = relation.instance_eval(&custom_scope) if custom_scope
|
167
189
|
relation = relation.where(reflection.type => model_class.to_s) if reflection.options[:as] # polymorphic
|
168
190
|
relation
|
169
191
|
end
|
@@ -172,7 +194,10 @@ module EvilSeed
|
|
172
194
|
model_class.reflect_on_all_associations(:belongs_to).reject do |reflection|
|
173
195
|
next false if reflection.options[:polymorphic] # TODO: Add support for polymorphic belongs_to
|
174
196
|
included = root.included?("#{association_path}.#{reflection.name}")
|
175
|
-
excluded =
|
197
|
+
excluded = reflection.options[:optional] && root.excluded_optional_belongs_to?
|
198
|
+
excluded ||= root.excluded?("#{association_path}.#{reflection.name}")
|
199
|
+
inverse = reflection.name == inverse_reflection
|
200
|
+
puts " -- belongs_to #{reflection.name} #{"excluded by #{excluded}" if excluded} #{"re-included by #{included}" if included}" if verbose
|
176
201
|
if excluded and not included
|
177
202
|
if model_class.column_names.include?(reflection.foreign_key)
|
178
203
|
puts(" -- excluded #{reflection.foreign_key}") if verbose
|
@@ -182,7 +207,7 @@ module EvilSeed
|
|
182
207
|
foreign_keys[reflection.name] = reflection.foreign_key
|
183
208
|
table_names[reflection.name] = reflection.table_name
|
184
209
|
end
|
185
|
-
excluded and not included
|
210
|
+
excluded and not included or inverse
|
186
211
|
end
|
187
212
|
end
|
188
213
|
|
@@ -190,14 +215,21 @@ module EvilSeed
|
|
190
215
|
def setup_has_many_reflections
|
191
216
|
puts(" -- reflections #{model_class._reflections.keys}") if verbose
|
192
217
|
model_class._reflections.select do |_reflection_name, reflection|
|
218
|
+
next false unless %i[has_one has_many].include?(reflection.macro)
|
219
|
+
|
193
220
|
next false if model_class.primary_key.nil?
|
194
221
|
|
195
222
|
next false if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
196
223
|
|
197
224
|
included = root.included?("#{association_path}.#{reflection.name}")
|
198
|
-
excluded =
|
199
|
-
|
200
|
-
|
225
|
+
excluded = :inverse if reflection.name == inverse_reflection
|
226
|
+
excluded ||= root.excluded_has_relations?
|
227
|
+
excluded ||= root.excluded?("#{association_path}.#{reflection.name}")
|
228
|
+
puts " -- #{reflection.macro} #{reflection.name} #{"excluded by #{excluded}" if excluded} #{"re-included by #{included}" if included}" if verbose
|
229
|
+
!(excluded and not included)
|
230
|
+
end.map do |_reflection_name, reflection|
|
231
|
+
[reflection, root.included?("#{association_path}.#{reflection.name}")&.last]
|
232
|
+
end
|
201
233
|
end
|
202
234
|
end
|
203
235
|
end
|
@@ -7,12 +7,11 @@ module EvilSeed
|
|
7
7
|
class RootDumper
|
8
8
|
attr_reader :root, :dumper, :model_class, :total_limit, :deep_limit, :dont_nullify, :association_limits
|
9
9
|
|
10
|
-
delegate :loaded_map, :configuration, to: :dumper
|
10
|
+
delegate :loaded_map, :to_load_map, :configuration, to: :dumper
|
11
11
|
|
12
12
|
def initialize(root, dumper)
|
13
13
|
@root = root
|
14
14
|
@dumper = dumper
|
15
|
-
@to_load_map = {}
|
16
15
|
@total_limit = root.total_limit
|
17
16
|
@deep_limit = root.deep_limit
|
18
17
|
@dont_nullify = root.dont_nullify
|
@@ -28,6 +27,8 @@ module EvilSeed
|
|
28
27
|
relation = model_class.all
|
29
28
|
relation = relation.unscoped if configuration.unscoped
|
30
29
|
relation = relation.where(*root.constraints) if root.constraints.any? # without arguments returns not a relation
|
30
|
+
relation = relation.limit(root.limit) if root.limit
|
31
|
+
relation = relation.order(root.order) if root.order
|
31
32
|
RelationDumper.new(relation, self, association_path).call
|
32
33
|
end
|
33
34
|
|
data/lib/evil_seed/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: evil-seed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrey Novikov
|
8
8
|
- Vladimir Dementyev
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2025-02-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -31,14 +31,14 @@ dependencies:
|
|
31
31
|
requirements:
|
32
32
|
- - "~>"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '
|
34
|
+
version: '13.0'
|
35
35
|
type: :development
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '
|
41
|
+
version: '13.0'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: minitest
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -175,7 +175,7 @@ homepage: https://github.com/palkan/evil-seed
|
|
175
175
|
licenses:
|
176
176
|
- MIT
|
177
177
|
metadata: {}
|
178
|
-
post_install_message:
|
178
|
+
post_install_message:
|
179
179
|
rdoc_options: []
|
180
180
|
require_paths:
|
181
181
|
- lib
|
@@ -190,8 +190,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
190
190
|
- !ruby/object:Gem::Version
|
191
191
|
version: '0'
|
192
192
|
requirements: []
|
193
|
-
rubygems_version: 3.5.
|
194
|
-
signing_key:
|
193
|
+
rubygems_version: 3.5.22
|
194
|
+
signing_key:
|
195
195
|
specification_version: 4
|
196
196
|
summary: Create partial and anonymized production database dumps for use in development
|
197
197
|
test_files: []
|