sp-squealer 1.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.
@@ -0,0 +1,7 @@
1
+ class Hash
2
+ def method_missing(name, *args, &block)
3
+ super if args.size > 0 || block_given?
4
+ #TODO: Warn if key doesn't exist - it's probably a typo in their squealer script
5
+ self[name.to_s]
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ class Object
2
+
3
+ def target(table_name, &block)
4
+ Squealer::Target.new(Squealer::Database.instance.export, table_name, &block)
5
+ end
6
+
7
+ def assign(column_name, &block)
8
+ Squealer::Target.current.assign(column_name, &block)
9
+ end
10
+
11
+ def import(*args)
12
+ if args.length > 0
13
+ Squealer::Database.instance.import_from(*args)
14
+ else
15
+ Squealer::Database.instance.import
16
+ end
17
+ end
18
+
19
+ def export(*args)
20
+ if args.length > 0
21
+ Squealer::Database.instance.export_to(*args)
22
+ else
23
+ Squealer::Database.instance.export
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ class NilClass
30
+ include Enumerable
31
+ def each
32
+ []
33
+ end
34
+ end
@@ -0,0 +1,122 @@
1
+ module Squealer
2
+ class ProgressBar
3
+
4
+ @@progress_bar = nil
5
+
6
+ def self.new(*args)
7
+ if @@progress_bar
8
+ nil
9
+ else
10
+ @@progress_bar = super
11
+ end
12
+ end
13
+
14
+ def initialize(total)
15
+ @total = total
16
+ @ticks = 0
17
+
18
+ @progress_bar_width = 50
19
+ @count_width = total.to_s.size
20
+ end
21
+
22
+ def start
23
+ @start_time = clock
24
+ @emitter = start_emitter if total > 0
25
+ self
26
+ end
27
+
28
+ def finish
29
+ @end_time = clock
30
+ @emitter.wakeup.join if @emitter
31
+ @@progress_bar = nil
32
+ end
33
+
34
+ def tick
35
+ @ticks += 1
36
+ end
37
+
38
+ private
39
+
40
+ def start_emitter
41
+ Thread.new do
42
+ emit
43
+ sleep(1) and emit until done?
44
+ end
45
+ end
46
+
47
+ def emit
48
+ format = "\r[%-#{progress_bar_width}s] %#{count_width}i/%i (%i%%) [%i/s]"
49
+ console.print format % [progress_markers, ticks, total, percentage, tps]
50
+ emit_final if done?
51
+ end
52
+
53
+ def emit_final
54
+ console.puts
55
+
56
+ console.puts "Start: #{start_time}"
57
+ console.puts "End: #{end_time}"
58
+ console.puts "Duration: #{duration}"
59
+ end
60
+
61
+ def clock
62
+ Time.now
63
+ end
64
+
65
+ def done?
66
+ ticks >= total || end_time
67
+ end
68
+
69
+ def start_time
70
+ @start_time
71
+ end
72
+
73
+ def end_time
74
+ @end_time
75
+ end
76
+
77
+ def ticks
78
+ @ticks
79
+ end
80
+
81
+ def tps
82
+ elapsed_secs = (clock - start_time) / 60
83
+ (ticks / elapsed_secs).ceil
84
+ rescue
85
+ 0
86
+ end
87
+
88
+ def total
89
+ @total
90
+ end
91
+
92
+ def percentage
93
+ ((ticks.to_f / total) * 100).floor
94
+ end
95
+
96
+ def progress_markers
97
+ "=" * ((ticks.to_f / total) * progress_bar_width).floor
98
+ end
99
+
100
+ def console
101
+ $stderr
102
+ end
103
+
104
+ def progress_bar_width
105
+ @progress_bar_width
106
+ end
107
+
108
+ def count_width
109
+ @count_width
110
+ end
111
+
112
+ def total_time
113
+ @end_time - @start_time
114
+ end
115
+
116
+ def duration
117
+ duration = Time.at(total_time).utc
118
+ duration.strftime("%H:%M:%S.#{duration.usec}")
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,177 @@
1
+ require 'delegate'
2
+ require 'singleton'
3
+
4
+ #TODO: Use logger and log throughout
5
+
6
+ module Squealer
7
+ class Target
8
+
9
+ def self.current
10
+ Queue.instance.current
11
+ end
12
+
13
+ def initialize(database_connection, table_name, &block)
14
+ raise BlockRequired, "Block must be given to target (otherwise, there's no work to do)" unless block_given?
15
+ raise ArgumentError, "Table name must be supplied" if table_name.to_s.strip.empty?
16
+
17
+ @dbc = database_connection
18
+ @table_name = table_name.to_s
19
+ @binding = block.binding
20
+
21
+ verify_table_name_in_scope
22
+ @row_id = infer_row_id
23
+ @column_names = []
24
+ @column_values = []
25
+ @sql = ''
26
+
27
+ target(&block)
28
+ end
29
+
30
+ def sql
31
+ @sql
32
+ end
33
+
34
+ def assign(column_name, &block)
35
+ @column_names << column_name
36
+ if block_given?
37
+ @column_values << yield
38
+ else
39
+ @column_values << infer_value(column_name, @binding)
40
+ end
41
+ end
42
+
43
+
44
+ private
45
+
46
+ def infer_row_id
47
+ (
48
+ (eval "#{@table_name}[:_id]", @binding, __FILE__, __LINE__) ||
49
+ (eval "#{@table_name}['_id']", @binding, __FILE__, __LINE__)
50
+ ).to_s
51
+ end
52
+
53
+ def verify_table_name_in_scope
54
+ table = eval "#{@table_name}", @binding, __FILE__, __LINE__
55
+ raise ArgumentError, "The variable '#{@table_name}' is not a hashmap" unless table.is_a? Hash
56
+ raise ArgumentError, "The hashmap '#{@table_name}' must have an '_id' key" unless table.has_key?('_id') || table.has_key?(:_id)
57
+ rescue NameError
58
+ raise NameError, "A variable named '#{@table_name}' must be in scope, and reference a hashmap with at least an '_id' key."
59
+ end
60
+
61
+
62
+ def infer_value(column_name, binding)
63
+ value = eval "#{@table_name}.#{column_name}", binding, __FILE__, __LINE__
64
+ unless value
65
+ name = column_name.to_s
66
+ if name =~ /_id$/
67
+ related = name[0..-4] #strip "_id"
68
+ value = eval "#{related}._id", binding, __FILE__, __LINE__
69
+ end
70
+ end
71
+ value
72
+ end
73
+
74
+ def target
75
+ Queue.instance.push(self)
76
+
77
+ yield self
78
+
79
+ insert_statement = %{INSERT INTO "#{@table_name}"}
80
+ insert_statement << %{ (#{pk_name}#{column_names}) VALUES ('#{@row_id}'#{column_value_markers})}
81
+ if Database.instance.upsertable?
82
+ insert_statement << %{ ON DUPLICATE KEY UPDATE #{column_markers}}
83
+ @sql = insert_statement
84
+ else
85
+ update_statement = %{UPDATE "#{@table_name}" SET #{column_markers} WHERE #{pk_name}='#{@row_id}'}
86
+ process_sql(update_statement)
87
+ @sql = update_statement + "; " + insert_statement
88
+ end
89
+
90
+ process_sql(insert_statement)
91
+
92
+ Queue.instance.pop
93
+ end
94
+
95
+ def self.targets
96
+ @@targets
97
+ end
98
+
99
+ def targets
100
+ @@targets
101
+ end
102
+
103
+ def process_sql(sql)
104
+ values = Database.instance.upsertable? ? typecast_values * 2 : typecast_values
105
+ execute_sql(sql, values)
106
+ end
107
+
108
+ def execute_sql(sql, values)
109
+ @dbc.create_command(sql).execute_non_query(*values)
110
+ rescue DataObjects::IntegrityError
111
+ raise "Failed to execute statement: #{sql} with #{values.inspect}.\nOriginal Exception was: #{$!.to_s}" if Database.instance.upsertable?
112
+ rescue
113
+ raise "Failed to execute statement: #{sql} with #{values.inspect}.\nOriginal Exception was: #{$!.to_s}"
114
+ end
115
+
116
+ def pk_name
117
+ 'id'
118
+ end
119
+
120
+ def column_names
121
+ return if @column_names.size == 0
122
+ ",#{@column_names.map { |name| quote_identifier(name) }.join(',')}"
123
+ end
124
+
125
+ def column_values
126
+ @column_values
127
+ end
128
+
129
+ def column_value_markers
130
+ return if @column_names.size == 0
131
+ result = ""
132
+ @column_names.size.times { result << ',?'}
133
+ result
134
+ end
135
+
136
+ def column_markers
137
+ return if @column_names.size == 0
138
+ result = ""
139
+ @column_names.each {|k| result << "#{quote_identifier(k)}=?," }
140
+ result.chop
141
+ end
142
+
143
+ def typecast_values
144
+ column_values.map do |value|
145
+ case value
146
+ when Array
147
+ value.join(",")
148
+ when BSON::ObjectId
149
+ value.to_s
150
+ else
151
+ value
152
+ end
153
+ end
154
+ end
155
+
156
+ def quote_identifier(name)
157
+ %{"#{name}"}
158
+ end
159
+
160
+ class Queue < DelegateClass(Array)
161
+ include Singleton
162
+
163
+ def current
164
+ last
165
+ end
166
+
167
+ protected
168
+
169
+ def initialize
170
+ super([])
171
+ end
172
+ end
173
+
174
+ class BlockRequired < ArgumentError; end
175
+
176
+ end
177
+ end
@@ -0,0 +1,15 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "squealer"
5
+ gemspec.summary = "Document-oriented to Relational database exporter"
6
+ gemspec.description = "Exports mongodb to mysql. More later."
7
+ gemspec.email = "joshua.graham@grahamis.com"
8
+ gemspec.homepage = "http://github.com/deltiscere/squealer/"
9
+ gemspec.authors = ["Josh Graham", "Durran Jordan"]
10
+ gem.add_dependency('mysql', '>= 2.8.1')
11
+ gem.add_dependency('mongo', '>= 0.18.3')
12
+ end
13
+ rescue LoadError
14
+ puts "Jeweler not available. Install it with: gem install jeweler"
15
+ end
@@ -0,0 +1,136 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/./spec_helper_dbms')
2
+
3
+ describe "Exporting" do
4
+ let(:databases) { Squealer::Database.instance }
5
+ let(:today) { Date.today }
6
+
7
+ def squeal_basic_users_document(user=users_document)
8
+ target(:user) do
9
+ assign(:name)
10
+ assign(:organization_id)
11
+ assign(:dob)
12
+ assign(:gender)
13
+ assign(:foreign)
14
+ assign(:dull)
15
+ assign(:symbolic)
16
+ assign(:interests)
17
+ end
18
+ end
19
+
20
+ def squeal_users_document_with_activities(user=users_document)
21
+ target(:user) do
22
+ assign(:name)
23
+ assign(:organization_id)
24
+ assign(:dob)
25
+ assign(:gender)
26
+ assign(:foreign)
27
+ assign(:dull)
28
+ assign(:symbolic)
29
+ assign(:interests)
30
+
31
+ user.activities.each do |activity|
32
+ target(:activity) do
33
+ assign(:name)
34
+ assign(:due_date)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ let :users_document do
41
+ { :_id => 'ABCDEFGHIJKLMNOPQRSTUVWX',
42
+ 'name' => 'Test User', 'dob' => as_time(Date.parse('04-Jul-1776')), 'gender' => 'M',
43
+ 'foreign' => true,
44
+ 'dull' => false,
45
+ 'symbolic' => :of_course,
46
+ 'interests' => ['health', 'education'],
47
+ 'organization_id' => '123456789012345678901234',
48
+ 'activities' => [
49
+ { :_id => 'a1', 'name' => 'Be independent', 'due_date' => as_time(today + 1) },
50
+ { :_id => 'a2', 'name' => 'Fight each other', 'due_date' => as_time(today + 7) }
51
+ ]
52
+ }
53
+ end
54
+
55
+ let :first_users_record do
56
+ dbc = databases.export
57
+ reader = dbc.create_command(%{SELECT * FROM "user"}).execute_reader
58
+ result = reader.each { |x| break x }
59
+ reader.close
60
+ result
61
+ end
62
+
63
+ let :first_activity_record do
64
+ dbc = databases.export
65
+ reader = dbc.create_command(%{SELECT * FROM "activity"}).execute_reader
66
+ result = reader.each { |x| break x }
67
+ reader.close
68
+ result
69
+ end
70
+
71
+ context "a new record" do
72
+ it "saves the data correctly" do
73
+ squeal_basic_users_document
74
+ result = first_users_record
75
+
76
+ result['name'].should == 'Test User'
77
+
78
+ result['dob'].mday.should == 4
79
+ result['dob'].mon.should == 7
80
+ result['dob'].year.should == 1776
81
+
82
+ result['gender'].should == 'M'
83
+
84
+ result['foreign'].should be_true
85
+ result['dull'].should be_false
86
+
87
+ result['symbolic'].should == :of_course.to_s
88
+
89
+ result['interests'].should == 'health,education'
90
+ end
91
+
92
+ it "saves embedded documents correctly" do
93
+ squeal_users_document_with_activities
94
+ result = first_activity_record
95
+
96
+ result['name'].should == 'Be independent'
97
+ result['due_date'].mday.should == (today + 1).mday
98
+ result['due_date'].mon.should == (today + 1).mon
99
+ result['due_date'].year.should == (today + 1).year
100
+ end
101
+ end
102
+
103
+ context "an existing record" do
104
+ it "updates the data correctly" do
105
+ squeal_basic_users_document
106
+ squeal_basic_users_document(users_document.merge('foreign' => false, 'gender' => 'F'))
107
+
108
+ result = first_users_record
109
+
110
+ result['name'].should == 'Test User'
111
+
112
+ result['dob'].mday.should == 4
113
+ result['dob'].mon.should == 7
114
+ result['dob'].year.should == 1776
115
+
116
+ result['gender'].should == 'F'
117
+
118
+ result['foreign'].should be_false
119
+ result['dull'].should be_false
120
+
121
+ result['symbolic'].should == :of_course.to_s
122
+
123
+ result['interests'].should == 'health,education'
124
+ end
125
+
126
+ it "updates the child record correctly" do
127
+ squeal_users_document_with_activities(users_document.merge('activities' => [{ :_id => 'a1', 'name' => 'Be expansionist', 'due_date' => as_time(today + 1) }]))
128
+ result = first_activity_record
129
+
130
+ result['name'].should == 'Be expansionist'
131
+ result['due_date'].mday.should == (today + 1).mday
132
+ result['due_date'].mon.should == (today + 1).mon
133
+ result['due_date'].year.should == (today + 1).year
134
+ end
135
+ end
136
+ end