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
@@ -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