evil-seed 0.6.0 → 0.7.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/.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: []
|