squealer 1.2.0 → 2.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.
data/.rvmrc CHANGED
@@ -1,3 +1,6 @@
1
1
  # These are the rubies that squealer has been tested on. Uncomment the one you want to use right now.
2
- rvm use 1.9.1@squealer
3
- # rvm use 1.8.7@squealer
2
+ #rvm use 1.9.1@squealer
3
+ #rvm use 1.8.7@squealer
4
+ rvm use 1.9.2
5
+ #rvm use 1.9.1
6
+ #rvm use 1.8.7
data/README.md CHANGED
@@ -10,6 +10,20 @@ To run standalone, simply make your data squeal thusly:
10
10
  where the squeal script includes a `require 'squealer'`.
11
11
 
12
12
  ## Release Notes
13
+ ### v2.1
14
+ * Ruby 1.8.6 back-compatibility added. Using `eval "", binding, __FILE__, __LINE__` instead of `binding.eval`
15
+ * Target SQL script using backtick-quoted (mySQL) identifiers to avoid column-name / keyword conflict
16
+ * Automatically typecast Ruby `Boolean` (to integer), `Symbol` (to string), `Array` (to comma-seperated string)
17
+ * Improved handling and reporting of Target SQL errors
18
+ * Schaefer's Special "skewer" Script to reflect on Mongoid models and generate an initial squeal script and SQL schema DDL script. This tool is intended to build the _initial_ scripts only. It is extremely useful to get you going, but do think about the needs of the consumer of the export database, and adjust the scripts to suit. [How do you make something squeal? You skewer it!]
19
+
20
+ ### v2.0
21
+ * `Object#import` now wraps a MongoDB cursor to provide counters and timings. Only `each` is supported for now, however `source` takes optional conditions.
22
+ * Progress bar and summary.
23
+
24
+ ### v1.2.1
25
+ * `Object#import` syntax has changed. Now `import.source(collection).each` rather than `import.collection(collection).find({}).each`. `source` returns a MongoDB cursor like `find` does. See lib/example_squeal.rb for options.
26
+
13
27
  ### v1.2
14
28
  * `Object#target` verifies there is a variable in scope with the same name as the `table_name` being targetted, it must be a `Hash` and must have an `_id` key
15
29
  * Block to `Object#assign` not required, infers value from source scope
data/Rakefile CHANGED
@@ -13,9 +13,14 @@ begin
13
13
  gemspec.description = "Exports mongodb to mysql. More later."
14
14
  gemspec.email = "joshua.graham@grahamis.com"
15
15
  gemspec.homepage = "http://github.com/delitescere/squealer/"
16
- gemspec.authors = ["Josh Graham", "Durran Jordan"]
16
+ gemspec.authors = ["Josh Graham", "Durran Jordan", "Matt Yoho", "Bernerd Schaefer"]
17
+
18
+ gemspec.default_executable = "skewer"
19
+ gemspec.executables = ["skewer"]
20
+
17
21
  gemspec.add_dependency('mysql', '>= 2.8.1')
18
22
  gemspec.add_dependency('mongo', '>= 0.18.3')
23
+ gemspec.add_dependency('bson_ext', '>= 1.0.1')
19
24
  end
20
25
  Jeweler::GemcutterTasks.new
21
26
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 2.1.0
data/bin/skewer ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ model = ARGV[0]
4
+ unless model
5
+ $stderr.puts "usage: skewer ModelName"
6
+ exit 1
7
+ end
8
+
9
+ require 'config/boot'
10
+ require 'config/environment'
11
+
12
+ model = Object.const_get(model)
13
+
14
+ unless defined?(Mongoid) and Mongoid::Document > model
15
+ $stderr.puts "#{model} must be a Mongoid::Document"
16
+ exit 1
17
+ end
18
+
19
+ write_schema = true
20
+ write_squeal = true
21
+
22
+ schema_filename = "#{model.name.underscore}_schema.sql"
23
+ squeal_filename = "#{model.name.underscore}_squeal.rb"
24
+
25
+ schema_exists = File.exists?(schema_filename)
26
+ squeal_exists = File.exists?(squeal_filename)
27
+
28
+ if schema_exists || squeal_exists
29
+ $stdout.print "#{schema_filename} already exists, overwrite? [Y/n] "
30
+ write_schema = $stdin.gets.chomp != "n"
31
+
32
+ $stdout.print "#{squeal_filename} already exists, overwrite? [Y/n] "
33
+ write_squeal = $stdin.gets.chomp != "n"
34
+ end
35
+
36
+ fields = model.fields.values.sort_by(&:name)
37
+ associations = model.associations
38
+
39
+ # SQL #
40
+ def create_table(model, parent = nil)
41
+ fields = model.fields.values.sort_by(&:name)
42
+ columns = []
43
+ columns << "`#{parent.name.underscore}_id` CHAR(24)" if parent
44
+
45
+ fields.each do |field|
46
+ mysql_type = case field.type.name
47
+ when "Boolean"
48
+ "BOOLEAN"
49
+ when "Time"
50
+ "TIMESTAMP NULL DEFAULT NULL"
51
+ when "Date"
52
+ "DATE"
53
+ when "Float"
54
+ "FLOAT"
55
+ when "Integer"
56
+ "INT"
57
+ else
58
+ "TEXT"
59
+ end
60
+ columns << "`#{field.name[0..63]}` #{mysql_type}"
61
+ end
62
+
63
+ table_name = if parent
64
+ "#{parent.name.underscore}_#{model.name.underscore}"
65
+ else
66
+ "#{model.name.underscore}"
67
+ end
68
+
69
+ table_sql = []
70
+ table_sql << "DROP TABLE IF EXISTS `#{table_name}`;"
71
+ table_sql << "CREATE TABLE `#{table_name}` (`id` CHAR(24) PRIMARY KEY);"
72
+ columns.each do |column|
73
+ table_sql << "ALTER TABLE `#{table_name}` ADD COLUMN #{column};"
74
+ end
75
+
76
+ table_sql.join("\n") + "\n"
77
+ end
78
+
79
+ # SQUEAL #
80
+ def create_squeal(model, indent=false, parents = [])
81
+ fields = model.fields.values.sort_by(&:name)
82
+
83
+ parent = parents.last
84
+ table_name = if parent
85
+ "#{parent.name.underscore}_#{model.name.underscore}"
86
+ else
87
+ "#{model.name.underscore}"
88
+ end
89
+
90
+ squeal = if parent
91
+ "#{parent.name.underscore}.#{model.name.tableize}.each do |#{model.name.underscore}|\n" \
92
+ " #{table_name} = #{model.name.underscore}"
93
+ else
94
+ "import.source(\"#{model.name.tableize}\").each do |#{model.name.underscore}|"
95
+ end
96
+
97
+ schemas = [create_table(model, parent)]
98
+
99
+ squeal << <<-EOS
100
+
101
+ target(:#{table_name}) do
102
+ EOS
103
+ if parent
104
+ squeal << <<-EOS
105
+ assign(:#{parent.name.underscore}_id)
106
+ EOS
107
+ end
108
+
109
+ fields.each do |field|
110
+ field_name = field.name
111
+ case
112
+ when %w(type target).include?(field.name)
113
+ value = " { #{table_name}['#{field.name}'] }"
114
+ when field.name.size > 64
115
+ field_name = field.name[0..63]
116
+ value = " { #{table_name}.#{field.name} }"
117
+ when field.name =~ /(.*)_id$/
118
+ value = " { #{table_name}.#{$1} }"
119
+ end
120
+ squeal << <<-EOS
121
+ assign(:#{field_name})#{value}
122
+ EOS
123
+ end
124
+
125
+ model.associations.values.each do |association|
126
+ begin
127
+ if [Mongoid::Associations::HasMany, Mongoid::Associations::HasOne].include?(association.association)
128
+ unless parents.include?(association.klass)
129
+ ruby, sql = create_squeal(association.klass, true, parents | [model])
130
+ squeal << "\n" + ruby + "\n"
131
+ schemas |= sql
132
+ end
133
+ end
134
+ rescue NameError
135
+ end
136
+ end
137
+
138
+ squeal << <<-EOS
139
+ end
140
+ end # #{table_name}
141
+ EOS
142
+ squeal.gsub!(/^/, " ") if indent
143
+ return squeal, schemas
144
+ end
145
+
146
+ squeal, schema = create_squeal(model)
147
+
148
+ if write_squeal
149
+ File.open(squeal_filename, "w") do |file|
150
+ file.write <<-EOS
151
+ require 'squealer'
152
+
153
+ import('localhost', 27017, 'development') # <--- Change this as needed
154
+ export('localhost', 'root', '', 'export') # <--- Change this as needed
155
+
156
+ EOS
157
+ file.write(squeal)
158
+ end
159
+ end
160
+
161
+ if write_schema
162
+ File.open(schema_filename, "w") do |file|
163
+ file.write(schema.join("\n"))
164
+ end
165
+ end
Binary file
@@ -7,7 +7,16 @@ import('localhost', 27017, 'development')
7
7
  export('localhost', 'root', '', 'reporting_export')
8
8
 
9
9
  # Here we extract, transform and load all documents in a collection...
10
- import.collection("users").find({}).each do |user|
10
+
11
+ #
12
+ # You don't want to use a find() on the MongoDB collection...
13
+ # import.source("users") { |users| users.find_one() }.each do |user|
14
+ #
15
+ # Also accepts optional conditions...
16
+ # import.source("users", "{disabled: 'false'}").each do |user|
17
+ #
18
+ # Defaults to find all...
19
+ import.source('users').each do |user|
11
20
  # Insert or Update on table 'user' where 'id' is the column name of the primary key.
12
21
  #
13
22
  # The primary key value is taken from the '_id' field of the source document,
@@ -54,8 +63,7 @@ import.collection("users").find({}).each do |user|
54
63
  #
55
64
  # You can use an empty block to infer the value from the '_id' field
56
65
  # of a parent document where the name of the parent collection matches
57
- # a variable that is in scope.
58
- #
66
+ # a variable that is in scope...
59
67
  assign(:user_id) #or# assign(:user_id) { user._id }
60
68
  assign(:name) #or# assign(:name) { activity.name }
61
69
  assign(:due_date) #or# assign(:due_date) { activity.due_date }
@@ -73,8 +81,8 @@ import.collection("users").find({}).each do |user|
73
81
  end #collection("users")
74
82
 
75
83
  # Here we use a procedural "join" on related collections to update a target...
76
- import.collection("organization").find({'disabled_date' : { 'exists' : 'true' }}).each do |organization|
77
- import.collection("users").find({ :organization_id => organization.id }) do |user|
84
+ import.source('organizations', {'disabled_date' => {'exists' => true}}).each do |organization|
85
+ import.source('users', {'organization_id' => organization.id}) do |user|
78
86
  target(:user) do
79
87
  #
80
88
  # Source boolean values are converted to integer (0 or 1)...
@@ -8,6 +8,7 @@ module Squealer
8
8
 
9
9
  def import_from(host, port, name)
10
10
  @import_dbc = Mongo::Connection.new(host, port, :slave_ok => true).db(name)
11
+ @import_connection = Connection.new(@import_dbc)
11
12
  end
12
13
 
13
14
  def export_to(host, username, password, name)
@@ -15,12 +16,57 @@ module Squealer
15
16
  end
16
17
 
17
18
  def import
18
- @import_dbc
19
+ @import_connection
19
20
  end
20
21
 
21
22
  def export
22
23
  @export_dbc
23
24
  end
24
25
 
26
+ class Connection
27
+ attr_reader :collections
28
+
29
+ def initialize(dbc)
30
+ @dbc = dbc
31
+ @collections = {}
32
+ end
33
+
34
+ def source(collection, conditions = {}, &block)
35
+ source = Source.new(@dbc, collection)
36
+ @collections[collection] = source
37
+ source.source(conditions, &block)
38
+ end
39
+
40
+ def eval(string)
41
+ @dbc.eval(string)
42
+ end
43
+ end
44
+
45
+ class Source
46
+ attr_reader :counts, :cursor
47
+
48
+ def initialize(dbc, collection)
49
+ @counts = {:exported => 0, :imported => 0}
50
+ @collection = dbc.collection(collection)
51
+ end
52
+
53
+ def source(conditions)
54
+ @cursor = block_given? ? yield(@collection) : @collection.find(conditions)
55
+ @counts[:total] = cursor.count
56
+ @progress_bar = Squealer::ProgressBar.new(cursor.count)
57
+ self
58
+ end
59
+
60
+ def each
61
+ @progress_bar.start if @progress_bar
62
+ @cursor.each do |row|
63
+ @counts[:imported] += 1
64
+ yield row
65
+ @progress_bar.tick if @progress_bar
66
+ @counts[:exported] += 1
67
+ end
68
+ @progress_bar.finish if @progress_bar
69
+ end
70
+ end
25
71
  end
26
72
  end
@@ -0,0 +1,112 @@
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
+
21
+ end
22
+
23
+ def start
24
+ @start_time = Time.new
25
+ @emitter = start_emitter if total > 0
26
+ self
27
+ end
28
+
29
+ def finish
30
+ @end_time = Time.new
31
+ @emitter.wakeup.join if @emitter
32
+ @@progress_bar = nil
33
+ end
34
+
35
+ def tick
36
+ @ticks += 1
37
+ end
38
+
39
+ private
40
+
41
+ def start_emitter
42
+ Thread.new do
43
+ emit
44
+ sleep(1) and emit until done?
45
+ end
46
+ end
47
+
48
+ def emit
49
+ format = "\r[%-#{progress_bar_width}s] %#{count_width}i/%i (%i%%)"
50
+ console.print format % [progress_markers, ticks, total, percentage]
51
+ emit_final if done?
52
+ end
53
+
54
+ def emit_final
55
+ console.puts
56
+
57
+ console.puts "Start: #{start_time}"
58
+ console.puts "End: #{end_time}"
59
+ console.puts "Duration: #{duration}"
60
+ end
61
+
62
+ def done?
63
+ ticks >= total || end_time
64
+ end
65
+
66
+ def start_time
67
+ @start_time
68
+ end
69
+
70
+ def end_time
71
+ @end_time
72
+ end
73
+
74
+ def ticks
75
+ @ticks
76
+ end
77
+
78
+ def total
79
+ @total
80
+ end
81
+
82
+ def percentage
83
+ ((ticks.to_f / total) * 100).floor
84
+ end
85
+
86
+ def progress_markers
87
+ "=" * ((ticks.to_f / total) * progress_bar_width).floor
88
+ end
89
+
90
+ def console
91
+ $stderr
92
+ end
93
+
94
+ def progress_bar_width
95
+ @progress_bar_width
96
+ end
97
+
98
+ def count_width
99
+ @count_width
100
+ end
101
+
102
+ def total_time
103
+ @end_time - @start_time
104
+ end
105
+
106
+ def duration
107
+ duration = Time.at(total_time).utc
108
+ duration.strftime("%H:%M:%S.#{duration.usec}")
109
+ end
110
+
111
+ end
112
+ end
@@ -55,11 +55,11 @@ module Squealer
55
55
  end
56
56
 
57
57
  def infer_row_id
58
- @binding.eval "#{@table_name}._id"
58
+ eval "#{@table_name}._id", @binding, __FILE__, __LINE__
59
59
  end
60
60
 
61
61
  def verify_table_name_in_scope
62
- table = @binding.eval "#{@table_name}"
62
+ table = eval "#{@table_name}", @binding, __FILE__, __LINE__
63
63
  raise ArgumentError, "The variable '#{@table_name}' is not a hashmap" unless table.is_a? Hash
64
64
  raise ArgumentError, "The hashmap '#{@table_name}' must have an '_id' key" unless table.has_key? '_id'
65
65
  rescue NameError
@@ -68,12 +68,12 @@ module Squealer
68
68
 
69
69
 
70
70
  def infer_value(column_name, binding)
71
- value = binding.eval "#{@table_name}.#{column_name}"
71
+ value = eval "#{@table_name}.#{column_name}", binding, __FILE__, __LINE__
72
72
  unless value
73
73
  name = column_name.to_s
74
- if name.end_with?("_id")
74
+ if name =~ /_id$/
75
75
  related = name[0..-4] #strip "_id"
76
- value = binding.eval "#{related}._id"
76
+ value = eval "#{related}._id", binding, __FILE__, __LINE__
77
77
  end
78
78
  end
79
79
  value
@@ -103,8 +103,11 @@ module Squealer
103
103
 
104
104
  def execute_sql(sql)
105
105
  statement = Database.instance.export.prepare(sql)
106
- values = [*column_values] + [*column_values] #array expando
106
+ values = typecast_values * 2
107
+
107
108
  statement.send(:execute, @row_id, *values) #expand values into distinct arguments
109
+ rescue Mysql::Error, TypeError
110
+ raise "Failed to execute statement: #{sql} with #{values.inspect}.\nOriginal Exception was: #{$!.to_s}"
108
111
  end
109
112
 
110
113
  def pk_name
@@ -113,7 +116,7 @@ module Squealer
113
116
 
114
117
  def column_names
115
118
  return if @column_names.size == 0
116
- ",#{@column_names.join(',')}"
119
+ ",#{@column_names.map { |name| quote_identifier(name) }.join(',')}"
117
120
  end
118
121
 
119
122
  def column_values
@@ -130,10 +133,29 @@ module Squealer
130
133
  def column_markers
131
134
  return if @column_names.size == 0
132
135
  result = ""
133
- @column_names.each {|k| result << "#{k}=?," }
136
+ @column_names.each {|k| result << "#{quote_identifier(k)}=?," }
134
137
  result.chop
135
138
  end
136
139
 
140
+ def typecast_values
141
+ column_values.map do |value|
142
+ case value
143
+ when true, false
144
+ value.to_i
145
+ when Symbol
146
+ value.to_s
147
+ when Array
148
+ value.join(",")
149
+ else
150
+ value
151
+ end
152
+ end
153
+ end
154
+
155
+ def quote_identifier(name)
156
+ "`#{name}`"
157
+ end
158
+
137
159
  class Queue < DelegateClass(Array)
138
160
  include Singleton
139
161
 
data/lib/squealer.rb CHANGED
@@ -3,5 +3,6 @@ require 'squealer/object'
3
3
  require 'squealer/hash'
4
4
  require 'squealer/time'
5
5
 
6
+ require 'squealer/progress_bar'
6
7
  require 'squealer/target'
7
8
  require 'squealer/database'
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,112 @@
1
+ require 'date'
2
+ require 'time'
1
3
  require 'rubygems'
2
4
 
3
5
  $LOAD_PATH.unshift(File.dirname(__FILE__))
4
6
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
7
 
6
8
  require 'squealer'
9
+
10
+ Spec::Runner.configure do |config|
11
+ config.before(:suite) do
12
+ $db_name = "test_export_#{object_id}"
13
+ create_test_db($db_name)
14
+ end
15
+
16
+ config.after(:suite) do
17
+ drop_test_db($db_name)
18
+ end
19
+
20
+ def create_test_db(name)
21
+ @my = Mysql.connect('localhost', 'root')
22
+ @my.query("DROP DATABASE IF EXISTS #{name}")
23
+ @my.query("CREATE DATABASE #{name}")
24
+ @my.query("USE #{name}")
25
+ @my.query("SET sql_mode='ANSI_QUOTES'")
26
+
27
+ create_export_tables
28
+
29
+ Squealer::Database.instance.import_from('localhost', 27017, $db_name)
30
+ @mongo = Squealer::Database.instance.import.send(:instance_variable_get, '@dbc')
31
+ drop_mongo
32
+ seed_import
33
+ end
34
+
35
+ def drop_test_db(name)
36
+ @my.query("DROP DATABASE IF EXISTS #{name}")
37
+ @my.close
38
+
39
+ drop_mongo
40
+ end
41
+
42
+ def drop_mongo
43
+ @mongo.eval('db.dropDatabase()') if @mongo
44
+ end
45
+
46
+ def seed_import
47
+ hashrocket = @mongo.collection('organizations').save({ :name => 'Hashrocket' })
48
+ zorganization = @mongo.collection('organizations').save({ :name => 'Zorganization', :disabled_date => as_time(Date.today) })
49
+
50
+ users = [
51
+ { :name => 'Josh Graham', :dob => as_time(Date.parse('01-Jan-1971')), :gender => 'M',
52
+ :organization_id => hashrocket,
53
+ :activities => [
54
+ { :name => 'Develop squealer', :due_date => as_time(Date.today + 1) },
55
+ { :name => 'Organize speakerconf.com', :due_date => as_time(Date.today + 30) },
56
+ { :name => 'Hashrocket party', :due_date => as_time(Date.today + 7) }
57
+ ]
58
+ },
59
+ { :name => 'Bernerd Schaefer', :dob => as_time(Date.parse('31-Dec-1985')), :gender => 'M',
60
+ :organization_id => hashrocket,
61
+ :activities => [
62
+ { :name => 'Retype all of the code Josh wrote in squealer', :due_date => as_time(Date.today + 2) },
63
+ { :name => 'Listen to rare Thelonius Monk EP', :due_date => as_time(Date.today) },
64
+ { :name => 'Practice karaoke', :due_date => as_time(Date.today + 7) }
65
+ ]
66
+ },
67
+ { :name => 'Your momma', :dob => as_time(Date.parse('15-Jun-1955')), :gender => 'F',
68
+ :organization_id => zorganization,
69
+ :activities => [
70
+ { :name => 'Cook me some pie', :due_date => as_time(Date.today) },
71
+ { :name => 'Make me a sammich', :due_date => as_time(Date.today) }
72
+ ]
73
+ }
74
+ ]
75
+
76
+ users.each { |user| @mongo.collection('users').save user }
77
+ end
78
+
79
+ def create_export_tables
80
+ command = <<-COMMAND.gsub(/\n\s*/, " ")
81
+ CREATE TABLE "users" (
82
+ "id" INT NOT NULL AUTO_INCREMENT ,
83
+ "name" VARCHAR(255) NULL ,
84
+ "gender" CHAR(1) NULL ,
85
+ "dob" DATE NULL ,
86
+ PRIMARY KEY ("id") )
87
+ COMMAND
88
+ @my.query(command)
89
+
90
+ command = <<-COMMAND.gsub(/\n\s*/, " ")
91
+ CREATE TABLE "activity" (
92
+ "id" INT NOT NULL AUTO_INCREMENT ,
93
+ "user_id" INT NULL ,
94
+ "name" VARCHAR(255) NULL ,
95
+ "due_date" DATE NULL ,
96
+ PRIMARY KEY ("id") )
97
+ COMMAND
98
+ @my.query(command)
99
+
100
+ command = <<-COMMAND.gsub(/\n\s*/, " ")
101
+ CREATE TABLE "organizations" (
102
+ "id" INT NOT NULL AUTO_INCREMENT ,
103
+ "disabed_date" DATE NULL ,
104
+ PRIMARY KEY ("id") )
105
+ COMMAND
106
+ @my.query(command)
107
+ end
108
+
109
+ def as_time(date)
110
+ Time.parse(date.to_s)
111
+ end
112
+ end
@@ -3,39 +3,131 @@ require 'mongo'
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Squealer::Database do
6
-
7
- before(:all) do
8
- @db_name = "test_export_#{object_id}"
9
- create_test_db(@db_name)
10
- end
11
-
12
- after(:all) do
13
- drop_test_db(@db_name)
14
- end
15
-
16
6
  it "is a singleton" do
17
7
  Squealer::Database.respond_to?(:instance).should be_true
18
8
  end
19
9
 
20
- it "takes an import database" do
21
- Squealer::Database.instance.import_from('localhost', 27017, @db_name)
22
- Squealer::Database.instance.import.should be_a_kind_of(Mongo::DB)
23
- end
10
+ describe "import" do
11
+ let(:databases) { Squealer::Database.instance }
12
+
13
+
14
+ it "takes an import database" do
15
+ databases.send(:instance_variable_get, '@import_dbc').should be_a_kind_of(Mongo::DB)
16
+ end
24
17
 
25
- it "takes an export database" do
26
- Squealer::Database.instance.export_to('localhost', 'root', '', @db_name)
27
- Squealer::Database.instance.export.should be_a_kind_of(Mysql)
18
+ it "returns a squealer connection object" do
19
+ databases.import.should be_a_kind_of(Squealer::Database::Connection)
20
+ end
21
+
22
+ it "delegates eval to Mongo" do
23
+ databases.send(:instance_variable_get, '@import_dbc').eval('db.getName()').should == $db_name
24
+ databases.import.eval('db.getName()').should == $db_name
25
+ end
28
26
  end
29
27
 
30
- private
28
+ describe "source" do
29
+ let(:databases) { Squealer::Database.instance }
30
+
31
+ before { databases.import_from('localhost', 27017, $db_name) }
32
+
33
+ it "returns a Source" do
34
+ databases.import.source('foo').should be_a_kind_of(Squealer::Database::Source)
35
+ end
36
+
37
+ describe "Source::cursor" do
38
+ it "returns a databases cursor" do
39
+ databases.import.source('foo').cursor.should be_a_kind_of(Mongo::Cursor)
40
+ end
41
+ end
42
+
43
+ context "an empty collection" do
44
+ subject { databases.import.source('foo') }
45
+
46
+ it "counts a total of zero" do
47
+ subject.counts[:total].should == 0
48
+ end
49
+
50
+ it "counts zero imported" do
51
+ subject.counts[:imported].should == 0
52
+ end
53
+
54
+ it "counts zero exported" do
55
+ subject.counts[:exported].should == 0
56
+ end
57
+ end
58
+
59
+ context "a collection with two documents" do
60
+ let(:mongo) { Squealer::Database.instance.import.send(:instance_variable_get, '@dbc') }
61
+
62
+ subject do
63
+ mongo.collection('foo').save({'name' => 'Bar'});
64
+ mongo.collection('foo').save({'name' => 'Baz'});
65
+ source = databases.import.source('foo') # activate the counter
66
+ source.send(:instance_variable_set, :@progress_bar, nil)
67
+ Squealer::ProgressBar.send(:class_variable_set, :@@progress_bar, nil)
68
+ source
69
+ end
70
+
71
+ after do
72
+ mongo.collection('foo').drop
73
+ end
74
+
75
+ it "returns a Source" do
76
+ subject.should be_a_kind_of(Squealer::Database::Source)
77
+ end
78
+
79
+ it "counts a total of two" do
80
+ subject.counts[:total].should == 2
81
+ end
82
+
83
+ context "before iterating" do
84
+ it "counts zero imported" do
85
+ subject.counts[:imported].should == 0
86
+ end
87
+
88
+ it "counts zero exported" do
89
+ subject.counts[:exported].should == 0
90
+ end
91
+ end
92
+
93
+ context "after iterating" do
94
+ before do
95
+ subject.each {}
96
+ end
97
+
98
+ it "counts two imported" do
99
+ subject.counts[:imported].should == 2
100
+ end
101
+
102
+ it "counts two exported" do
103
+ subject.counts[:exported].should == 2
104
+ end
105
+ end
106
+
107
+ context "real squeal" do
108
+ before { pending "interactive_view" }
109
+ subject do
110
+ source = databases.import.source("users")
111
+ end
112
+
113
+ it "^^^ you saw that progress bar right there" do
114
+ subject.each do
115
+ sleep (rand(2) + 1)
116
+ end
117
+ end
118
+ end
119
+
120
+ end
31
121
 
32
- def create_test_db(name)
33
- @my = Mysql.connect('localhost', 'root')
34
- @my.query("create database #{name}")
35
122
  end
36
123
 
37
- def drop_test_db(name)
38
- @my.query("drop database #{name}")
39
- @my.close
124
+ describe "export" do
125
+ let(:databases) { Squealer::Database.instance }
126
+
127
+ it "takes an export database" do
128
+ databases.export_to('localhost', 'root', '', $db_name)
129
+ databases.send(:instance_variable_get, '@export_dbc').should be_a_kind_of(Mysql)
130
+ end
40
131
  end
132
+
41
133
  end
@@ -0,0 +1,191 @@
1
+ require 'spec_helper'
2
+
3
+ describe Squealer::ProgressBar do
4
+ let(:total) { 200 }
5
+ let(:progress_bar) do
6
+ testable_progress_bar = Class.new(Squealer::ProgressBar) do
7
+ attr_reader :emitter
8
+ public :total, :ticks, :percentage, :progress_markers, :emit,
9
+ :duration, :start_time, :end_time, :progress_bar_width
10
+
11
+ def console
12
+ @console ||= StringIO.new
13
+ end
14
+
15
+ alias real_start_emitter start_emitter
16
+ def start_emitter; end
17
+ public :real_start_emitter
18
+
19
+ end
20
+ testable_progress_bar.new(total).start
21
+ end
22
+ let(:console) { progress_bar.console }
23
+ let(:progress_bar_width) { progress_bar.progress_bar_width }
24
+
25
+ before { progress_bar.start }
26
+ after { progress_bar.finish }
27
+
28
+ it "allows only one progress bar at a time" do
29
+ Squealer::ProgressBar.new(0).should be_nil
30
+ end
31
+
32
+ it "records the starting time" do
33
+ progress_bar.start_time.should be_an_instance_of(Time)
34
+ end
35
+
36
+ it "records the starting time" do
37
+ progress_bar.start_time.should be_an_instance_of(Time)
38
+ end
39
+
40
+ context "threaded" do
41
+ before { progress_bar.stub(:emitter).and_return(progress_bar.real_start_emitter) }
42
+ after { progress_bar.emitter.kill }
43
+
44
+ it "has an emitter" do
45
+ progress_bar.tick
46
+ progress_bar.emitter.should_not be_nil
47
+ end
48
+
49
+ it "emits at least once" do
50
+ progress_bar.tick
51
+ progress_bar.emitter.wakeup
52
+ sleep 0.1
53
+ console.string.split("\r").length.should > 0
54
+ end
55
+ end
56
+
57
+ context "no items completed" do
58
+ it "emits the total number provided" do
59
+ progress_bar.total.should == total
60
+ end
61
+
62
+ it "emits the number of ticks" do
63
+ progress_bar.ticks.should == 0
64
+ end
65
+
66
+ it "displays an empty bar" do
67
+ progress_bar.progress_markers.size.should == 0
68
+ end
69
+
70
+ it "prints a progress bar to the console" do
71
+ progress_bar.emit
72
+ console.string.should == "\r[#{' ' * progress_bar_width}] #{0}/#{total} (0%)"
73
+ end
74
+ end
75
+
76
+ context "one item completed" do
77
+ before { progress_bar.tick }
78
+ it "emits the number of ticks" do
79
+ progress_bar.ticks.should == 1
80
+ end
81
+
82
+ it "emits the number of ticks as a percentage of the total number (rounded down)" do
83
+ progress_bar.percentage.should == 0
84
+ end
85
+ end
86
+
87
+ context "1/nth complete (where n is the width of the progress bar)" do
88
+ let(:ticks) { (total / progress_bar_width) }
89
+ before { ticks.times { progress_bar.tick } }
90
+ it "emits the number of ticks" do
91
+ progress_bar.ticks.should == ticks
92
+ end
93
+
94
+ it "emits the number of ticks as a percentage of the total number (rounded down)" do
95
+ progress_bar.percentage.should == (ticks.to_f * 100 / total).floor
96
+ end
97
+
98
+ it "displays the first progress marker" do
99
+ progress_bar.progress_markers.size.should == 1
100
+ end
101
+
102
+ it "prints a progress bar to the console" do
103
+ progress_bar.emit
104
+ percentag = (ticks.to_f * 100 / total).floor
105
+ console.string.should == "\r[=#{' ' * (progress_bar_width - 1)}] #{ticks}/#{total} (#{percentag}%)"
106
+ end
107
+ end
108
+
109
+ context "all but one item completed" do
110
+ let(:ticks) { total - 1 }
111
+ before { ticks.times { progress_bar.tick } }
112
+
113
+ it "emits the number of ticks" do
114
+ progress_bar.ticks.should == ticks
115
+ end
116
+
117
+ it "emits the number of ticks as a percentage of the total number (rounded down)" do
118
+ progress_bar.percentage.should == 99
119
+ end
120
+
121
+ it "has not yet displayed the final progress marker" do
122
+ progress_bar.progress_markers.size.should == (progress_bar_width - 1)
123
+ end
124
+ end
125
+
126
+ context "all items completed" do
127
+ let(:ticks) { total }
128
+ before { ticks.times { progress_bar.tick } }
129
+
130
+ it "emits the number of ticks" do
131
+ progress_bar.ticks.should == ticks
132
+ end
133
+
134
+ it "emits 100%" do
135
+ progress_bar.percentage.should == 100
136
+ end
137
+
138
+ it "fills the progress bar with progress markers" do
139
+ progress_bar.progress_markers.size.should == progress_bar_width
140
+ end
141
+
142
+ it "records the ending time when finished" do
143
+ progress_bar.finish
144
+ progress_bar.end_time.should be_an_instance_of(Time)
145
+ end
146
+
147
+ it "prints a progress bar to the console" do
148
+ progress_bar.finish
149
+ progress_bar.emit
150
+ console.string.split("\n").first.should == "\r[#{'=' * progress_bar_width}] #{ticks}/#{total} (100%)"
151
+ end
152
+ end
153
+
154
+ context "multiple emits" do
155
+ let(:ticks) { total }
156
+ subject { console.string }
157
+ before do
158
+ progress_bar.emit
159
+ end
160
+
161
+ context "not done" do
162
+ it "emitted two lines with no final newline" do
163
+ progress_bar.emit
164
+
165
+ subject.split("\r").size.should == 3
166
+ subject[-1, 1].should_not == "\n"
167
+ end
168
+ end
169
+
170
+ context "done" do
171
+ it "emitted two lines with a final newline" do
172
+ ticks.times { progress_bar.tick }
173
+ progress_bar.finish
174
+ progress_bar.emit
175
+
176
+ subject.split("\r").size.should == 3
177
+ subject[-1, 1].should == "\n"
178
+ end
179
+
180
+ it "emitted final timings" do
181
+ ticks.times { progress_bar.tick }
182
+ progress_bar.finish
183
+ progress_bar.emit
184
+
185
+ subject.should include("Start: #{progress_bar.start_time}\n")
186
+ subject.should include("End: #{progress_bar.end_time}\n")
187
+ subject.should include("Duration: #{progress_bar.duration}\n")
188
+ end
189
+ end
190
+ end
191
+ end
@@ -196,7 +196,6 @@ describe Squealer::Target do
196
196
  end
197
197
  end
198
198
  end
199
-
200
199
  end
201
200
  end
202
201
  end
@@ -210,6 +209,31 @@ describe Squealer::Target do
210
209
  end
211
210
 
212
211
  describe "#target" do
212
+ describe "#typecast_values" do
213
+ subject { target.send(:typecast_values) }
214
+ let(:target) { Squealer::Target.new(export_dbc, table_name) {} }
215
+
216
+ it "casts array to comma-separated string" do
217
+ target.assign(:colA) { ['1', '2'] }
218
+ subject.should == ['1,2']
219
+ end
220
+
221
+ it "casts false to 0 (for mysql TINYINT)" do
222
+ target.assign(:colA) { false }
223
+ subject.should == [0]
224
+ end
225
+
226
+ it "casts true to 1 (for mysql TINYINT)" do
227
+ target.assign(:colA) { true }
228
+ subject.should == [1]
229
+ end
230
+
231
+ it "casts symbol to string" do
232
+ target.assign(:colA) { :open }
233
+ subject.should == ['open']
234
+ end
235
+ end
236
+
213
237
  context "generates SQL command strings" do
214
238
  let(:target) { Squealer::Target.new(export_dbc, table_name) { nil } }
215
239
 
@@ -239,7 +263,7 @@ describe Squealer::Target do
239
263
  end
240
264
 
241
265
  it "includes the column name in the INSERT" do
242
- target.sql.should =~ /\(id,colA\) VALUES/
266
+ target.sql.should =~ /\(id,`colA`\) VALUES/
243
267
  end
244
268
 
245
269
  it "includes the column value in the INSERT" do
@@ -249,7 +273,7 @@ describe Squealer::Target do
249
273
 
250
274
  it "includes the column name and value in the UPDATE" do
251
275
  # target.sql.should =~ /UPDATE colA='#{value_1}'/
252
- target.sql.should =~ /UPDATE colA=\?/
276
+ target.sql.should =~ /UPDATE `colA`=\?/
253
277
  end
254
278
 
255
279
  end
@@ -265,7 +289,7 @@ describe Squealer::Target do
265
289
  end
266
290
 
267
291
  it "includes the column names in the INSERT" do
268
- target.sql.should =~ /\(id,colA,colB\) VALUES/
292
+ target.sql.should =~ /\(id,`colA`,`colB`\) VALUES/
269
293
  end
270
294
 
271
295
  it "includes the column values in the INSERT" do
@@ -275,7 +299,7 @@ describe Squealer::Target do
275
299
 
276
300
  it "includes the column names and values in the UPDATE" do
277
301
  # target.sql.should =~ /UPDATE colA='#{value_1}',colB='#{value_2}'/
278
- target.sql.should =~ /UPDATE colA=\?,colB=\?/
302
+ target.sql.should =~ /UPDATE `colA`=\?,`colB`=\?/
279
303
  end
280
304
  end
281
305
  end
data/squealer.gemspec CHANGED
@@ -5,13 +5,15 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{squealer}
8
- s.version = "1.2.0"
8
+ s.version = "2.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Josh Graham", "Durran Jordan"]
12
- s.date = %q{2010-05-17}
11
+ s.authors = ["Josh Graham", "Durran Jordan", "Matt Yoho", "Bernerd Schaefer"]
12
+ s.date = %q{2010-05-20}
13
+ s.default_executable = %q{skewer}
13
14
  s.description = %q{Exports mongodb to mysql. More later.}
14
15
  s.email = %q{joshua.graham@grahamis.com}
16
+ s.executables = ["skewer"]
15
17
  s.extra_rdoc_files = [
16
18
  "README.md"
17
19
  ]
@@ -22,12 +24,15 @@ Gem::Specification.new do |s|
22
24
  "README.md",
23
25
  "Rakefile",
24
26
  "VERSION",
27
+ "bin/skewer",
28
+ "lib/.example_squeal.rb.swp",
25
29
  "lib/example_squeal.rb",
26
30
  "lib/squealer.rb",
27
31
  "lib/squealer/boolean.rb",
28
32
  "lib/squealer/database.rb",
29
33
  "lib/squealer/hash.rb",
30
34
  "lib/squealer/object.rb",
35
+ "lib/squealer/progress_bar.rb",
31
36
  "lib/squealer/target.rb",
32
37
  "lib/squealer/time.rb",
33
38
  "lib/tasks/jeweler.rake",
@@ -37,6 +42,7 @@ Gem::Specification.new do |s|
37
42
  "spec/squealer/database_spec.rb",
38
43
  "spec/squealer/hash_spec.rb",
39
44
  "spec/squealer/object_spec.rb",
45
+ "spec/squealer/progress_bar_spec.rb",
40
46
  "spec/squealer/target_spec.rb",
41
47
  "spec/squealer/time_spec.rb",
42
48
  "squealer.gemspec"
@@ -44,7 +50,7 @@ Gem::Specification.new do |s|
44
50
  s.homepage = %q{http://github.com/delitescere/squealer/}
45
51
  s.rdoc_options = ["--charset=UTF-8"]
46
52
  s.require_paths = ["lib"]
47
- s.rubygems_version = %q{1.3.7}
53
+ s.rubygems_version = %q{1.3.6}
48
54
  s.summary = %q{Document-oriented to Relational database exporter}
49
55
  s.test_files = [
50
56
  "spec/spec_helper.rb",
@@ -52,6 +58,7 @@ Gem::Specification.new do |s|
52
58
  "spec/squealer/database_spec.rb",
53
59
  "spec/squealer/hash_spec.rb",
54
60
  "spec/squealer/object_spec.rb",
61
+ "spec/squealer/progress_bar_spec.rb",
55
62
  "spec/squealer/target_spec.rb",
56
63
  "spec/squealer/time_spec.rb"
57
64
  ]
@@ -60,16 +67,19 @@ Gem::Specification.new do |s|
60
67
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
61
68
  s.specification_version = 3
62
69
 
63
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
70
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
64
71
  s.add_runtime_dependency(%q<mysql>, [">= 2.8.1"])
65
72
  s.add_runtime_dependency(%q<mongo>, [">= 0.18.3"])
73
+ s.add_runtime_dependency(%q<bson_ext>, [">= 1.0.1"])
66
74
  else
67
75
  s.add_dependency(%q<mysql>, [">= 2.8.1"])
68
76
  s.add_dependency(%q<mongo>, [">= 0.18.3"])
77
+ s.add_dependency(%q<bson_ext>, [">= 1.0.1"])
69
78
  end
70
79
  else
71
80
  s.add_dependency(%q<mysql>, [">= 2.8.1"])
72
81
  s.add_dependency(%q<mongo>, [">= 0.18.3"])
82
+ s.add_dependency(%q<bson_ext>, [">= 1.0.1"])
73
83
  end
74
84
  end
75
85
 
metadata CHANGED
@@ -1,33 +1,32 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: squealer
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
5
4
  prerelease: false
6
5
  segments:
7
- - 1
8
6
  - 2
7
+ - 1
9
8
  - 0
10
- version: 1.2.0
9
+ version: 2.1.0
11
10
  platform: ruby
12
11
  authors:
13
12
  - Josh Graham
14
13
  - Durran Jordan
14
+ - Matt Yoho
15
+ - Bernerd Schaefer
15
16
  autorequire:
16
17
  bindir: bin
17
18
  cert_chain: []
18
19
 
19
- date: 2010-05-17 00:00:00 -05:00
20
- default_executable:
20
+ date: 2010-05-20 00:00:00 -05:00
21
+ default_executable: skewer
21
22
  dependencies:
22
23
  - !ruby/object:Gem::Dependency
23
24
  name: mysql
24
25
  prerelease: false
25
26
  requirement: &id001 !ruby/object:Gem::Requirement
26
- none: false
27
27
  requirements:
28
28
  - - ">="
29
29
  - !ruby/object:Gem::Version
30
- hash: 45
31
30
  segments:
32
31
  - 2
33
32
  - 8
@@ -39,11 +38,9 @@ dependencies:
39
38
  name: mongo
40
39
  prerelease: false
41
40
  requirement: &id002 !ruby/object:Gem::Requirement
42
- none: false
43
41
  requirements:
44
42
  - - ">="
45
43
  - !ruby/object:Gem::Version
46
- hash: 81
47
44
  segments:
48
45
  - 0
49
46
  - 18
@@ -51,10 +48,24 @@ dependencies:
51
48
  version: 0.18.3
52
49
  type: :runtime
53
50
  version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: bson_ext
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 1
60
+ - 0
61
+ - 1
62
+ version: 1.0.1
63
+ type: :runtime
64
+ version_requirements: *id003
54
65
  description: Exports mongodb to mysql. More later.
55
66
  email: joshua.graham@grahamis.com
56
- executables: []
57
-
67
+ executables:
68
+ - skewer
58
69
  extensions: []
59
70
 
60
71
  extra_rdoc_files:
@@ -66,12 +77,15 @@ files:
66
77
  - README.md
67
78
  - Rakefile
68
79
  - VERSION
80
+ - bin/skewer
81
+ - lib/.example_squeal.rb.swp
69
82
  - lib/example_squeal.rb
70
83
  - lib/squealer.rb
71
84
  - lib/squealer/boolean.rb
72
85
  - lib/squealer/database.rb
73
86
  - lib/squealer/hash.rb
74
87
  - lib/squealer/object.rb
88
+ - lib/squealer/progress_bar.rb
75
89
  - lib/squealer/target.rb
76
90
  - lib/squealer/time.rb
77
91
  - lib/tasks/jeweler.rake
@@ -81,6 +95,7 @@ files:
81
95
  - spec/squealer/database_spec.rb
82
96
  - spec/squealer/hash_spec.rb
83
97
  - spec/squealer/object_spec.rb
98
+ - spec/squealer/progress_bar_spec.rb
84
99
  - spec/squealer/target_spec.rb
85
100
  - spec/squealer/time_spec.rb
86
101
  - squealer.gemspec
@@ -94,27 +109,23 @@ rdoc_options:
94
109
  require_paths:
95
110
  - lib
96
111
  required_ruby_version: !ruby/object:Gem::Requirement
97
- none: false
98
112
  requirements:
99
113
  - - ">="
100
114
  - !ruby/object:Gem::Version
101
- hash: 3
102
115
  segments:
103
116
  - 0
104
117
  version: "0"
105
118
  required_rubygems_version: !ruby/object:Gem::Requirement
106
- none: false
107
119
  requirements:
108
120
  - - ">="
109
121
  - !ruby/object:Gem::Version
110
- hash: 3
111
122
  segments:
112
123
  - 0
113
124
  version: "0"
114
125
  requirements: []
115
126
 
116
127
  rubyforge_project:
117
- rubygems_version: 1.3.7
128
+ rubygems_version: 1.3.6
118
129
  signing_key:
119
130
  specification_version: 3
120
131
  summary: Document-oriented to Relational database exporter
@@ -124,5 +135,6 @@ test_files:
124
135
  - spec/squealer/database_spec.rb
125
136
  - spec/squealer/hash_spec.rb
126
137
  - spec/squealer/object_spec.rb
138
+ - spec/squealer/progress_bar_spec.rb
127
139
  - spec/squealer/target_spec.rb
128
140
  - spec/squealer/time_spec.rb