squealer 1.2.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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