mongify 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -28,6 +28,7 @@ Examples:
28
28
  #{progname} check database.config
29
29
  #{progname} translation datbase.config > database_translation.rb
30
30
  #{progname} process database.config database_translation.rb
31
+ #{progname} sync database.config database_translation.rb
31
32
 
32
33
  See http://github.com/anlek/mongify for more details
33
34
 
@@ -85,4 +86,4 @@ EOB
85
86
  end
86
87
  end
87
88
  end
88
- end
89
+ end
@@ -6,7 +6,7 @@ module Mongify
6
6
  class Configuration
7
7
  class << self
8
8
  attr_accessor :in_stream, :out_stream
9
-
9
+
10
10
  # Parses a external configuration file and evaluates it and returns a instence of a configuration class
11
11
  def parse(file_name)
12
12
  raise Mongify::ConfigurationFileNotFound, "File #{file_name} is missing" unless File.exists?(file_name)
@@ -16,7 +16,7 @@ module Mongify
16
16
  end
17
17
 
18
18
  end #self
19
-
19
+
20
20
  # Returns a no_sql_connection which is bound to a mongodb adapter
21
21
  # or builds a new no_sql_connection if block is given
22
22
  def mongodb_connection(options={}, &block)
@@ -25,7 +25,7 @@ module Mongify
25
25
  options['adapter'] ||= 'mongodb'
26
26
  @mongodb_connection = no_sql_connection(options, &block)
27
27
  end
28
-
28
+
29
29
  # Returns a sql_connection
30
30
  # If a block is given, it will be executed on the connection
31
31
  # For more information, see {Mongify::Database::SqlConnection}
@@ -34,7 +34,7 @@ module Mongify
34
34
  @sql_connection.instance_exec(&block) if block_given?
35
35
  @sql_connection
36
36
  end
37
-
37
+
38
38
  # Returns a sql_connection
39
39
  # If a block is given, it will be executed on the connection
40
40
  # For more information, see {Mongify::Database::NoSqlConnection}
@@ -43,6 +43,6 @@ module Mongify
43
43
  @no_sql_connection.instance_exec(&block) if block_given?
44
44
  @no_sql_connection
45
45
  end
46
-
46
+
47
47
  end
48
48
  end
@@ -8,10 +8,10 @@ module Mongify
8
8
  # List of required fields to make a valid base connection
9
9
  REQUIRED_FIELDS = %w{host}
10
10
  # List of all the available fields to make up a connection
11
- AVAILABLE_FIELDS = %w{adapter host username password database socket port encoding}
11
+ AVAILABLE_FIELDS = %w{adapter host username password database socket port encoding batch_size}
12
12
  # List of all fields that should be forced to a string
13
13
  STRING_FIELDS = %w{adapter}
14
-
14
+
15
15
  def initialize(options=nil)
16
16
  if options
17
17
  options.stringify_keys!
@@ -20,7 +20,7 @@ module Mongify
20
20
  end
21
21
  end
22
22
  end
23
-
23
+
24
24
  # Returns all settings as a hash, this is used mainly in building ActiveRecord::Base.establish_connection
25
25
  def to_hash
26
26
  hash = {}
@@ -53,15 +53,15 @@ module Mongify
53
53
 
54
54
 
55
55
  # Returns true if we are trying to respond_to AVAILABLE_FIELDS functions
56
- def respond_to?(method, *args)
56
+ def respond_to?(method, *args)
57
57
  return true if AVAILABLE_FIELDS.include?(method.to_s)
58
58
  super(method)
59
59
  end
60
-
60
+
61
61
  # Building set and/or return functions for AVAILABLE_FIELDS.
62
62
  # STRING_FIELDS are forced into string.
63
63
  # Example:
64
- #
64
+ #
65
65
  # def host(value=nil)
66
66
  # @host = value.to_s unless value.nil?
67
67
  # @host
@@ -4,17 +4,17 @@ module Mongify
4
4
  #
5
5
  # A column that is used to access sql data and transform it into the no sql database
6
6
  #
7
- #
7
+ #
8
8
  # ==== Structure
9
- #
9
+ #
10
10
  # Structure for defining a column is as follows:
11
11
  # column "name", :type, {options}
12
12
  # <em>Columns with no type given will be set to <tt>:string</tt></em>
13
- #
13
+ #
14
14
  # ==== Notes
15
15
  # Leaving a column out when defining a table will result in a copy of the information (as a string).
16
16
  # ==== Types
17
- #
17
+ #
18
18
  # Types of columns are supported:
19
19
  # :key # Columns that are primary keys need to be marked as :key type. You can provide an :as if your :key is not an integer column
20
20
  # :integer # Will be converted to a integer
@@ -28,42 +28,42 @@ module Mongify
28
28
  # :time # Will be converted to a Time format (the date portion of the Time object will be 2000-01-01)
29
29
  # :binary # Will be converted to a string
30
30
  # :boolean # Will be converted to a true or false values
31
- #
31
+ #
32
32
  # ==== Options
33
- #
33
+ #
34
34
  # column "post_id", :integer, :references => :posts # Referenced columns need to be marked as such, this will mean that they will be updated
35
35
  # # with the new BSON::ObjectID.
36
36
  # <b>NOTE: if you rename the table 'posts', you should set the :references to the new name</b>
37
- #
37
+ #
38
38
  # column "name", :string, :ignore => true # Ignoring a column will make the column NOT copy over to the new database
39
- #
39
+ #
40
40
  # column "surname", :string, :rename_to => 'last_name'# Rename_to allows you to rename the column
41
41
  #
42
42
  # column "post_id", :integer, :auto_detect => true # Will run auto detect and make this column a :references => 'posts', :on => 'post_id' for you
43
43
  # # More used when reading a sql database, NOT recommended for use during processing of translation
44
- #
44
+ #
45
45
  # <em>For decimal columns you can specify a few options:</em>
46
46
  # column "total", # This is a default conversion setting.
47
47
  # :decimal,
48
- # :as => 'string'
49
- #
48
+ # :as => 'string'
49
+ #
50
50
  # column "total", # You can specify to convert your decimal to integer
51
51
  # :decimal, # specifying scale will define how many decimal places to keep
52
52
  # :as => 'integer', # Example: :scale => 2 will convert 123.4567 to 12346 in to mongodb
53
53
  # :scale => 2
54
54
  # ==== Decimal Storage
55
- #
55
+ #
56
56
  # Unfortunately MongoDB Ruby Drivers doesn't support BigDecimal, so to ensure all data is stored correctly (without losing information)
57
57
  # I've chosen to store as String, however you can overwrite this functionality in one of two ways:
58
58
  # <em>The reason you would want to do this, is to make this searchable via a query.</em>
59
- #
59
+ #
60
60
  # <b>1) You can specify :as => 'integer', :scale => 2</b>
61
61
  # column "total", :decimal, :as => 'integer', :scale => 2
62
- #
62
+ #
63
63
  # #It would take a value of 123.456 and store it as an integer of value 12346
64
- #
64
+ #
65
65
  # <b>2) You can specify your own custom conversion by doing a {Mongify::Database::Table#before_save}
66
- #
66
+ #
67
67
  # Example:
68
68
  # table "invoice" do
69
69
  # column "name", :string
@@ -72,16 +72,16 @@ module Mongify
72
72
  # row.total = (BigDecimal.new(row.total) * 1000).round
73
73
  # end
74
74
  # end
75
- #
75
+ #
76
76
  # This would take 123.456789 in the total column and convert it to an interger of value 123457 (and in your app you can convert it back to a decimal)
77
- #
77
+ #
78
78
  # *REMEMBER* there is a limit on how big of an integer you can store in BSON/MongoDB (http://bsonspec.org/#/specification)
79
79
  class Column
80
80
  attr_reader :sql_name, :type, :options
81
-
81
+
82
82
  #List of available options for a column
83
83
  AVAILABLE_OPTIONS = ['references', 'ignore', 'rename_to', 'as', 'scale']
84
-
84
+
85
85
  # Auto detects if a column is an :key column or is a reference column
86
86
  def self.auto_detect(column)
87
87
  case column.sql_name.downcase
@@ -92,7 +92,7 @@ module Mongify
92
92
  column.references = $1.to_s.pluralize unless column.referenced?
93
93
  end
94
94
  end
95
-
95
+
96
96
  def initialize(sql_name, type=:string, options={})
97
97
  @sql_name = sql_name
98
98
  options, type = type, nil if type.is_a?(Hash)
@@ -100,21 +100,21 @@ module Mongify
100
100
  @options = options.stringify_keys
101
101
  @auto_detect = @options.delete('auto_detect')
102
102
  run_auto_detect!
103
-
103
+
104
104
  self
105
105
  end
106
-
106
+
107
107
  # Allows you to set a type
108
108
  def type=(value=:string)
109
109
  value = :string if value.nil?
110
110
  @type = value.is_a?(Symbol) ? value : value.to_sym
111
111
  end
112
-
112
+
113
113
  # Returns the no_sql record name
114
114
  def name
115
115
  @name ||= rename_to || sql_name
116
116
  end
117
-
117
+
118
118
  # Returns a translated hash from a given value
119
119
  # Example:
120
120
  # @column = Column.new("surname", :string, :rename_to => 'last_name')
@@ -128,12 +128,12 @@ module Mongify
128
128
  {"#{self.name}" => type_cast(value)}
129
129
  end
130
130
  end
131
-
131
+
132
132
  # Returns a string representation of the column as it would show in a translation file.
133
133
  # Mainly used during print out of translation file
134
134
  def to_print
135
135
  "column \"#{sql_name}\", :#{type}".tap do |output|
136
- output_options = options.map do |k, v|
136
+ output_options = options.map do |k, v|
137
137
  next if v.nil?
138
138
  ":#{k} => #{v.is_a?(Symbol) ? ":#{v}" : %Q["#{v}"] }"
139
139
  end.compact
@@ -141,7 +141,7 @@ module Mongify
141
141
  end
142
142
  end
143
143
  alias :to_s :to_print
144
-
144
+
145
145
  # Sets up a accessor method for an option
146
146
  #
147
147
  # def rename_to=(value)
@@ -172,32 +172,32 @@ module Mongify
172
172
  super(meth, *args, &blk)
173
173
  end
174
174
  end
175
-
175
+
176
176
  # Returns true if the column is a reference column
177
177
  def referenced?
178
178
  !self.options['references'].nil?
179
179
  end
180
-
180
+
181
181
  # Returns true if column is being renamed
182
182
  def renamed?
183
183
  self.name != self.sql_name
184
184
  end
185
-
185
+
186
186
  # Returns true if column is a :key type column
187
187
  def key?
188
188
  self.type == :key
189
189
  end
190
-
190
+
191
191
  # Returns true if column should be auto_detected (passed via options)
192
192
  def auto_detect?
193
193
  !!@auto_detect
194
194
  end
195
-
195
+
196
196
  # Returns true if column is ignored
197
197
  def ignored?
198
198
  !!self.ignore
199
199
  end
200
-
200
+
201
201
  # Used when trying to figure out how to convert a decimal value
202
202
  # @return [String] passed option['as'] or defaults to 'string'
203
203
  def as
@@ -213,7 +213,7 @@ module Mongify
213
213
  def as_integer?
214
214
  self.as == :integer
215
215
  end
216
-
216
+
217
217
  # Get the scale option for decimal to integer conversion
218
218
  # column 'total', :decimal, :as => 'integer', :scale => 3
219
219
  # @return [integer] passed option['scale'] or 0
@@ -229,7 +229,7 @@ module Mongify
229
229
  #######
230
230
  private
231
231
  #######
232
-
232
+
233
233
  # Casts the value to a given type
234
234
  def type_cast(value)
235
235
  return nil if value.nil?
@@ -243,7 +243,7 @@ module Mongify
243
243
  value = ActiveRecord::ConnectionAdapters::Column.value_to_decimal(value)
244
244
  if as_integer?
245
245
  (value * (10 ** self.scale)).round.to_i
246
- else
246
+ else
247
247
  value.to_s
248
248
  end
249
249
  when :datetime then ActiveRecord::ConnectionAdapters::Column.string_to_time(value)
@@ -256,14 +256,14 @@ module Mongify
256
256
  end
257
257
  end
258
258
 
259
-
259
+
260
260
 
261
261
  # runs auto detect (see {Mongify::Database::Column.auto_detect})
262
262
  def run_auto_detect!
263
263
  self.class.auto_detect(self) if auto_detect?
264
264
  end
265
-
266
-
265
+
266
+
267
267
  end
268
268
  end
269
269
  end
@@ -6,28 +6,28 @@ module Mongify
6
6
  hash = {} if hash.nil?
7
7
  @hash = hash.dup.stringify_keys!
8
8
  end
9
-
9
+
10
10
  # See if a given key is included
11
11
  def include?(key)
12
12
  @hash.has_key?(key)
13
13
  end
14
-
14
+
15
15
  # Deletes a given key from the object
16
16
  def delete(key)
17
17
  @hash.delete(key)
18
18
  end
19
-
19
+
20
20
  # Returns a list of available keys
21
21
  def keys
22
22
  @hash.keys
23
23
  end
24
-
24
+
25
25
  # Outputs an hash
26
26
  # This is used to write into the no sql database
27
27
  def to_hash
28
28
  @hash
29
29
  end
30
-
30
+
31
31
  # Used to manually read an attribute
32
32
  def read_attribute(key)
33
33
  @hash[key.to_s]
@@ -36,18 +36,18 @@ module Mongify
36
36
  def write_attribute(key, value)
37
37
  @hash[key.to_s] = value
38
38
  end
39
-
39
+
40
40
  # Passes Inspect onto the internal hash
41
41
  def inspect
42
42
  @hash.inspect
43
43
  end
44
-
44
+
45
45
  # Updated respond_to to return true if it's a key the hash
46
46
  def respond_to?(method)
47
47
  return true if @hash.has_key?(method.gsub('=', ''))
48
48
  super(method)
49
49
  end
50
-
50
+
51
51
  # Added the ability to read and write attributes in the hash
52
52
  def method_missing(meth, *args, &blk)
53
53
  match = meth.to_s.match(/^([a-zA-Z\_]+)(=|$)$/)
@@ -62,7 +62,7 @@ module Mongify
62
62
  super(meth, *args, &blk)
63
63
  end
64
64
  end
65
-
65
+
66
66
  end
67
67
  end
68
68
  end
@@ -3,15 +3,15 @@ module Mongify
3
3
  module Database
4
4
  #
5
5
  # No Sql Connection configuration
6
- #
6
+ #
7
7
  # Basic format should look something like this:
8
- #
8
+ #
9
9
  # no_sql_connection {options} do
10
10
  # adapter "mongodb"
11
11
  # host "localhost"
12
12
  # database "my_database"
13
13
  # end
14
- #
14
+ #
15
15
  # Possible attributes:
16
16
  # adapter
17
17
  # host
@@ -19,106 +19,132 @@ module Mongify
19
19
  # username
20
20
  # password
21
21
  # port
22
- #
22
+ #
23
23
  # Options:
24
- # :force => true # This will force a database drop before processing
24
+ # :force => true # This will force a database drop before processing
25
25
  # <em>You're also able to set attributes via the options</em>
26
26
  #
27
27
  class NoSqlConnection < Mongify::Database::BaseConnection
28
28
  include Mongo
29
-
30
-
29
+
30
+
31
31
  #Required fields for a no sql connection
32
- REQUIRED_FIELDS = %w{host database}
33
-
32
+ REQUIRED_FIELDS = %w{host database}
33
+
34
34
  def initialize(options={})
35
35
  super options
36
36
  @options = options
37
- adapter 'mongodb' if adapter.nil? || adapter == 'mongo'
37
+ adapter 'mongodb' if adapter.nil? || adapter.downcase == "mongo"
38
38
  end
39
-
39
+
40
40
  # Sets and/or returns a adapter
41
41
  # It takes care of renaming adapter('mongo') to 'mongodb'
42
42
  def adapter(name=nil)
43
- name = 'mongodb' if name && name.to_s.downcase == 'mongo'
44
43
  super(name)
45
44
  end
46
-
45
+
47
46
  # Returns a connection string that can be used to build a Mongo Connection
48
47
  # (Currently this isn't used due to some issue early on in development)
49
48
  def connection_string
50
49
  "#{@adapter}://#{@host}#{":#{@port}" if @port}"
51
50
  end
52
-
53
- # Returns true or false depending if the given attributes are present and valid to make up a
51
+
52
+ # Returns true or false depending if the given attributes are present and valid to make up a
54
53
  # connection to a mongo server
55
54
  def valid?
56
55
  super && @database.present?
57
56
  end
58
-
57
+
59
58
  # Returns true if :force was set to true
60
59
  # This will force a drop of the database upon connection
61
60
  def forced?
62
61
  !!@options['force']
63
62
  end
64
-
63
+
65
64
  # Sets up a connection to the database
66
65
  def setup_connection_adapter
67
66
  connection = Connection.new(host, port)
68
67
  connection.add_auth(database, username, password) if username && password
69
68
  connection
70
69
  end
71
-
70
+
72
71
  # Returns a mongo connection
73
72
  # NOTE: If forced? is true, the first time a connection is made, it will ask to drop the
74
- # database before continuing
73
+ # database before continuing
75
74
  def connection
76
75
  return @connection if @connection
77
76
  @connection = setup_connection_adapter
78
77
  @connection
79
78
  end
80
-
79
+
81
80
  # Returns true or false depending if we have a connection to a mongo server
82
81
  def has_connection?
83
82
  connection.connected?
84
83
  end
85
-
84
+
86
85
  # Returns the database from the connection
87
86
  def db
88
87
  @db ||= connection[database]
89
88
  end
90
-
89
+
91
90
  # Returns a hash of all the rows from the database of a given collection
92
91
  def select_rows(collection)
93
92
  db[collection].find
94
93
  end
95
-
94
+
95
+ def select_by_query(collection, query)
96
+ db[collection].find(query)
97
+ end
98
+
96
99
  # Inserts into the collection a given row
97
100
  def insert_into(colleciton_name, row)
98
101
  db[colleciton_name].insert(row)
99
102
  end
100
-
103
+
101
104
  # Updates a collection item with a given ID with the given attributes
102
105
  def update(colleciton_name, id, attributes)
103
106
  db[colleciton_name].update({"_id" => id}, attributes)
104
107
  end
105
-
108
+
109
+ # Upserts into the collection a given row
110
+ def upsert(collection_name, row)
111
+ # We can't use the save method of the Mongo collection
112
+ # The reason is that it detects duplicates using _id
113
+ # but we should detect it using pre_mongified_id instead
114
+ # because in the case of sync, same rows are identified by their original sql ids
115
+ #
116
+ # db[collection_name].save(row)
117
+
118
+ if row.has_key?(:pre_mongified_id) || row.has_key?('pre_mongified_id')
119
+ id = row[:pre_mongified_id] || row['pre_mongified_id']
120
+ duplicate = find_one(collection_name, {"pre_mongified_id" => id})
121
+ if duplicate
122
+ update(collection_name, duplicate[:_id] || duplicate["_id"], row)
123
+ else
124
+ insert_into(collection_name, row)
125
+ end
126
+ else
127
+ # no pre_mongified_id, fallback to the upsert method of Mongo
128
+ db[collection_name].save(row)
129
+ end
130
+ end
131
+
106
132
  # Finds one item from a collection with the given query
107
133
  def find_one(collection_name, query)
108
134
  db[collection_name].find_one(query)
109
135
  end
110
-
136
+
111
137
  # Returns a row of a item from a given collection with a given pre_mongified_id
112
138
  def get_id_using_pre_mongified_id(colleciton_name, pre_mongified_id)
113
139
  db[colleciton_name].find_one('pre_mongified_id' => pre_mongified_id).try(:[], '_id')
114
140
  end
115
-
141
+
116
142
  # Removes pre_mongified_id from all records in a given collection
117
143
  def remove_pre_mongified_ids(collection_name)
118
144
  drop_mongified_index(collection_name)
119
145
  db[collection_name].update({}, { '$unset' => { 'pre_mongified_id' => 1} }, :multi => true)
120
146
  end
121
-
147
+
122
148
  # Removes pre_mongified_id from collection
123
149
  # @param [String] collection_name name of collection to remove the index from
124
150
  # @return True if successful
@@ -126,13 +152,13 @@ module Mongify
126
152
  def drop_mongified_index(collection_name)
127
153
  db[collection_name].drop_index('pre_mongified_id_1') if db[collection_name].index_information.keys.include?("pre_mongified_id_1")
128
154
  end
129
-
155
+
130
156
  # Creates a pre_mongified_id index to ensure
131
157
  # speedy lookup for collections via the pre_mongified_id
132
158
  def create_pre_mongified_id_index(collection_name)
133
159
  db[collection_name].create_index([['pre_mongified_id', Mongo::ASCENDING]])
134
160
  end
135
-
161
+
136
162
  # Asks user permission to drop the database
137
163
  # @return true or false depending on user's response
138
164
  def ask_to_drop_database
@@ -140,18 +166,18 @@ module Mongify
140
166
  drop_database
141
167
  end
142
168
  end
143
-
169
+
144
170
  #######
145
171
  private
146
172
  #######
147
-
173
+
148
174
  # Drops the mongodb database
149
175
  def drop_database
150
176
  connection.drop_database(database)
151
177
  end
152
-
153
178
 
154
-
179
+
180
+
155
181
  end
156
182
  end
157
- end
183
+ end