mongify 0.0.4

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 (63) hide show
  1. data/.gitignore +20 -0
  2. data/CHANGELOG.rdoc +22 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +68 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +86 -0
  7. data/Rakefile +73 -0
  8. data/bin/mongify +14 -0
  9. data/features/options.feature +46 -0
  10. data/features/print.feature +10 -0
  11. data/features/process.feature +19 -0
  12. data/features/step_definitions/mongify_steps.rb +42 -0
  13. data/features/step_definitions/mongo_steps.rb +7 -0
  14. data/features/support/env.rb +28 -0
  15. data/lib/mongify.rb +22 -0
  16. data/lib/mongify/cli.rb +8 -0
  17. data/lib/mongify/cli/application.rb +43 -0
  18. data/lib/mongify/cli/help_command.rb +16 -0
  19. data/lib/mongify/cli/options.rb +94 -0
  20. data/lib/mongify/cli/report.rb +11 -0
  21. data/lib/mongify/cli/version_command.rb +18 -0
  22. data/lib/mongify/cli/worker_command.rb +93 -0
  23. data/lib/mongify/configuration.rb +46 -0
  24. data/lib/mongify/database.rb +5 -0
  25. data/lib/mongify/database/base_connection.rb +78 -0
  26. data/lib/mongify/database/column.rb +81 -0
  27. data/lib/mongify/database/no_sql_connection.rb +62 -0
  28. data/lib/mongify/database/sql_connection.rb +61 -0
  29. data/lib/mongify/database/table.rb +74 -0
  30. data/lib/mongify/exceptions.rb +13 -0
  31. data/lib/mongify/translation.rb +55 -0
  32. data/lib/mongify/translation/printer.rb +20 -0
  33. data/lib/mongify/translation/process.rb +61 -0
  34. data/lib/mongify/ui.rb +45 -0
  35. data/lib/mongify/version.rb +3 -0
  36. data/mongify.gemspec +42 -0
  37. data/spec/default.watch +199 -0
  38. data/spec/files/base_configuration.rb +9 -0
  39. data/spec/files/empty_translation.rb +0 -0
  40. data/spec/files/simple_translation.rb +26 -0
  41. data/spec/mongify/cli/application_spec.rb +19 -0
  42. data/spec/mongify/cli/help_command_spec.rb +18 -0
  43. data/spec/mongify/cli/options_spec.rb +62 -0
  44. data/spec/mongify/cli/version_command_spec.rb +24 -0
  45. data/spec/mongify/cli/worker_command_spec.rb +115 -0
  46. data/spec/mongify/configuration_spec.rb +25 -0
  47. data/spec/mongify/database/base_connection_spec.rb +59 -0
  48. data/spec/mongify/database/column_spec.rb +103 -0
  49. data/spec/mongify/database/no_sql_connection_spec.rb +131 -0
  50. data/spec/mongify/database/sql_connection_spec.rb +91 -0
  51. data/spec/mongify/database/table_spec.rb +120 -0
  52. data/spec/mongify/translation/printer_spec.rb +34 -0
  53. data/spec/mongify/translation/process_spec.rb +68 -0
  54. data/spec/mongify/translation_spec.rb +59 -0
  55. data/spec/mongify/ui_spec.rb +73 -0
  56. data/spec/mongify_spec.rb +15 -0
  57. data/spec/spec.opts +1 -0
  58. data/spec/spec_helper.rb +22 -0
  59. data/spec/support/config_reader.rb +21 -0
  60. data/spec/support/database.example +17 -0
  61. data/spec/support/database_output.txt +27 -0
  62. data/spec/support/generate_database.rb +91 -0
  63. metadata +370 -0
@@ -0,0 +1,81 @@
1
+ module Mongify
2
+ module Database
3
+ #
4
+ # A column in the sql table
5
+ #
6
+ class Column
7
+ attr_reader :name, :type, :options
8
+
9
+ AVAILABLE_OPTIONS = ['references', 'default']
10
+
11
+ def initialize(name, type=:string, *args)
12
+ @name = name
13
+ type = :string if type.nil?
14
+ @type = type.is_a?(Symbol) ? type : type.to_sym
15
+ @options = args.extract_options!.stringify_keys
16
+
17
+ auto_detect!
18
+
19
+ self
20
+ end
21
+
22
+ def translate(value)
23
+ case type
24
+ when :key
25
+ {"pre_mongified_#{name}" => value}
26
+ when :datetime
27
+ {"#{name}" => value.to_time}
28
+ else
29
+ {"#{self.name}" => value}
30
+ end
31
+ end
32
+
33
+ def to_print
34
+ "column \"#{name}\", :#{type}".tap do |output|
35
+ output_options = options.map{|k, v| (v == nil) ? nil : ":#{k} => \"#{v}\""}.compact
36
+ output << ", #{output_options.join(', ')}" unless output_options.blank?
37
+ end
38
+ end
39
+
40
+ def method_missing(meth, *args, &blk)
41
+ method_name = meth.to_s.gsub("=", '')
42
+ if AVAILABLE_OPTIONS.include?(method_name)
43
+ class_eval <<-EOF
44
+ def #{method_name}=(value)
45
+ options['#{method_name}'] = value
46
+ end
47
+ def #{method_name}
48
+ options['#{method_name}']
49
+ end
50
+ EOF
51
+ send(meth, *args, &blk)
52
+ else
53
+ super(meth, *args, &blk)
54
+ end
55
+ end
56
+
57
+ def reference?
58
+ !self.options['references'].nil?
59
+ end
60
+
61
+ #######
62
+ private
63
+ #######
64
+
65
+ def key?
66
+ self.type == :key
67
+ end
68
+
69
+ def auto_detect!
70
+ case name.downcase
71
+ when 'id'
72
+ @type = :key if self.type == :integer
73
+ when /(.*)_id/
74
+ self.references = $1.to_s.pluralize unless self.references
75
+ end
76
+ end
77
+
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,62 @@
1
+ require 'mongo'
2
+ module Mongify
3
+ module Database
4
+ #
5
+ # No sql connection configuration
6
+ #
7
+ class NoSqlConnection < Mongify::Database::BaseConnection
8
+ include Mongo
9
+ REQUIRED_FIELDS = %w{host database}
10
+
11
+ def initialize(options=nil)
12
+ super options
13
+ adapter 'mongodb' if adapter.nil? || adapter == 'mongo'
14
+ end
15
+
16
+ def connection_string
17
+ "#{@adapter}://#{@host}#{":#{@port}" if @port}"
18
+ end
19
+
20
+ def valid?
21
+ @database.present? && @host.present?
22
+ end
23
+
24
+ def connection
25
+ return @connection if @connection
26
+ @connection = Connection.new(host, port)
27
+ @connection.add_auth(database, username, password) if username && password
28
+ @connection
29
+ end
30
+
31
+ def has_connection?
32
+ connection.connected?
33
+ end
34
+
35
+ def db
36
+ @db ||= connection[database]
37
+ end
38
+
39
+ def select_rows(collection)
40
+ db[collection].find
41
+ end
42
+
43
+ def insert_into(colleciton_name, row)
44
+ db[colleciton_name].insert(row, :safe => true)
45
+ end
46
+
47
+ def update(colleciton_name, id, attributes)
48
+ db[colleciton_name].update({"_id" => id}, attributes)
49
+ end
50
+
51
+ def get_id_using_pre_mongified_id(colleciton_name, pre_mongified_id)
52
+ db[colleciton_name].find_one('pre_mongified_id' => pre_mongified_id).try(:[], '_id')
53
+ end
54
+
55
+
56
+ def reset!
57
+ @connection = nil
58
+ @db = nil
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,61 @@
1
+ module Mongify
2
+ module Database
3
+ #
4
+ # Sql connection configuration
5
+ #
6
+ class SqlConnection < Mongify::Database::BaseConnection
7
+
8
+ REQUIRED_FIELDS = %w{host adapter database}
9
+
10
+ def initialize(options=nil)
11
+ @prefixed_db = false
12
+ super(options)
13
+ end
14
+
15
+ def setup_connection_adapter
16
+ ActiveRecord::Base.establish_connection(self.to_hash)
17
+ end
18
+
19
+ def valid?
20
+ return false unless @adapter
21
+ if sqlite_adapter?
22
+ return true if @database
23
+ else
24
+ return super
25
+ end
26
+ false
27
+ end
28
+
29
+ def has_connection?
30
+ setup_connection_adapter
31
+ connection.send(:connect) if ActiveRecord::Base.connection.respond_to?(:connect)
32
+ true
33
+ end
34
+
35
+ def connection
36
+ return nil unless has_connection?
37
+ ActiveRecord::Base.connection
38
+ end
39
+
40
+ def tables
41
+ return nil unless has_connection?
42
+ self.connection.tables
43
+ end
44
+
45
+ def columns_for(table_name)
46
+ self.connection.columns(table_name)
47
+ end
48
+
49
+ def select_rows(table_name)
50
+ self.connection.select_all("SELECT * FROM #{table_name}")
51
+ end
52
+
53
+ #######
54
+ private
55
+ #######
56
+ def sqlite_adapter?
57
+ @adapter && (@adapter.downcase == 'sqlite' || @adapter.downcase == 'sqlite3')
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,74 @@
1
+ module Mongify
2
+ module Database
3
+ #
4
+ # A representation of a sql table and how it should map to a no_sql system
5
+ #
6
+ class Table
7
+
8
+ attr_accessor :name
9
+ attr_reader :options, :columns
10
+
11
+ def initialize(name, *args, &block)
12
+ @columns = []
13
+ @column_lookup = {}
14
+ @options = args.extract_options!.stringify_keys
15
+ self.name = name
16
+
17
+ self.instance_exec(&block) if block_given?
18
+
19
+ import_columns
20
+
21
+ self
22
+ end
23
+
24
+ #Add a Database Column
25
+ def add_column(column)
26
+ raise Mongify::DatabaseColumnExpected, "Expected a Mongify::Database::Column" unless column.is_a?(Mongify::Database::Column)
27
+ add_column_index(column.name, @columns.size)
28
+ @columns << column
29
+ end
30
+
31
+
32
+ def column(name, type=nil, options={})
33
+ options = type and type = nil if type.is_a?(Hash)
34
+ type = type.to_sym if type
35
+ add_column_index(name.to_s.downcase, @columns.size)
36
+ @columns << (col = Mongify::Database::Column.new(name, type, options))
37
+ col
38
+ end
39
+
40
+ def find_column(name)
41
+ return nil unless (index = @column_lookup[name.to_s.downcase])
42
+ @columns[index]
43
+ end
44
+
45
+
46
+ def reference_columns
47
+ @columns.reject{ |c| !c.reference? }
48
+ end
49
+
50
+ def translate(row)
51
+ new_row = {}
52
+ row.each do |key, value|
53
+ c = find_column(key)
54
+ new_row.merge!(c.present? ? c.translate(value) : {"#{key}" => value})
55
+ end
56
+ new_row
57
+ end
58
+
59
+ #######
60
+ private
61
+ #######
62
+
63
+ def add_column_index(name, index)
64
+ @column_lookup[name] = index
65
+ end
66
+
67
+ def import_columns
68
+ return unless import_columns = @options.delete('columns')
69
+ import_columns.each { |c| add_column(c) }
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,13 @@
1
+ module Mongify
2
+ # File Not Found Exception
3
+ class FileNotFound < RuntimeError; end
4
+ class ConfigurationFileNotFound < FileNotFound; end
5
+ class TranslationFileNotFound < FileNotFound; end
6
+
7
+ class ConfigurationError < RuntimeError; end
8
+ class SqlConnectionRequired < ConfigurationError; end
9
+ class NoSqlConnectionRequired < ConfigurationError; end
10
+ class DatabaseColumnExpected < ConfigurationError; end
11
+
12
+ class RootMissing < RuntimeError; end
13
+ end
@@ -0,0 +1,55 @@
1
+ require 'mongify/translation/printer'
2
+ require 'mongify/translation/process'
3
+ module Mongify
4
+ #
5
+ # Actually runs the translation from sql to no sql
6
+ #
7
+ class Translation
8
+ attr_reader :tables, :sql_connection, :no_sql_connection
9
+
10
+ include Printer
11
+ include Process
12
+ class << self
13
+ def parse(file_name)
14
+ translation = self.new
15
+ translation.instance_eval(File.read(file_name))
16
+ translation
17
+ end
18
+
19
+ def load(connection)
20
+ raise Mongify::SqlConnectionRequired, "Can only read from Mongify::Database::SqlConnection" unless connection.is_a?(Mongify::Database::SqlConnection)
21
+ return unless connection.has_connection?
22
+ translation = self.new
23
+ connection.tables.each do |t|
24
+ columns = []
25
+ connection.columns_for(t).each do |ar_col|
26
+ columns << Mongify::Database::Column.new(ar_col.name, ar_col.type, :default => ar_col.default)
27
+ end
28
+ translation.table(t, :columns => columns)
29
+ end
30
+ translation
31
+ end
32
+ end
33
+
34
+
35
+ def initialize
36
+ @tables = []
37
+ end
38
+
39
+ def table(table_name, options={}, &block)
40
+ table = Mongify::Database::Table.new(table_name, options, &block)
41
+ @tables << table
42
+ end
43
+
44
+ def add_table(table)
45
+ @tables << table
46
+ end
47
+
48
+ #######
49
+ private
50
+ #######
51
+
52
+
53
+
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ module Mongify
2
+ #
3
+ # Actually runs the translation from sql to no sql
4
+ #
5
+ class Translation
6
+ module Printer
7
+ def print
8
+ ''.tap do |output|
9
+ @tables.each do |t|
10
+ output << %Q[table "#{t.name}" do\n]
11
+ t.columns.each do |c|
12
+ output << "\t#{c.to_print}\n"
13
+ end
14
+ output << "end\n\n"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,61 @@
1
+ module Mongify
2
+ #
3
+ # This will take the Translation and do the processing on it
4
+ #
5
+ class Translation
6
+ module Process
7
+ def sql_connection=(value)
8
+ @sql_connection=value
9
+ end
10
+ def no_sql_connection=(value)
11
+ @no_sql_connection=value
12
+ end
13
+ def process(sql_connection, no_sql_connection)
14
+ raise Mongify::SqlConnectionRequired, "Can only read from Mongify::Database::SqlConnection" unless sql_connection.is_a?(Mongify::Database::SqlConnection)
15
+ raise Mongify::NoSqlConnectionRequired, "Can only write to Mongify::Database::NoSqlConnection" unless no_sql_connection.is_a?(Mongify::Database::NoSqlConnection)
16
+
17
+ self.sql_connection = sql_connection
18
+ raise "SQL Connection is not valid" unless self.sql_connection.valid?
19
+ self.no_sql_connection = no_sql_connection
20
+ raise "noSql Connection is not valid" unless self.no_sql_connection.valid?
21
+
22
+ copy
23
+ update_reference_ids
24
+ nil
25
+ end
26
+
27
+ #######
28
+ private
29
+ #######
30
+
31
+ def copy
32
+ self.tables.each do |t|
33
+ sql_connection.select_rows(t.name).each do |row|
34
+ no_sql_connection.insert_into(t.name, t.translate(row))
35
+ end
36
+ end
37
+ end
38
+
39
+ def update_reference_ids
40
+ self.tables.each do |t|
41
+ no_sql_connection.select_rows(t.name).each do |row|
42
+ id = row["_id"]
43
+ attributes = {}
44
+ t.reference_columns.each do |c|
45
+ new_id = no_sql_connection.get_id_using_pre_mongified_id(c.references.to_s, row[c.name])
46
+ attributes.merge!(c.name => new_id) unless new_id.nil?
47
+ end
48
+ no_sql_connection.update(t.name, id, {"$set" => attributes}) unless attributes.blank?
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+
57
+
58
+ # Process that needs to take place
59
+ # import the data (moving the id to premongified_id)
60
+ # fix all the references to the new ids
61
+ #
@@ -0,0 +1,45 @@
1
+ module Mongify
2
+ #
3
+ # Used to output messages to the UI
4
+ #
5
+ class UI
6
+ class << self
7
+ def puts(msg)
8
+ out_stream.puts(msg) if out_stream
9
+ end
10
+
11
+ def print(msg)
12
+ out_stream.print(msg) if out_stream
13
+ end
14
+
15
+ def gets
16
+ in_stream ? in_stream.gets : ''
17
+ end
18
+
19
+ def request(msg)
20
+ print(msg)
21
+ gets.chomp
22
+ end
23
+
24
+ def ask(msg)
25
+ request("#{msg} [yn] ") == 'y'
26
+ end
27
+
28
+ def warn(msg)
29
+ puts "WARNING: #{msg}"
30
+ end
31
+
32
+ def abort(message)
33
+ UI.puts "PROGRAM HALT: #{message}"
34
+ Kernel.abort message
35
+ end
36
+
37
+ def in_stream
38
+ Configuration.in_stream
39
+ end
40
+ def out_stream
41
+ Configuration.out_stream
42
+ end
43
+ end
44
+ end
45
+ end