relational_exporter 0.0.4 → 0.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8c1b454011ca52b5ec7122722273f8503e1bd55a
4
- data.tar.gz: 3c8f0d8b4371f5177d4b04b46debd9ef0df49287
3
+ metadata.gz: 9fef27ab7b4fa6f13f35191995f9c8590ac6bd59
4
+ data.tar.gz: e6bfdaae73b812140fe7ded8747d03384d8a9f4d
5
5
  SHA512:
6
- metadata.gz: 5ce81c194f3d0168cdf9dd88ba22f35e8a1909e269d925a2b560b302f3930054ca0c505d7f975ad50e52988c6cd7ea0880ddd1870e0a5f912a628428500e9890
7
- data.tar.gz: 3db2e8d53cfe005eadf99f53034d50a325f0b1c4d477fd7999fae25f3019f77f5225f86854f9190c41398a21736b21af75d8c4ddee571dc36041659298ff0fa6
6
+ metadata.gz: 13c2c195e7c783b734b93d51e6aba5edac1bebf48fad6f3b149e55a28525a2f276eae232ac4aa2d3676702ba04b869d3d70d3fcb09f8a20134cda1204e744419
7
+ data.tar.gz: 0413a341cfed6cad14169aadfb296a2cc841cf7aadc7909b63fc992efb67bdc48b50d9d607bb676bf40f829ddd8e4e0dd470ff3b9b9c842bf549ac3011dd4fe6
@@ -3,6 +3,9 @@ require 'csv'
3
3
  require 'hashie'
4
4
  require 'relational_exporter/version'
5
5
  require 'relational_exporter/active_record_extension'
6
+ require 'relational_exporter/csv_builder'
7
+ require 'relational_exporter/record_worker'
8
+ require 'celluloid'
6
9
 
7
10
  module RelationalExporter
8
11
  class Runner
@@ -26,6 +29,7 @@ module RelationalExporter
26
29
 
27
30
  def export(output_config, &block)
28
31
  ActiveRecord::Base.logger = @logger
32
+ Celluloid.logger = @logger
29
33
 
30
34
  output_config = Hashie::Mash.new output_config
31
35
 
@@ -33,128 +37,36 @@ module RelationalExporter
33
37
 
34
38
  main_klass.set_scope_from_hash output_config.output.scope.as_json
35
39
 
36
- header_row = []
37
- max_associations = {}
38
-
39
- csv_options = {headers: true}
40
- if output_config.file_path.blank?
41
- csv_method = :instance
42
- csv_args = [STDOUT, csv_options]
43
- else
44
- csv_method = :open
45
- csv_args = [output_config.file_path, 'wb', csv_options]
46
- end
47
-
48
- ::CSV.send(csv_method, *csv_args) do |csv|
49
- main_klass.find_all_by_scope(output_config.output.scope.as_json).find_in_batches do |batch|
50
- batch.each do |single|
51
- if block_given?
52
- yield single
53
-
54
- return unless single
55
- end
56
-
57
- row = []
58
-
59
- # Add main record headers
60
- serialized_attributes(single).each do |field, value|
61
- header_row << csv_header_prefix_for_key(main_klass, field) if csv.header_row?
62
- row << value
63
- end
64
-
65
- output_config.output.associations.each do |association_accessor, association_options|
66
- association_accessor = association_accessor.to_s.to_sym
67
- association_klass = association_accessor.to_s.classify.constantize
68
- scope = symbolize_options association_options.scope
69
-
70
- associated = single.send association_accessor
71
- # TODO - this might suck for single associations (has_one) because they don't return an ar::associations::collectionproxy
72
- associated = associated.find_all_by_scope(scope) unless scope.blank? || !associated.respond_to?(:find_all_by_scope)
73
-
74
- if associated.is_a? Hash
75
- associated = [ associated ]
76
- elsif associated.blank?
77
- associated = []
78
- end
79
-
80
- foreign_key = main_klass.reflections[association_accessor].foreign_key rescue nil
81
-
82
- fields = serialized_attributes(association_klass).keys
83
-
84
- fields.reject! {|v| v == foreign_key } if foreign_key
85
-
86
- if csv.header_row?
87
- case main_klass.reflections[association_accessor].macro
88
- when :has_many
89
- max_associated = association_klass.find_all_by_scope(scope)
90
- .joins(main_klass.table_name.to_sym)
91
- .order('count_all desc')
92
- .group(foreign_key)
93
- .limit(1).count.flatten[1]
94
- when :has_one
95
- max_associated = 1
96
- end
97
-
98
- max_associated = 0 if max_associated.nil?
99
-
100
- max_associations[association_accessor] = max_associated
101
-
102
- max_associated.times do |i|
103
- fields.each do |field|
104
- header_row << csv_header_prefix_for_key(association_klass, field, i+1)
105
- end
106
- end
107
- end
108
-
109
- get_row_arr(associated, fields, max_associations[association_accessor]) {|field| row << field}
110
- end
111
-
112
- csv << header_row if csv.header_row?
113
- if row.count != header_row.count
114
- @logger.error "Encountered invalid row, skipping."
115
- end
116
- csv << row
40
+ csv_builder = RelationalExporter::CsvBuilder.new output_config.file_path
41
+ Celluloid::Actor[:csv_builder] = csv_builder
42
+ result = csv_builder.future.start
43
+ pool = RelationalExporter::RecordWorker.pool size: 8
44
+ get_headers = true
45
+
46
+ record_sequence = -1
47
+ main_klass.find_all_by_scope(output_config.output.scope.as_json).find_in_batches(batch_size: 100) do |records|
48
+ records.each do |record|
49
+ record_sequence += 1
50
+
51
+ args = [record_sequence, record, output_config.output.associations, get_headers]
52
+ if get_headers
53
+ pool.get_csv_row(*args)
54
+ get_headers = false
55
+ else
56
+ pool.async.get_csv_row(*args)
117
57
  end
118
58
  end
119
59
  end
120
- end
121
-
122
- private
123
-
124
- def csv_header_prefix_for_key(klass, key, index=nil)
125
- if klass.respond_to?(:active_model_serializer) && !klass.active_model_serializer.nil? && klass.active_model_serializer.respond_to?(:csv_header_prefix_for_key)
126
- header_prefix = klass.active_model_serializer.csv_header_prefix_for_key key.to_sym
127
- else
128
- header_prefix = klass.to_s
129
- end
130
60
 
131
- header_prefix + index.to_s + key.to_s.classify
132
- end
133
-
134
- def serialized_attributes(object)
135
- return {} if object.nil?
136
-
137
- klass, model = object.is_a?(Class) ? [object, object.first] : [object.class, object]
61
+ csv_builder.end_index = record_sequence
138
62
 
139
- return {} if model.nil?
140
-
141
- if model.respond_to?(:active_model_serializer) && !model.active_model_serializer.nil?
142
- serialized = model.active_model_serializer.new(model).as_json(root: false)
143
- end
63
+ @logger.info "CSV export complete" if result.value === true
144
64
 
145
- serialized = model.attributes if serialized.nil?
146
- serialized
65
+ pool.terminate
66
+ csv_builder.terminate
147
67
  end
148
68
 
149
- def get_row_arr(records, fields, max_count, &block)
150
- max_count.times do |i|
151
- record = records[i].nil? ? {} : serialized_attributes(records[i])
152
- fields.each do |field|
153
- val = record[field]
154
- yield val
155
- end
156
- end
157
- end
69
+ private
158
70
 
159
71
  def symbolize_options(options)
160
72
  options = options.as_json
@@ -0,0 +1,71 @@
1
+ require 'celluloid'
2
+
3
+ module RelationalExporter
4
+ class CsvBuilder
5
+ include Celluloid
6
+ include Celluloid::Logger
7
+
8
+ attr_accessor :queue, :end_index
9
+
10
+ trap_exit :actor_died
11
+ def actor_died(actor, reason)
12
+ warn "Oh no! #{actor.inspect} has died because of a #{reason.class}" unless reason.nil?
13
+ end
14
+
15
+ def initialize(file_path=nil)
16
+ @header_row = []
17
+ @index = 0
18
+ @end_index = nil
19
+ @queue = {}
20
+ @file_path = file_path
21
+ end
22
+
23
+ def start
24
+ csv_args = @file_path.blank? ? STDOUT : @file_path
25
+
26
+ csv_options = {headers: true}
27
+ if @file_path.blank?
28
+ csv_method = :instance
29
+ csv_args = [STDOUT, csv_options]
30
+ else
31
+ csv_method = :open
32
+ csv_args = [@file_path, 'wb', csv_options]
33
+ end
34
+
35
+ ::CSV.send(csv_method, *csv_args) do |csv|
36
+ until @index == @end_index
37
+ if row = @queue.delete(@index)
38
+ write_row(row, csv)
39
+ @index += 1
40
+ else
41
+ sleep 1
42
+ end
43
+ end
44
+ end
45
+
46
+ true
47
+ end
48
+
49
+ def remaining
50
+ @end_index - @index if @end_index
51
+ end
52
+
53
+ private
54
+
55
+ def write_row(row, csv)
56
+ headers, values = row.is_a?(Celluloid::Future) ? row.value : row
57
+ if csv.header_row?
58
+ @header_row = headers
59
+ info "Writing headers to file (#{headers.count})"
60
+ csv << @header_row
61
+ end
62
+ if values.count == @header_row.count
63
+ info "Writing row to file (#{@index})"
64
+ csv << values
65
+ else
66
+ # @logger.error "Encountered invalid row, skipping."
67
+ error "Bad row! #{values.count} vs #{@header_row.count}", @header_row.join(','), values.join(',')
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,130 @@
1
+ require 'celluloid'
2
+
3
+ module RelationalExporter
4
+ class RecordWorker
5
+ include Celluloid
6
+ include Celluloid::Logger
7
+
8
+ trap_exit :actor_died
9
+ def actor_died(actor, reason)
10
+ puts "Oh no! #{actor.inspect} has died because of a #{reason.class}" unless reason.nil?
11
+ end
12
+
13
+ @@MAX_ASSOCIATED = {}
14
+
15
+ def get_csv_row(record_sequence, record, associations, with_headers=false)
16
+ @record = record
17
+ @associations = associations
18
+
19
+ get_rows with_headers
20
+
21
+ Celluloid::Actor[:csv_builder].queue[record_sequence] = [@header_row, @value_row]
22
+ end
23
+
24
+ def get_rows(get_headers=false)
25
+ @header_row = []
26
+ @value_row = []
27
+ main_klass = @record.class
28
+
29
+ RecordWorker.serialized_attributes_for_object_or_class(@record).each do |field, value|
30
+ @header_row << RecordWorker.csv_header_prefix_for_key(main_klass, field) if get_headers
31
+ @value_row << value
32
+ end
33
+
34
+ @associations.each do |association_accessor, association_options|
35
+ association_accessor = association_accessor.to_s.to_sym
36
+ association_klass = association_accessor.to_s.classify.constantize
37
+ scope = RecordWorker.symbolize_options association_options.scope
38
+
39
+ associated = @record.send association_accessor
40
+ # TODO - this might suck for single associations (has_one) because they don't return an ar::associations::collectionproxy
41
+ associated = associated.find_all_by_scope(scope) unless scope.blank? || !associated.respond_to?(:find_all_by_scope)
42
+
43
+ if associated.is_a? Hash
44
+ associated = [ associated ]
45
+ elsif associated.blank?
46
+ associated = []
47
+ end
48
+
49
+ foreign_key = main_klass.reflections[association_accessor].foreign_key rescue nil
50
+
51
+ fields = RecordWorker.serialized_attributes_for_object_or_class(association_klass).keys
52
+
53
+ fields.reject! {|v| v == foreign_key } if foreign_key
54
+
55
+ if get_headers
56
+ @@MAX_ASSOCIATED[association_accessor] ||= begin
57
+ case main_klass.reflections[association_accessor].macro
58
+ when :has_many
59
+ max_associated = association_klass.find_all_by_scope(scope)
60
+ .joins(main_klass.table_name.to_sym)
61
+ .order('count_all desc')
62
+ .group(foreign_key)
63
+ .limit(1).count.flatten[1]
64
+ when :has_one
65
+ max_associated = 1
66
+ end
67
+
68
+ max_associated = 0 if max_associated.nil?
69
+ max_associated
70
+ end
71
+
72
+ @@MAX_ASSOCIATED[association_accessor].times do |i|
73
+ fields.each do |field|
74
+ @header_row << RecordWorker.csv_header_prefix_for_key(association_klass, field, i+1)
75
+ end
76
+ end
77
+ end
78
+
79
+ RecordWorker.get_maxed_row_arr(associated, fields, @@MAX_ASSOCIATED[association_accessor]) do |field|
80
+ @value_row << field
81
+ end
82
+ end
83
+ end
84
+
85
+ def self.get_maxed_row_arr(records, fields, max_count=0, &block)
86
+ return if max_count.nil?
87
+ max_count.times do |i|
88
+ record = records[i].nil? ? {} : RecordWorker.serialized_attributes_for_object_or_class(records[i])
89
+ fields.each do |field|
90
+ val = record[field]
91
+ yield val
92
+ end
93
+ end
94
+ end
95
+
96
+ def self.csv_header_prefix_for_key(klass, key, index=nil)
97
+ if klass.respond_to?(:active_model_serializer) && !klass.active_model_serializer.nil? && klass.active_model_serializer.respond_to?(:csv_header_prefix_for_key)
98
+ header_prefix = klass.active_model_serializer.csv_header_prefix_for_key key.to_sym
99
+ else
100
+ header_prefix = klass.to_s
101
+ end
102
+
103
+ header_prefix + index.to_s + key.to_s.classify
104
+ end
105
+
106
+ def self.serialized_attributes_for_object_or_class(object)
107
+ return {} if object.nil?
108
+
109
+ klass, model = object.is_a?(Class) ? [object, object.first] : [object.class, object]
110
+
111
+ return {} if model.nil?
112
+
113
+ if model.respond_to?(:active_model_serializer) && !model.active_model_serializer.nil?
114
+ serialized = model.active_model_serializer.new(model).as_json(root: false)
115
+ end
116
+
117
+ serialized = model.attributes if serialized.nil?
118
+ serialized
119
+ end
120
+
121
+ def self.symbolize_options(options)
122
+ options = options.as_json
123
+ if options.is_a? Hash
124
+ options.deep_symbolize_keys!
125
+ elsif options.is_a? Array
126
+ options.map { |val| RecordWorker.symbolize_options val }
127
+ end
128
+ end
129
+ end
130
+ end
@@ -1,3 +1,3 @@
1
1
  module RelationalExporter
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency 'hashie'
22
22
  spec.add_dependency 'activesupport'
23
+ spec.add_dependency 'celluloid'
23
24
 
24
25
  spec.add_development_dependency 'bundler', '~> 1.3'
25
26
  spec.add_development_dependency 'rake'
metadata CHANGED
@@ -1,83 +1,97 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relational_exporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hammond
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-12 00:00:00.000000000 Z
11
+ date: 2014-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: celluloid
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
- - - ~>
59
+ - - "~>"
46
60
  - !ruby/object:Gem::Version
47
61
  version: '1.3'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
- - - ~>
66
+ - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '1.3'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - '>='
73
+ - - ">="
60
74
  - !ruby/object:Gem::Version
61
75
  version: '0'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - '>='
80
+ - - ">="
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: byebug
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
- - - '>='
87
+ - - ">="
74
88
  - !ruby/object:Gem::Version
75
89
  version: '0'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
- - - '>='
94
+ - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
83
97
  description: Export relational databases as flat files
@@ -87,13 +101,15 @@ executables: []
87
101
  extensions: []
88
102
  extra_rdoc_files: []
89
103
  files:
90
- - .gitignore
104
+ - ".gitignore"
91
105
  - Gemfile
92
106
  - LICENSE.txt
93
107
  - README.md
94
108
  - Rakefile
95
109
  - lib/relational_exporter.rb
96
110
  - lib/relational_exporter/active_record_extension.rb
111
+ - lib/relational_exporter/csv_builder.rb
112
+ - lib/relational_exporter/record_worker.rb
97
113
  - lib/relational_exporter/version.rb
98
114
  - relational_exporter.gemspec
99
115
  homepage: http://github.com/andrhamm
@@ -106,19 +122,18 @@ require_paths:
106
122
  - lib
107
123
  required_ruby_version: !ruby/object:Gem::Requirement
108
124
  requirements:
109
- - - '>='
125
+ - - ">="
110
126
  - !ruby/object:Gem::Version
111
127
  version: '0'
112
128
  required_rubygems_version: !ruby/object:Gem::Requirement
113
129
  requirements:
114
- - - '>='
130
+ - - ">="
115
131
  - !ruby/object:Gem::Version
116
132
  version: '0'
117
133
  requirements: []
118
134
  rubyforge_project:
119
- rubygems_version: 2.0.0
135
+ rubygems_version: 2.2.0
120
136
  signing_key:
121
137
  specification_version: 4
122
138
  summary: Export relational databases as flat files
123
139
  test_files: []
124
- has_rdoc: