activerecord_dumper 0.9.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 +7 -0
- data/README.md +68 -0
- data/lib/activerecord_dumper/csv_db.rb +73 -0
- data/lib/activerecord_dumper/rake_tasks.rb +55 -0
- data/lib/activerecord_dumper/serialization_helper.rb +212 -0
- data/lib/activerecord_dumper/version.rb +3 -0
- data/lib/activerecord_dumper.rb +75 -0
- data/lib/tasks/activerecord_dumper_tasks.rake +29 -0
- data/spec/activerecord_dumper/dump_spec.rb +54 -0
- data/spec/activerecord_dumper/integration_spec.rb +49 -0
- data/spec/activerecord_dumper/load_spec.rb +42 -0
- data/spec/activerecord_dumper/rake_tasks_spec.rb +96 -0
- data/spec/activerecord_dumper/serialization_helper_base_spec.rb +54 -0
- data/spec/activerecord_dumper/serialization_helper_dump_spec.rb +100 -0
- data/spec/activerecord_dumper/serialization_helper_load_spec.rb +73 -0
- data/spec/activerecord_dumper/serialization_helper_utils_spec.rb +60 -0
- data/spec/activerecord_dumper/utils_spec.rb +23 -0
- data/spec/spec_helper.rb +77 -0
- data/spec/tasks/activerecord_dumper_tasks_spec.rb +66 -0
- metadata +130 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2257a54f22f47577b964bcc2ccc807350eae41e165cf450cfdaae99df2767770
|
|
4
|
+
data.tar.gz: 2865477d775d608804fa9c8fc3ae1c2a5a85920b3f62fdcad3799aec527f3d1c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5ad675615728f99ddac5801b8456cda901e3be335867a64f3b537ce4e6070a5ea96cdec10d22294b945b3bddf04a55dcaa386454dc2a3f6cf15d814f3085e18e
|
|
7
|
+
data.tar.gz: 37d1aaa78937bfaadd31bab80634547274e6ce5734a95e5d0a3ae965d726bf3a9951142afeb5f9c59b49c40e077a566c67d09a7040a4aca7b5e8acd00429dbca
|
data/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ActiveRecord Dumper
|
|
2
|
+
[](https://github.com/spijet/activerecord_dumper/blob/master/LICENSE)
|
|
3
|
+
[](https://github.com/spijet/activerecord_dumper/issues)
|
|
4
|
+
[](https://travis-ci.com/spijet/activerecord_dumper)
|
|
5
|
+
[](https://github.com/spijet/activerecord_dumper/actions/workflows/rspec.yml)
|
|
6
|
+
[](https://coveralls.io/github/spijet/activerecord_dumper?branch=master)
|
|
7
|
+
|
|
8
|
+
ActiveRecordDumper is a fork of YamlDb gem without any explicit Rails dependencies. This
|
|
9
|
+
way it can be used by any AR-enabled app (e.g. Sinatra) without pulling whole
|
|
10
|
+
Rails in.
|
|
11
|
+
|
|
12
|
+
YamlDB/ActiveRecordDumper is a database-independent format for dumping and
|
|
13
|
+
restoring data. It complements the database-independent schema format found in
|
|
14
|
+
`db/schema.rb`. The data is saved into `db/data.yml`. This can be used as a
|
|
15
|
+
replacement for `mysqldump` or `pg_dump`, but it only supports features found in
|
|
16
|
+
ActiveRecord-based (Rails, etc.) apps. Users, permissions, schemas, triggers,
|
|
17
|
+
and other advanced database features are not supported by design.
|
|
18
|
+
|
|
19
|
+
Any database that has an ActiveRecord adapter should work.
|
|
20
|
+
This gem aims to support ActiveRecord versions 4.2 through 8.1.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Simply add to your `Gemfile`:
|
|
26
|
+
|
|
27
|
+
gem 'activerecord_dumper'
|
|
28
|
+
|
|
29
|
+
All rake tasks will then be available to you.
|
|
30
|
+
|
|
31
|
+
## Usage (Rails)
|
|
32
|
+
|
|
33
|
+
rake db:data:dump -> Dump contents of Rails database to db/data.yml
|
|
34
|
+
rake db:data:load -> Load contents of db/data.yml into the database
|
|
35
|
+
|
|
36
|
+
Further, there are tasks `db:dump` and `db:load` which do the entire database
|
|
37
|
+
(the equivalent of running `db:schema:dump` followed by `db:data:load`). Also,
|
|
38
|
+
there are other tasks recently added that allow the export of the database
|
|
39
|
+
contents to/from multiple files (each one named after the table being dumped or
|
|
40
|
+
loaded).
|
|
41
|
+
|
|
42
|
+
rake db:data:dump_dir -> Dump contents of database to curr_dir_name/tablename.extension (defaults to yaml)
|
|
43
|
+
rake db:data:load_dir -> Load contents of db/#{dir} into database (where dir is ENV['dir'] || 'base')
|
|
44
|
+
|
|
45
|
+
In addition, we have plugins whereby you can export your database to/from
|
|
46
|
+
various formats. We only deal with YAML and CSV right now, but you can easily
|
|
47
|
+
write tools for your own formats (such as Excel or XML). To use another format,
|
|
48
|
+
just load setting the "class" parameter to the class you are using. This
|
|
49
|
+
defaults to "YamlDb::Helper" which is a refactoring of the old yaml_db code.
|
|
50
|
+
We'll shorten this to use class nicknames in a little bit.
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
One common use would be to switch your data from one database backend to
|
|
55
|
+
another. For example, let's say you wanted to switch from SQLite to MySQL. You
|
|
56
|
+
might execute the following steps:
|
|
57
|
+
|
|
58
|
+
1. `rake db:dump`;
|
|
59
|
+
|
|
60
|
+
2. Edit `config/database.yml` and change your adapter to `mysql`, set up database params;
|
|
61
|
+
|
|
62
|
+
3. `mysqladmin create [database name]`
|
|
63
|
+
|
|
64
|
+
4. `rake db:load`
|
|
65
|
+
|
|
66
|
+
## Credits
|
|
67
|
+
|
|
68
|
+
Created by Orion Henry and Adam Wiggins. Major updates by Ricardo Chimal Jr. and Nate Kidwell. Adapted for non-Rails usage by Serge Tkatchouk.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module ActiveRecordDumper
|
|
2
|
+
module CSVDB
|
|
3
|
+
module Helper
|
|
4
|
+
def self.loader
|
|
5
|
+
Load
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.dumper
|
|
9
|
+
Dump
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.extension
|
|
13
|
+
'csv'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Load < SerializationHelper::Load
|
|
18
|
+
def self.load_documents(io, truncate = true)
|
|
19
|
+
tables = {}
|
|
20
|
+
curr_table = nil
|
|
21
|
+
io.each do |line|
|
|
22
|
+
if /BEGIN_CSV_TABLE_DECLARATION(.+)END_CSV_TABLE_DECLARATION/ =~ line
|
|
23
|
+
curr_table = Regexp.last_match[1] if Regexp.last_match
|
|
24
|
+
tables[curr_table] = {}
|
|
25
|
+
elsif tables[curr_table]['columns']
|
|
26
|
+
tables[curr_table]['records'] << FasterCSV.parse(line)[0]
|
|
27
|
+
else
|
|
28
|
+
tables[curr_table]['columns'] = FasterCSV.parse(line)[0]
|
|
29
|
+
tables[curr_table]['records'] = []
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
tables.each_pair do |table_name, contents|
|
|
34
|
+
load_table(table_name, contents, truncate)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Dump < SerializationHelper::Dump
|
|
40
|
+
def self.before_table(io, table)
|
|
41
|
+
io.write "BEGIN_CSV_TABLE_DECLARATION#{table}END_CSV_TABLE_DECLARATION\n"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.dump(io)
|
|
45
|
+
tables.each do |table|
|
|
46
|
+
before_table(io, table)
|
|
47
|
+
dump_table(io, table)
|
|
48
|
+
after_table(io, table)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.after_table(io, _table)
|
|
53
|
+
io.write ''
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.dump_table_columns(io, table)
|
|
57
|
+
io.write(table_column_names(table).to_csv)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.dump_table_records(io, table)
|
|
61
|
+
column_names = table_column_names(table)
|
|
62
|
+
helper = SerializationHelper::Utils
|
|
63
|
+
|
|
64
|
+
each_table_page(table) do |records|
|
|
65
|
+
rows = helper.unhash_records(records, column_names)
|
|
66
|
+
records.each do |record|
|
|
67
|
+
io.write(record.to_csv)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module ActiveRecordDumper
|
|
2
|
+
module RakeTasks
|
|
3
|
+
def self.data_dump_task
|
|
4
|
+
SerializationHelper::Base.new(helper).dump(db_dump_data_file(helper.extension))
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.data_dump_dir_task
|
|
8
|
+
dir = ENV['dir'] || default_dir_name
|
|
9
|
+
SerializationHelper::Base.new(helper).dump_to_dir(dump_dir("/#{dir}"))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.data_load_task
|
|
13
|
+
SerializationHelper::Base.new(helper).load(db_dump_data_file(helper.extension))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.data_load_dir_task
|
|
17
|
+
dir = ENV['dir'] || 'base'
|
|
18
|
+
SerializationHelper::Base.new(helper).load_from_dir(dump_dir("/#{dir}"))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Private class methods below.
|
|
22
|
+
|
|
23
|
+
def self.default_dir_name
|
|
24
|
+
Time.now.strftime('%FT%H%M%S')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.db_dump_data_file(extension = 'yml')
|
|
28
|
+
"#{dump_dir}/data.#{extension}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.dump_dir(dir = '')
|
|
32
|
+
"#{root_dir}/db#{dir}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.root_dir
|
|
36
|
+
return Rails.root if defined? Rails
|
|
37
|
+
return APP_DIR if defined? APP_DIR
|
|
38
|
+
return APP_ROOT if defined? APP_ROOT
|
|
39
|
+
|
|
40
|
+
if ENV.key? 'APP_DIR'
|
|
41
|
+
ENV['APP_DIR']
|
|
42
|
+
elsif ENV.key? 'APP_ROOT'
|
|
43
|
+
ENV['APP_ROOT']
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.helper
|
|
48
|
+
format_class = ENV['class'] || 'ActiveRecordDumper::Helper'
|
|
49
|
+
format_class.constantize
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private_class_method :default_dir_name, :db_dump_data_file,
|
|
53
|
+
:dump_dir, :root_dir, :helper
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
require 'active_support/core_ext/kernel/reporting'
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDumper
|
|
4
|
+
module SerializationHelper
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :extension
|
|
7
|
+
|
|
8
|
+
def initialize(helper)
|
|
9
|
+
@dumper = helper.dumper
|
|
10
|
+
@loader = helper.loader
|
|
11
|
+
@extension = helper.extension
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def dump(filename)
|
|
15
|
+
disable_logger
|
|
16
|
+
File.open(filename, 'w') do |file|
|
|
17
|
+
@dumper.dump(file)
|
|
18
|
+
end
|
|
19
|
+
reenable_logger
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dump_to_dir(dirname)
|
|
23
|
+
Dir.mkdir(dirname)
|
|
24
|
+
tables = @dumper.tables
|
|
25
|
+
tables.each do |table|
|
|
26
|
+
File.open("#{dirname}/#{table}.#{@extension}", 'w') do |io|
|
|
27
|
+
@dumper.before_table(io, table)
|
|
28
|
+
@dumper.dump_table io, table
|
|
29
|
+
@dumper.after_table(io, table)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def load(filename, truncate = true)
|
|
35
|
+
disable_logger
|
|
36
|
+
@loader.load(File.new(filename, 'r'), truncate)
|
|
37
|
+
reenable_logger
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load_from_dir(dirname, truncate = true)
|
|
41
|
+
Dir.entries(dirname).each do |filename|
|
|
42
|
+
next if /^\./ =~ filename
|
|
43
|
+
|
|
44
|
+
@loader.load(File.new("#{dirname}/#{filename}", 'r'), truncate)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def disable_logger
|
|
49
|
+
@@old_logger = ActiveRecord::Base.logger
|
|
50
|
+
ActiveRecord::Base.logger = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reenable_logger
|
|
54
|
+
ActiveRecord::Base.logger = @@old_logger
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Load
|
|
59
|
+
def self.load(io, truncate = true)
|
|
60
|
+
ActiveRecord::Base.connection.transaction do
|
|
61
|
+
load_documents(io, truncate)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.truncate_table(table)
|
|
66
|
+
ActiveRecord::Base.connection.execute("TRUNCATE #{Utils.quote_table(table)}")
|
|
67
|
+
rescue Exception
|
|
68
|
+
ActiveRecord::Base.connection.execute("DELETE FROM #{Utils.quote_table(table)}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.load_table(table, data, truncate = true)
|
|
72
|
+
column_names = data['columns']
|
|
73
|
+
truncate_table(table) if truncate
|
|
74
|
+
load_records(table, column_names, data['records'])
|
|
75
|
+
reset_pk_sequence!(table)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.load_records(table, column_names, records)
|
|
79
|
+
return if column_names.nil?
|
|
80
|
+
|
|
81
|
+
quoted_column_names = column_names.map do |column|
|
|
82
|
+
ActiveRecord::Base.connection.quote_column_name(column)
|
|
83
|
+
end.join(',')
|
|
84
|
+
quoted_table_name = Utils.quote_table(table)
|
|
85
|
+
records.each do |record|
|
|
86
|
+
quoted_values = record.map do |c|
|
|
87
|
+
ActiveRecord::Base.connection.quote(c)
|
|
88
|
+
end.join(',')
|
|
89
|
+
ActiveRecord::Base.connection.execute(
|
|
90
|
+
"INSERT INTO #{quoted_table_name} (#{quoted_column_names}) VALUES (#{quoted_values})"
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.reset_pk_sequence!(table_name)
|
|
96
|
+
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
|
|
97
|
+
ActiveRecord::Base.connection.reset_pk_sequence!(table_name)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
module Utils
|
|
103
|
+
def self.unhash(hash, keys)
|
|
104
|
+
keys.map { |key| hash[key] }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.unhash_records(records, keys)
|
|
108
|
+
records.each_with_index do |record, index|
|
|
109
|
+
records[index] = unhash(record, keys)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
records
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.convert_booleans(records, columns)
|
|
116
|
+
records.each do |record|
|
|
117
|
+
columns.each do |column|
|
|
118
|
+
next if is_boolean(record[column])
|
|
119
|
+
|
|
120
|
+
record[column] = convert_boolean(record[column])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
records
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.convert_boolean(value)
|
|
127
|
+
['t', '1', true, 1].include?(value)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.boolean_columns(table)
|
|
131
|
+
columns = ActiveRecord::Base.connection.columns(table).reject { |c| silence_warnings { c.type != :boolean } }
|
|
132
|
+
columns.map(&:name)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.is_boolean(value)
|
|
136
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.quote_table(table)
|
|
140
|
+
ActiveRecord::Base.connection.quote_table_name(table)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.quote_column(column)
|
|
144
|
+
ActiveRecord::Base.connection.quote_column_name(column)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
class Dump
|
|
149
|
+
def self.before_table(io, table); end
|
|
150
|
+
|
|
151
|
+
def self.dump(io)
|
|
152
|
+
tables.each do |table|
|
|
153
|
+
before_table(io, table)
|
|
154
|
+
dump_table(io, table)
|
|
155
|
+
after_table(io, table)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def self.after_table(io, table); end
|
|
160
|
+
|
|
161
|
+
def self.tables
|
|
162
|
+
ActiveRecord::Base.connection.tables.reject do |table|
|
|
163
|
+
%w[schema_info schema_migrations].include?(table)
|
|
164
|
+
end.sort
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.dump_table(io, table)
|
|
168
|
+
return if table_record_count(table).zero?
|
|
169
|
+
|
|
170
|
+
dump_table_columns(io, table)
|
|
171
|
+
dump_table_records(io, table)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.table_column_names(table)
|
|
175
|
+
ActiveRecord::Base.connection.columns(table).map(&:name)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.each_table_page(table, records_per_page = 1000)
|
|
179
|
+
total_count = table_record_count(table)
|
|
180
|
+
pages = (total_count.to_f / records_per_page).ceil - 1
|
|
181
|
+
keys = sort_keys(table)
|
|
182
|
+
boolean_columns = Utils.boolean_columns(table)
|
|
183
|
+
|
|
184
|
+
(0..pages).to_a.each do |page|
|
|
185
|
+
query = Arel::Table.new(table).order(*keys).skip(records_per_page * page).take(records_per_page).project(Arel.sql('*'))
|
|
186
|
+
records = ActiveRecord::Base.connection.select_all(query.to_sql)
|
|
187
|
+
records = Utils.convert_booleans(records, boolean_columns)
|
|
188
|
+
yield records
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def self.table_record_count(table)
|
|
193
|
+
ActiveRecord::Base.connection.select_one(
|
|
194
|
+
"SELECT COUNT(*) FROM #{Utils.quote_table(table)}"
|
|
195
|
+
).values.first.to_i
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Return the first column as sort key unless the table looks like a
|
|
199
|
+
# standard has_and_belongs_to_many join table,
|
|
200
|
+
# in which case add the second "ID column"
|
|
201
|
+
def self.sort_keys(table)
|
|
202
|
+
first_column, second_column = table_column_names(table)
|
|
203
|
+
|
|
204
|
+
if [first_column, second_column].all? { |name| name =~ /_id$/ }
|
|
205
|
+
[Utils.quote_column(first_column), Utils.quote_column(second_column)]
|
|
206
|
+
else
|
|
207
|
+
[Utils.quote_column(first_column)]
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
require 'active_record'
|
|
4
|
+
require 'activerecord_dumper/rake_tasks'
|
|
5
|
+
require 'activerecord_dumper/version'
|
|
6
|
+
require 'activerecord_dumper/serialization_helper'
|
|
7
|
+
|
|
8
|
+
module ActiveRecordDumper
|
|
9
|
+
module Helper
|
|
10
|
+
def self.loader
|
|
11
|
+
Load
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.dumper
|
|
15
|
+
Dump
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.extension
|
|
19
|
+
'yml'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Utils
|
|
24
|
+
def self.chunk_records(records)
|
|
25
|
+
yaml = [records].to_yaml
|
|
26
|
+
yaml.sub!(/---\s\n|---\n/, '')
|
|
27
|
+
yaml.sub!('- - -', ' - -')
|
|
28
|
+
yaml
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Dump < SerializationHelper::Dump
|
|
33
|
+
def self.dump_table_columns(io, table)
|
|
34
|
+
io.write("\n")
|
|
35
|
+
io.write({ table => { 'columns' => table_column_names(table) } }.to_yaml)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.dump_table_records(io, table)
|
|
39
|
+
table_record_header(io)
|
|
40
|
+
|
|
41
|
+
column_names = table_column_names(table)
|
|
42
|
+
|
|
43
|
+
each_table_page(table) do |records|
|
|
44
|
+
rows = SerializationHelper::Utils.unhash_records(
|
|
45
|
+
records.to_a, column_names
|
|
46
|
+
)
|
|
47
|
+
io.write(Utils.chunk_records(rows))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.table_record_header(io)
|
|
52
|
+
io.write(" records:\n")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Load < SerializationHelper::Load
|
|
57
|
+
def self.load_documents(io, truncate = true)
|
|
58
|
+
YAML.load_stream(io) do |ydoc|
|
|
59
|
+
ydoc.keys.each do |table_name|
|
|
60
|
+
next if ydoc[table_name].nil?
|
|
61
|
+
|
|
62
|
+
load_table(table_name, ydoc[table_name], truncate)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if defined?(Rails::Railtie)
|
|
69
|
+
class Railtie < Rails::Railtie
|
|
70
|
+
rake_tasks do
|
|
71
|
+
load File.expand_path('tasks/activerecord_dumper_tasks.rake', __dir__)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
namespace :db do
|
|
2
|
+
desc 'Dump schema and data to db/schema.rb and db/data.yml'
|
|
3
|
+
task(dump: ['db:schema:dump', 'db:data:dump'])
|
|
4
|
+
|
|
5
|
+
desc 'Load schema and data from db/schema.rb and db/data.yml'
|
|
6
|
+
task(load: ['db:schema:load', 'db:data:load'])
|
|
7
|
+
|
|
8
|
+
namespace :data do
|
|
9
|
+
desc 'Dump contents of database to db/data.extension (defaults to yaml)'
|
|
10
|
+
task dump: :environment do
|
|
11
|
+
ActiveRecordDumper::RakeTasks.data_dump_task
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc 'Dump contents of database to curr_dir_name/tablename.extension (defaults to yaml)'
|
|
15
|
+
task dump_dir: :environment do
|
|
16
|
+
ActiveRecordDumper::RakeTasks.data_dump_dir_task
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc 'Load contents of db/data.extension (defaults to yaml) into database'
|
|
20
|
+
task load: :environment do
|
|
21
|
+
ActiveRecordDumper::RakeTasks.data_load_task
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc 'Load contents of db/data_dir into database'
|
|
25
|
+
task load_dir: :environment do
|
|
26
|
+
ActiveRecordDumper::RakeTasks.data_load_dir_task
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module ActiveRecordDumper
|
|
2
|
+
RSpec.describe Dump do
|
|
3
|
+
before do
|
|
4
|
+
allow(ActiveRecord::Base).to receive(:connection).and_return(double('connection').as_null_object)
|
|
5
|
+
allow(ActiveRecord::Base.connection).to receive(:tables).and_return(%w[mytable schema_info schema_migrations])
|
|
6
|
+
allow(ActiveRecord::Base.connection).to receive(:columns).with('mytable').and_return([double('a', name: 'a', type: :string), double('b', name: 'b', type: :string)])
|
|
7
|
+
allow(ActiveRecord::Base.connection).to receive(:select_one).and_return('count' => '2')
|
|
8
|
+
allow(ActiveRecord::Base.connection).to receive(:select_all).and_return([{ 'a' => 1, 'b' => 2 }, { 'a' => 3, 'b' => 4 }])
|
|
9
|
+
allow(Utils).to receive(:quote_table).with('mytable').and_return('mytable')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before(:each) do
|
|
13
|
+
allow(File).to receive(:open).with('dump.yml', 'w').and_yield(StringIO.new)
|
|
14
|
+
@io = StringIO.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'returns a formatted string' do
|
|
18
|
+
Dump.table_record_header(@io)
|
|
19
|
+
@io.rewind
|
|
20
|
+
expect(@io.read).to eq(" records:\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'returns a yaml string that contains a table header and column names' do
|
|
24
|
+
allow(Dump).to receive(:table_column_names).with('mytable').and_return(%w[a b])
|
|
25
|
+
Dump.dump_table_columns(@io, 'mytable')
|
|
26
|
+
@io.rewind
|
|
27
|
+
expected_yml = <<EOYAML
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
mytable:
|
|
31
|
+
columns:
|
|
32
|
+
- a
|
|
33
|
+
- b
|
|
34
|
+
EOYAML
|
|
35
|
+
expect(@io.read).to eq(expected_yml)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'dumps the records for a table in yaml to a given io stream' do
|
|
39
|
+
allow(SerializationHelper::Dump).to receive(:each_table_page).with('mytable').and_yield(
|
|
40
|
+
[{ 'a' => 1, 'b' => 2 }, { 'a' => 3, 'b' => 4 }]
|
|
41
|
+
)
|
|
42
|
+
Dump.dump_table_records(@io, 'mytable')
|
|
43
|
+
@io.rewind
|
|
44
|
+
expected_yml = <<EOYAML
|
|
45
|
+
records:
|
|
46
|
+
- - 1
|
|
47
|
+
- 2
|
|
48
|
+
- - 3
|
|
49
|
+
- 4
|
|
50
|
+
EOYAML
|
|
51
|
+
expect(@io.read).to eq(expected_yml)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'active_record/railtie'
|
|
2
|
+
# require 'active_record'
|
|
3
|
+
|
|
4
|
+
# connect to in-memory SQLite database
|
|
5
|
+
ActiveRecord::Base.establish_connection(
|
|
6
|
+
adapter: 'sqlite3',
|
|
7
|
+
database: ':memory:'
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# define a dummy User model
|
|
11
|
+
class User < ActiveRecord::Base
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# create the users table
|
|
15
|
+
ActiveRecord::Schema.define do
|
|
16
|
+
self.verbose = false
|
|
17
|
+
|
|
18
|
+
create_table :users, force: true do |t|
|
|
19
|
+
t.string :username
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# add some users
|
|
24
|
+
User.create(
|
|
25
|
+
[
|
|
26
|
+
{ username: 'alice' },
|
|
27
|
+
{ username: 'bob' }
|
|
28
|
+
]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
RSpec.describe 'with real ActiveRecord,' do
|
|
32
|
+
it 'contains two users' do
|
|
33
|
+
expect(User.count).to eq(2)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'dumps the user records' do
|
|
37
|
+
@io = StringIO.new
|
|
38
|
+
ActiveRecordDumper::Dump.dump_table_records(@io, 'users')
|
|
39
|
+
@io.rewind
|
|
40
|
+
expected_yml = <<EOYAML
|
|
41
|
+
records:
|
|
42
|
+
- - 1
|
|
43
|
+
- alice
|
|
44
|
+
- - 2
|
|
45
|
+
- bob
|
|
46
|
+
EOYAML
|
|
47
|
+
expect(@io.read).to eq(expected_yml)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module ActiveRecordDumper
|
|
2
|
+
RSpec.describe Load do
|
|
3
|
+
before do
|
|
4
|
+
allow(SerializationHelper::Utils).to(
|
|
5
|
+
receive(:quote_table).with('mytable').and_return('mytable')
|
|
6
|
+
)
|
|
7
|
+
allow(ActiveRecord::Base).to(
|
|
8
|
+
receive(:connection).and_return(double('connection').as_null_object)
|
|
9
|
+
)
|
|
10
|
+
allow(ActiveRecord::Base.connection).to receive(:transaction).and_yield
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
before(:each) do
|
|
14
|
+
@io = StringIO.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'calls load structure for each document in the file' do
|
|
18
|
+
stream_struct = {
|
|
19
|
+
'mytable' => {
|
|
20
|
+
'columns' => %w[a b],
|
|
21
|
+
'records' => [[1, 2], [3, 4]]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
expect(YAML).to receive(:load_stream).with(@io).and_yield(stream_struct)
|
|
25
|
+
table_struct = [
|
|
26
|
+
'mytable',
|
|
27
|
+
{ 'columns' => %w[a b], 'records' => [[1, 2], [3, 4]] },
|
|
28
|
+
true
|
|
29
|
+
]
|
|
30
|
+
expect(Load).to receive(:load_table).with(*table_struct)
|
|
31
|
+
Load.load(@io)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'calls load structure when the document contains no records' do
|
|
35
|
+
expect(YAML).to receive(:load_stream).with(@io).and_yield(
|
|
36
|
+
'mytable' => nil
|
|
37
|
+
)
|
|
38
|
+
expect(Load).not_to receive(:load_table)
|
|
39
|
+
Load.load(@io)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|