mongify 1.0.1 → 1.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.yardopts +3 -0
  4. data/CHANGELOG.rdoc +5 -0
  5. data/Gemfile.lock +52 -6
  6. data/LICENSE +1 -1
  7. data/README.rdoc +29 -11
  8. data/Rakefile +37 -9
  9. data/features/options.feature +2 -0
  10. data/features/print.feature +1 -1
  11. data/features/process.feature +10 -1
  12. data/features/step_definitions/process_steps.rb +11 -1
  13. data/features/support/env.rb +1 -1
  14. data/lib/mongify/cli/application.rb +7 -7
  15. data/lib/mongify/cli/command/worker.rb +18 -14
  16. data/lib/mongify/cli/options.rb +2 -1
  17. data/lib/mongify/configuration.rb +5 -5
  18. data/lib/mongify/database/base_connection.rb +6 -6
  19. data/lib/mongify/database/column.rb +40 -40
  20. data/lib/mongify/database/data_row.rb +9 -9
  21. data/lib/mongify/database/no_sql_connection.rb +61 -35
  22. data/lib/mongify/database/sql_connection.rb +44 -15
  23. data/lib/mongify/database/table.rb +62 -46
  24. data/lib/mongify/exceptions.rb +5 -5
  25. data/lib/mongify/progressbar.rb +15 -15
  26. data/lib/mongify/status.rb +8 -8
  27. data/lib/mongify/translation.rb +19 -17
  28. data/lib/mongify/translation/process.rb +16 -123
  29. data/lib/mongify/translation/processor_common.rb +132 -0
  30. data/lib/mongify/translation/sync.rb +112 -0
  31. data/lib/mongify/ui.rb +9 -9
  32. data/lib/mongify/version.rb +1 -1
  33. data/mongify.gemspec +4 -2
  34. data/spec/files/deleting_fields_from_embedding_parent_translation.rb +19 -0
  35. data/spec/files/embedded_parent_translation.rb +1 -1
  36. data/spec/mongify/cli/application_spec.rb +1 -1
  37. data/spec/mongify/cli/options_spec.rb +1 -1
  38. data/spec/mongify/cli/worker_command_spec.rb +46 -17
  39. data/spec/mongify/database/column_spec.rb +21 -21
  40. data/spec/mongify/database/data_row_spec.rb +11 -11
  41. data/spec/mongify/database/no_sql_connection_spec.rb +61 -27
  42. data/spec/mongify/database/sql_connection_spec.rb +62 -2
  43. data/spec/mongify/database/table_spec.rb +53 -29
  44. data/spec/mongify/translation/printer_spec.rb +2 -2
  45. data/spec/mongify/translation/process_spec.rb +50 -34
  46. data/spec/mongify/translation/sync_spec.rb +184 -0
  47. data/spec/mongify/translation_spec.rb +8 -8
  48. data/spec/mongify/ui_spec.rb +12 -12
  49. data/spec/mongify_spec.rb +1 -1
  50. data/spec/spec_helper.rb +8 -1
  51. data/spec/support/config_reader.rb +2 -2
  52. data/spec/support/database_generator.rb +68 -25
  53. data/spec/support/database_output.txt +17 -0
  54. metadata +41 -6
@@ -0,0 +1,112 @@
1
+ require 'mongify/translation/processor_common'
2
+ module Mongify
3
+ class Translation
4
+ #
5
+ # This module does the sync processing on the translation object
6
+ #
7
+ include ProcessorCommon
8
+ module Sync
9
+ attr_accessor :max_updated_at
10
+ DRAFT_KEY = "__mongify_sync_draft__"
11
+ SYNC_HELPER_TABLE = "__mongify_sync_helper__"
12
+
13
+ class SyncHelperMigrator < ActiveRecord::Migration
14
+ def up
15
+ create_table SYNC_HELPER_TABLE, :id => false do |t|
16
+ t.string :table_name
17
+ t.datetime :last_updated_at
18
+ end
19
+ add_index SYNC_HELPER_TABLE, :table_name
20
+ end
21
+ end
22
+
23
+ # Does the actual act of sync processing the translation.
24
+ # Takes in both a sql connection and a no sql connection
25
+ def sync(sql_connection, no_sql_connection)
26
+ prepare_connections(sql_connection, no_sql_connection)
27
+ setup_sync_table
28
+ setup_db_index
29
+ sync_data
30
+ set_last_updated_at
31
+ copy_embedded_tables
32
+ sync_update_reference_ids
33
+ copy_polymorphic_tables
34
+ nil
35
+ end
36
+
37
+ #######
38
+ private
39
+ #######
40
+
41
+ def setup_sync_table
42
+ # make sure table exists
43
+ begin
44
+ self.sql_connection.execute("SELECT count(*) FROM #{SYNC_HELPER_TABLE}")
45
+ rescue
46
+ SyncHelperMigrator.new.up
47
+ end
48
+ # insert missing records for sync tables
49
+ self.copy_tables.each do |t|
50
+ if self.sql_connection.count(SYNC_HELPER_TABLE, "table_name = '#{t.sql_name}'") == 0
51
+ self.sql_connection.execute("INSERT INTO #{SYNC_HELPER_TABLE} (table_name, last_updated_at) VALUES ('#{t.sql_name}', '1970-01-01')")
52
+ end
53
+ end
54
+ end
55
+
56
+ # Does the straight copy (of tables)
57
+ def sync_data
58
+ self.copy_tables.each do |t|
59
+ q = "SELECT t.* FROM #{t.sql_name} t, #{SYNC_HELPER_TABLE} u " +
60
+ "WHERE t.updated_at > u.last_updated_at AND u.table_name = '#{t.sql_name}'"
61
+ rows = sql_connection.select_by_query(q)
62
+ Mongify::Status.publish('copy_data', :size => rows.count, :name => "Syncing #{t.name}", :action => 'add')
63
+ max_updated_at, max_updated_at_id = Time.new(1970), nil
64
+ rows.each do |row|
65
+ row_hash = t.translate(row)
66
+ updated_at = Time.parse(row['updated_at'])
67
+ if updated_at > max_updated_at
68
+ max_updated_at = updated_at
69
+ max_updated_at_id = row_hash['pre_mongified_id']
70
+ end
71
+ no_sql_connection.upsert(t.name, row_hash.merge({DRAFT_KEY => true}))
72
+ Mongify::Status.publish('copy_data')
73
+ end
74
+ (self.max_updated_at ||= {})[t.sql_name] = {'max_updated_at_id' => max_updated_at_id, 'key_column' => t.key_column.name}
75
+ Mongify::Status.publish('copy_data', :action => 'finish')
76
+ end
77
+ end
78
+
79
+ # Updates the reference ids in the no sql database
80
+ def sync_update_reference_ids
81
+ self.copy_tables.each do |t|
82
+ rows = no_sql_connection.select_by_query(t.name, {DRAFT_KEY => true})
83
+ Mongify::Status.publish('update_references', :size => rows.count, :name => "Updating References #{t.name}", :action => 'add')
84
+ rows.each do |row|
85
+ id = row["_id"]
86
+ attributes = fetch_reference_ids(t, row)
87
+ setter = {"$unset" => {DRAFT_KEY => true}}
88
+ setter["$set"] = attributes unless attributes.blank?
89
+ no_sql_connection.update(t.name, id, setter)
90
+ Mongify::Status.publish('update_references')
91
+ end
92
+ Mongify::Status.publish('update_references', :action => 'finish')
93
+ end
94
+ end
95
+
96
+ # Sets the last updated_at flag so that next sync doesn't unnecessarily copy old data
97
+ def set_last_updated_at
98
+ tables = self.copy_tables
99
+ Mongify::Status.publish('set_last_updated_at', :size => tables.length, :name => "Setting last_updated_at", :action => 'add')
100
+ tables.each do |t|
101
+ info = self.max_updated_at[t.sql_name]
102
+ if info && info['max_updated_at_id']
103
+ sql_connection.execute("UPDATE #{SYNC_HELPER_TABLE} SET last_updated_at = (SELECT updated_at FROM #{t.sql_name} WHERE #{info['key_column']} = '#{info['max_updated_at_id']}') WHERE table_name = '#{t.sql_name}'")
104
+ end
105
+ Mongify::Status.publish('set_last_updated_at')
106
+ end
107
+ Mongify::Status.publish('set_last_updated_at', :action => 'finish')
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -8,39 +8,39 @@ module Mongify
8
8
  def puts(msg)
9
9
  out_stream.puts(msg) if out_stream
10
10
  end
11
-
11
+
12
12
  # Outputs to stream using print method
13
13
  def print(msg)
14
14
  out_stream.print(msg) if out_stream
15
15
  end
16
-
16
+
17
17
  # Gets input from user
18
18
  def gets
19
19
  in_stream ? in_stream.gets : ''
20
20
  end
21
-
21
+
22
22
  # Outputs a question and gets input
23
23
  def request(msg)
24
24
  print(msg)
25
25
  gets.chomp
26
26
  end
27
-
27
+
28
28
  # Asks a yes or no question and waits for reply
29
29
  def ask(msg)
30
30
  request("#{msg} [yn] ") == 'y'
31
31
  end
32
-
32
+
33
33
  # Outputs a Warning (using puts command)
34
34
  def warn(msg)
35
35
  puts "WARNING: #{msg}"
36
36
  end
37
-
37
+
38
38
  # Outputs a message and aborts execution of app
39
39
  def abort(message)
40
40
  UI.puts "PROGRAM HALT: #{message}"
41
41
  Kernel.abort message
42
42
  end
43
-
43
+
44
44
  # Incoming stream
45
45
  def in_stream
46
46
  Configuration.in_stream
@@ -49,11 +49,11 @@ module Mongify
49
49
  def out_stream
50
50
  Configuration.out_stream
51
51
  end
52
-
52
+
53
53
  # Creates an instance of HighLine
54
54
  # which lets us figure out width of console
55
55
  # plus a whole lot more
56
- # @return [HighLine] instance
56
+ # @return [HighLine] instance
57
57
  def terminal_helper
58
58
  @terminal_helper ||= HighLine.new
59
59
  end
@@ -1,4 +1,4 @@
1
1
  module Mongify
2
2
  # Mongify's Current Version Number
3
- VERSION = "1.0.1"
3
+ VERSION = "1.1.0"
4
4
  end
@@ -23,10 +23,12 @@ Gem::Specification.new do |s|
23
23
  s.add_development_dependency('cucumber', '>= 0.10')
24
24
  s.add_development_dependency('mocha', '>= 0.9.8')
25
25
  s.add_development_dependency('yard', '>= 0.5.3')
26
- s.add_development_dependency('sqlite3-ruby', '>= 1.3')
27
- s.add_development_dependency('mysql2', '~> 0.2.7')
26
+ s.add_development_dependency('sqlite3', '>= 1.3')
27
+ s.add_development_dependency('pg', '>= 0.17')
28
+ s.add_development_dependency('mysql2', '~> 0.3.1')
28
29
  s.add_development_dependency('watchr', '>= 0.6')
29
30
  s.add_development_dependency('rake')
31
+ s.add_development_dependency('pry-plus')
30
32
 
31
33
  s.files = `git ls-files`.split("\n")
32
34
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,19 @@
1
+ table 'teams' do
2
+ column 'id', :key
3
+ column 'name', :string
4
+ column 'phone', :string
5
+ column 'created_at', :datetime
6
+ column 'updated_at', :datetime
7
+ end
8
+
9
+ table 'coaches', :embed_in => 'teams', :as => 'object', :rename_to => 'coach' do
10
+ column 'id', :key
11
+ column 'team_id', :integer, :references => "teams"
12
+ column 'first_name', :string
13
+ column 'last_name', :string
14
+ column 'created_at', :datetime
15
+ column 'updated_at', :datetime
16
+ before_save do |row, parent_row|
17
+ row.write_attribute('phone',parent_row.delete('phone'))
18
+ end
19
+ end
@@ -12,7 +12,7 @@ table "preferences", :embed_in => :users, :as => :array do
12
12
  column "notify_by_email", :boolean
13
13
  column "created_at", :datetime, :ignore => true
14
14
  column "updated_at", :datetime, :ignore => true
15
-
15
+
16
16
  before_save do |row, parent|
17
17
  parent.notify_by_email = row.delete('notify_by_email')
18
18
  end
@@ -7,7 +7,7 @@ describe Mongify::CLI::Application do
7
7
  Mongify::Configuration.out_stream = nil
8
8
  @application.execute!.should == 0
9
9
  end
10
-
10
+
11
11
  it "should return a 1 on error" do
12
12
  @application = Mongify::CLI::Application.new(["door"])
13
13
  Mongify::Configuration.out_stream = nil
@@ -52,7 +52,7 @@ describe Mongify::CLI::Options do
52
52
  it "should call Configuration.parse" do
53
53
  Mongify::Configuration.should_receive(:parse).and_return(Mongify::Configuration.new)
54
54
  @options = Mongify::CLI::Options.new(['check', config_file])
55
- @options.parse
55
+ @options.parse
56
56
  end
57
57
  end
58
58
  end
@@ -11,19 +11,19 @@ describe Mongify::CLI::Command::Worker do
11
11
  @no_sql_connection.stub(:valid?).and_return(true)
12
12
  @no_sql_connection.stub(:has_connection?).and_return(true)
13
13
  @config.stub(:no_sql_connection).and_return(@no_sql_connection)
14
-
14
+
15
15
  @translation_file = 'spec/files/translation.rb'
16
-
16
+
17
17
  Mongify::Translation.stub(:load).and_return(stub(:print => 'worked'))
18
18
  @view = mock('view').as_null_object
19
19
  end
20
-
20
+
21
21
  context "list_commands" do
22
22
  it "should return same number as available" do
23
23
  Mongify::CLI::Command::Worker.list_commands.size.should == Mongify::CLI::Command::Worker::AVAILABLE_COMMANDS.size
24
24
  end
25
25
  end
26
-
26
+
27
27
  context "check command" do
28
28
  before(:each) do
29
29
  @command = Mongify::CLI::Command::Worker.new('check', @config)
@@ -42,17 +42,17 @@ describe Mongify::CLI::Command::Worker do
42
42
  lambda { @command = Mongify::CLI::Command::Worker.new('check').execute(@view) }.should raise_error(Mongify::ConfigurationFileNotFound)
43
43
  end
44
44
  end
45
-
45
+
46
46
  context "non-existing command" do
47
47
  before(:each) do
48
48
  @command = Mongify::CLI::Command::Worker.new('unknown')
49
49
  end
50
-
50
+
51
51
  it "should report error" do
52
52
  @view.should_receive(:report_error)
53
53
  @command.execute(@view)
54
54
  end
55
-
55
+
56
56
  it "should output an error" do
57
57
  @view.should_receive(:output).with("Unknown action unknown\n\n")
58
58
  @command.execute(@view)
@@ -64,22 +64,22 @@ describe Mongify::CLI::Command::Worker do
64
64
  @command = Mongify::CLI::Command::Worker.new('translation', @config)
65
65
  Mongify::Translation.stub(:load).with(@sql_connection).and_return(stub(:print => 'worked'))
66
66
  end
67
-
67
+
68
68
  it "should require configuration file" do
69
69
  lambda { Mongify::CLI::Command::Worker.new('translation').execute(@view) }.should raise_error(Mongify::ConfigurationFileNotFound)
70
70
  end
71
-
71
+
72
72
  it "should check sql connection" do
73
73
  @command.should_receive(:check_sql_connection).and_return(true)
74
74
  @command.execute(@view)
75
75
  end
76
-
76
+
77
77
  it "should call load on Translation" do
78
78
  Mongify::Translation.should_receive(:load).with(@sql_connection).and_return(stub(:print => 'worked'))
79
79
  @command.execute(@view)
80
80
  end
81
81
  end
82
-
82
+
83
83
  context "process command" do
84
84
  before(:each) do
85
85
  @command = Mongify::CLI::Command::Worker.new('process', @config, 'spec/files/translation.rb')
@@ -90,25 +90,54 @@ describe Mongify::CLI::Command::Worker do
90
90
  @view.should_receive(:report_success)
91
91
  @command.execute(@view)
92
92
  end
93
-
93
+
94
94
  it "should require config file" do
95
95
  lambda { @command = Mongify::CLI::Command::Worker.new('process').execute(@view) }.should raise_error(Mongify::ConfigurationFileNotFound)
96
96
  end
97
-
97
+
98
98
  it "should require transaction file" do
99
99
  lambda { @command = Mongify::CLI::Command::Worker.new('process', @config).execute(@view) }.should raise_error(Mongify::TranslationFileNotFound)
100
100
  end
101
-
101
+
102
102
  it "should check_connection" do
103
103
  @command.should_receive(:check_connections).and_return(true)
104
104
  @command.execute(@view)
105
105
  end
106
-
106
+
107
107
  it "should call process on translation" do
108
108
  Mongify::Translation.should_receive(:parse).and_return(mock(:process => true))
109
109
  @command.execute(@view)
110
110
  end
111
-
112
-
113
111
  end
112
+
113
+ context "sync command" do
114
+ before(:each) do
115
+ @command = Mongify::CLI::Command::Worker.new('sync', @config, 'spec/files/translation.rb')
116
+ Mongify::Translation.stub(:parse).and_return(mock(:sync => true))
117
+ end
118
+ it "should report success" do
119
+ @view.should_receive(:report_error).never
120
+ @view.should_receive(:report_success)
121
+ @command.execute(@view)
122
+ end
123
+
124
+ it "should require config file" do
125
+ lambda { @command = Mongify::CLI::Command::Worker.new('sync').execute(@view) }.should raise_error(Mongify::ConfigurationFileNotFound)
126
+ end
127
+
128
+ it "should require transaction file" do
129
+ lambda { @command = Mongify::CLI::Command::Worker.new('sync', @config).execute(@view) }.should raise_error(Mongify::TranslationFileNotFound)
130
+ end
131
+
132
+ it "should check_connection" do
133
+ @command.should_receive(:check_connections).and_return(true)
134
+ @command.execute(@view)
135
+ end
136
+
137
+ it "should call sync on translation" do
138
+ Mongify::Translation.should_receive(:parse).and_return(mock(:sync => true))
139
+ @command.execute(@view)
140
+ end
141
+ end
142
+
114
143
  end
@@ -4,41 +4,41 @@ describe Mongify::Database::Column do
4
4
  before(:each) do
5
5
  @column = Mongify::Database::Column.new('first_name')
6
6
  end
7
-
7
+
8
8
  it "should have name" do
9
9
  @column.name.should == 'first_name'
10
10
  end
11
11
  it "should have a sql_name" do
12
12
  @column.sql_name.should == 'first_name'
13
13
  end
14
-
14
+
15
15
  it "should allow you to omit the type while giving options" do
16
16
  @column = Mongify::Database::Column.new('account_id', :references => 'accounts')
17
17
  @column.options.should == {'references' => 'accounts'}
18
18
  end
19
-
19
+
20
20
  it "should get setup options" do
21
21
  @column = Mongify::Database::Column.new('account_id', :integer, :references => 'accounts')
22
22
  @column.options.should == {'references' => 'accounts'}
23
23
  end
24
-
24
+
25
25
  it "should force type to string if nil" do
26
26
  @column = Mongify::Database::Column.new('first_name', nil)
27
27
  @column.type.should == :string
28
28
  end
29
-
29
+
30
30
  context "auto_detect!" do
31
31
  it "should not auto detect automatically" do
32
32
  Mongify::Database::Column.should_receive(:auto_detect).never
33
33
  @column = Mongify::Database::Column.new('id', :integer)
34
34
  @column.should_not be_key
35
35
  end
36
-
36
+
37
37
  it "should auto_detect when option is passed in" do
38
38
  @column = Mongify::Database::Column.new('id', :integer, :auto_detect => true)
39
39
  @column.should be_key
40
40
  end
41
-
41
+
42
42
  context "id column" do
43
43
  before(:each) do
44
44
  @col = mock(:sql_name => 'id')
@@ -82,19 +82,19 @@ describe Mongify::Database::Column do
82
82
  end
83
83
  end
84
84
  end
85
-
85
+
86
86
  context "key?" do
87
87
  it "should be true" do
88
88
  @column = Mongify::Database::Column.new('id', :key)
89
89
  @column.should be_a_key
90
90
  end
91
-
91
+
92
92
  it "should be true" do
93
93
  @column = Mongify::Database::Column.new('first_name', :string)
94
94
  @column.should_not be_a_key
95
95
  end
96
96
  end
97
-
97
+
98
98
  context "rename_to" do
99
99
  before(:each) do
100
100
  @column = Mongify::Database::Column.new('surname', :string, :rename_to => 'last_name')
@@ -108,12 +108,12 @@ describe Mongify::Database::Column do
108
108
  it "should translate to new name" do
109
109
  @column.translate('value').should == {'last_name' => 'value'}
110
110
  end
111
-
111
+
112
112
  it "should rename this" do
113
113
  @column = Mongify::Database::Column.new("geoCode_longitude", :string, :rename_to => 'longitude')
114
114
  @column.translate(42.2222).should == {'longitude' => '42.2222'}
115
115
  end
116
-
116
+
117
117
  it "should be renamed" do
118
118
  @column.should be_renamed
119
119
  end
@@ -122,7 +122,7 @@ describe Mongify::Database::Column do
122
122
  col.should_not be_renamed
123
123
  end
124
124
  end
125
-
125
+
126
126
  context "options" do
127
127
  it "should allow to be set by name" do
128
128
  @column = Mongify::Database::Column.new('first_name')
@@ -134,8 +134,8 @@ describe Mongify::Database::Column do
134
134
  @column = Mongify::Database::Column.new('first_name')
135
135
  lambda { @column.unknown = "users" }.should raise_error(NoMethodError)
136
136
  end
137
- end
138
-
137
+ end
138
+
139
139
  context "as" do
140
140
  subject {Mongify::Database::Column.new('total', :decimal)}
141
141
  it "should default to string" do
@@ -173,7 +173,7 @@ describe Mongify::Database::Column do
173
173
  subject.scale.should be_zero
174
174
  end
175
175
  end
176
-
176
+
177
177
  context :to_print do
178
178
  before(:each) do
179
179
  @column = Mongify::Database::Column.new('first_name', :string)
@@ -195,14 +195,14 @@ describe Mongify::Database::Column do
195
195
  @column.to_print.should == %q{column "first_name", :key, :as => :integer}
196
196
  end
197
197
  end
198
-
198
+
199
199
  context :referenced? do
200
200
  it "should be true" do
201
201
  @column = Mongify::Database::Column.new('user_id', :integer, :references => 'users')
202
202
  @column.should be_a_referenced
203
203
  end
204
204
  end
205
-
205
+
206
206
  context :translate do
207
207
  it "should return a hash with the new translation" do
208
208
  @column = Mongify::Database::Column.new('first_name', :string)
@@ -213,7 +213,7 @@ describe Mongify::Database::Column do
213
213
  @column.should be_ignored
214
214
  @column.translate('bob').should == {}
215
215
  end
216
-
216
+
217
217
  it "should return pre_mongified_id when type is a key" do
218
218
  @column = Mongify::Database::Column.new('id', :key)
219
219
  @column.translate(123123).should == {"pre_mongified_id" => 123123}
@@ -295,7 +295,7 @@ describe Mongify::Database::Column do
295
295
  it "should return a string value" do
296
296
  @column.send(:type_cast, 101.43).should be_a_kind_of String
297
297
  end
298
-
298
+
299
299
  context :integer do
300
300
  before(:each) do
301
301
  @column = Mongify::Database::Column.new('price', :decimal, :as => :integer)
@@ -387,7 +387,7 @@ describe Mongify::Database::Column do
387
387
  @column.send(:type_cast, nil).should == result
388
388
  @column.send(:type_cast, "").should == result
389
389
  end
390
-
390
+
391
391
  end
392
392
  end
393
393
  end