relational_exporter 0.0.1

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
+ SHA1:
3
+ metadata.gz: 812375c407e516d9335f9de5ef56b1472bd67e2a
4
+ data.tar.gz: 05079ba3387f1a94910da2d2e9338883156e3bbf
5
+ SHA512:
6
+ metadata.gz: bec4595a08715fb900cf95e6c3b7b647b8b5a3e996068e75aa9f19d39d06c00857a1af22480c45f230e103e892efadaba06349d8147872b2b5af7fe43a0cbdba
7
+ data.tar.gz: 70b0fe2eebfc33cfef4d5ceb2096a20a0dd7f03af68e9f51a117228a886940d05bd27ff5f536c1e2971f9337d2d5e3e072e46cb52ffcf5fbb86bcd67b3d57940
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in relational_exporter.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Andrew Hammond
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # relational_exporter
2
+
3
+ A gem to make it easy to export data from relational databases. RelationalExporter shines when your intended output is a "flat" CSV file, but your data is relational (each record can have multiple associated sub-records). Define your schema (once) in a familiar ActiveRecord-y way and leverage the robust model featureset. Once your schema is defined, define one or more output configurations which can be re-used to generate an export file.
4
+
5
+ ## TODO
6
+
7
+ * Support multiple formats (currently only CSV)
8
+ * Integrate serializers of some sort
9
+ * Improve DSL
10
+ * Clean up the code
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'relational_exporter'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install relational_exporter
25
+
26
+ ## Usage
27
+
28
+ ```ruby
29
+ # Define schemas
30
+ my_schema = {
31
+ person: {
32
+ table_name: :person,
33
+ primary_key: :id,
34
+ has_many: {
35
+ addresses: nil,
36
+ emails: nil
37
+ },
38
+ has_one: {
39
+ avatar: [{ foreign_key: :ref_id }]
40
+ }
41
+ },
42
+ avatar: {
43
+ table_name: :avatar,
44
+ belongs_to: {
45
+ person: [{ foreign_key: :ref_id }]
46
+ }
47
+ }
48
+ }
49
+
50
+ # Define output
51
+ my_output_config = {
52
+ format: :csv,
53
+ output: {
54
+ model: :person,
55
+ scope: {
56
+ where: "person.status = 'active'"
57
+ },
58
+ associations: {
59
+ emails: {},
60
+ avatar: {
61
+ scope: {
62
+ where: "ref_type like 'person'",
63
+ limit: 1,
64
+ order: "id desc"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ # Define DB connection info
72
+ my_conn = {
73
+ adapter: 'mysql2',
74
+ host: ENV['DB_HOST'],
75
+ username: ENV['DB_USER'],
76
+ password: ENV['DB_PASS'],
77
+ database: 'my_database'
78
+ }
79
+
80
+ # Run the export!
81
+ r = RelationalExporter::Runner.new schema: my_schema, connection_config: my_conn
82
+ r.export(my_output_config) do |record|
83
+ # modify the record and it's models/associations
84
+ end
85
+ ```
86
+
87
+ ## Contributing
88
+
89
+ 1. Fork it
90
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
91
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
92
+ 4. Push to the branch (`git push origin my-new-feature`)
93
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,38 @@
1
+ module ActiveRecordExtension
2
+ extend ActiveSupport::Concern
3
+
4
+ def find_all_by_scope(scope_hash={})
5
+ scope_hash = { where: {} } if scope_hash.nil?
6
+ the_scope = nil
7
+ scope_hash.each do |method, scoping|
8
+ if the_scope.nil?
9
+ the_scope = send method.to_sym, scoping
10
+ else
11
+ the_scope.send method.to_sym, scoping
12
+ end
13
+ end
14
+ the_scope
15
+ end
16
+
17
+ def set_scope_from_hash(scope_hash={}, clear_default_scope=false)
18
+ scope_hash = {} if scope_hash.nil?
19
+ clear_default_scopes if clear_default_scope
20
+
21
+ result = nil
22
+ scope_hash.each do |method, scoping|
23
+ if result.nil?
24
+ result = send method.to_sym, scoping
25
+ else
26
+ result.send method.to_sym, scoping
27
+ end
28
+ end
29
+
30
+ default_scope { result }
31
+ end
32
+
33
+ def clear_default_scopes
34
+ self.default_scopes = []
35
+ end
36
+ end
37
+
38
+ ActiveRecord::Base.send(:extend, ActiveRecordExtension)
@@ -0,0 +1,3 @@
1
+ module RelationalExporter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,147 @@
1
+ require 'byebug'
2
+ require 'csv'
3
+ require 'hashie'
4
+ require 'relational_exporter/version'
5
+ require 'relational_exporter/active_record_extension'
6
+
7
+ module RelationalExporter
8
+ class Runner
9
+ attr_accessor :schema
10
+
11
+ def initialize(options={})
12
+ # TODO - disable when not byebugging!!!
13
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
14
+
15
+ @connection_config = options[:connection_config]
16
+ begin
17
+ ActiveRecord::Base.establish_connection @connection_config
18
+ ActiveRecord::Base.connection.active?
19
+ rescue Exception => e
20
+ raise "Database connection failed: #{e.message}"
21
+ end
22
+
23
+ @schema = Hashie::Mash.new(options[:schema] || YAML.load_file(options[:schema_file]))
24
+
25
+ load_models
26
+ end
27
+
28
+ def export(output_config, &block)
29
+ output_config = Hashie::Mash.new output_config
30
+
31
+ main_klass = output_config.output.model.to_s.classify.constantize
32
+
33
+ main_klass.set_scope_from_hash output_config.output.scope.as_json
34
+
35
+ header_row = []
36
+ max_associations = {}
37
+
38
+ ::CSV.open('/tmp/test.csv', 'wb', headers: true) do |csv|
39
+ main_klass.all.find_in_batches do |batch|
40
+ batch.each do |single|
41
+ if block_given?
42
+ yield single
43
+ end
44
+
45
+ row = []
46
+
47
+ # Add main record headers
48
+ single.attributes.each do |field, value|
49
+ header_row << [main_klass.to_s.underscore, field].join('_').classify if csv.header_row?
50
+ row << value
51
+ end
52
+
53
+ output_config.output.associations.each do |association_accessor, association_options|
54
+ association_accessor = association_accessor.to_s.to_sym
55
+ association_klass = association_accessor.to_s.classify.constantize
56
+ scope = symbolize_options association_options.scope
57
+
58
+ associated = single.send association_accessor
59
+ # TODO - this might suck for single associations (has_one) because they don't return an ar::associations::collectionproxy
60
+ associated = associated.find_all_by_scope(scope) unless scope.blank? || !associated.respond_to?(:find_all_by_scope)
61
+
62
+ if associated.is_a? Hash
63
+ associated = [ associated ]
64
+ elsif associated.blank?
65
+ associated = []
66
+ end
67
+
68
+ foreign_key = main_klass.reflections[association_accessor].foreign_key rescue nil
69
+
70
+ fields = association_klass.first.attributes.keys
71
+
72
+ fields.reject! {|v| v == foreign_key } if foreign_key
73
+
74
+ if csv.header_row?
75
+ case main_klass.reflections[association_accessor].macro
76
+ when :has_many
77
+ max_associated = association_klass.find_all_by_scope(scope)
78
+ .joins(main_klass.table_name.to_sym)
79
+ .order('count_all desc')
80
+ .group(foreign_key)
81
+ .limit(1).count.flatten[1]
82
+ when :has_one
83
+ max_associated = 1
84
+ end
85
+
86
+ max_associations[association_accessor] = max_associated
87
+
88
+ max_associated.times do |i|
89
+ fields.each do |field|
90
+ header_row << [association_klass.to_s.underscore, i+1, field].join('_').classify
91
+ end
92
+ end
93
+ end
94
+
95
+ get_row_arr(associated, fields, max_associations[association_accessor]) {|field| row << field}
96
+ end
97
+
98
+ csv << header_row if csv.header_row?
99
+ if row.count != header_row.count
100
+ puts "OH SHIT, this row is not right!"
101
+ byebug
102
+ end
103
+ csv << row
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def get_row_arr(records, fields, max_count=1, &block)
112
+ max_count.times do |i|
113
+ fields.each do |field|
114
+ val = records[i][field] rescue nil
115
+ yield val
116
+ end
117
+ end
118
+ end
119
+
120
+ def symbolize_options(options)
121
+ options = options.as_json
122
+ if options.is_a? Hash
123
+ options.deep_symbolize_keys!
124
+ elsif options.is_a? Array
125
+ options.map { |val| symbolize_options val }
126
+ end
127
+ end
128
+
129
+ def load_models
130
+ @schema.each do |model, options|
131
+ klass = Object.const_set model.to_s.classify, Class.new(ActiveRecord::Base)
132
+ # klass.extend ActiveRecordExtension
133
+ options.each do |method, calls|
134
+ method = "#{method}=".to_sym if klass.respond_to?("#{method}=")
135
+ if calls.respond_to? :each_pair
136
+ calls.each do |association, association_options|
137
+ association_options = symbolize_options association_options
138
+ klass.send method, association.to_sym, *association_options
139
+ end
140
+ else
141
+ klass.send method, *calls
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'relational_exporter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'relational_exporter'
8
+ spec.version = RelationalExporter::VERSION
9
+ spec.authors = ['Andrew Hammond']
10
+ spec.email = ['andrew@tremorlab.com']
11
+ spec.description = %q{Export relational databases as flat files}
12
+ spec.summary = %q{Export relational databases as flat files}
13
+ spec.homepage = 'http://github.com/andrhamm'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'hashie'
22
+ spec.add_dependency 'activesupport'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.3'
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_development_dependency 'byebug'
27
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: relational_exporter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Hammond
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hashie
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Export relational databases as flat files
84
+ email:
85
+ - andrew@tremorlab.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .gitignore
91
+ - Gemfile
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - lib/relational_exporter.rb
96
+ - lib/relational_exporter/active_record_extension.rb
97
+ - lib/relational_exporter/version.rb
98
+ - relational_exporter.gemspec
99
+ homepage: http://github.com/andrhamm
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.0.0
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Export relational databases as flat files
123
+ test_files: []
124
+ has_rdoc: