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 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
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/spijet/activerecord_dumper/blob/master/LICENSE)
3
+ [![GitHub issues](https://img.shields.io/github/issues/spijet/activerecord_dumper.svg)](https://github.com/spijet/activerecord_dumper/issues)
4
+ [![Build Status](https://img.shields.io/travis/spijet/activerecord_dumper.svg)](https://travis-ci.com/spijet/activerecord_dumper)
5
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/spijet/activerecord_dumper/rspec.yml?label=RSpec)](https://github.com/spijet/activerecord_dumper/actions/workflows/rspec.yml)
6
+ [![Coverage Status](https://img.shields.io/coveralls/spijet/activerecord_dumper.svg)](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,3 @@
1
+ module ActiveRecordDumper
2
+ VERSION = '0.9.0'.freeze
3
+ 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