oracle_to_mysql 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.
@@ -0,0 +1,45 @@
1
+ class OracleToMysql::Command
2
+ attr_accessor :client_class
3
+ def info(msg)
4
+ self.output("[info] #{msg}")
5
+ end
6
+ def error(msg)
7
+ self.output("[ERROR] #{msg}")
8
+ raise OracleToMysql::CommandError.new("#{self.client_class.to_s}#otm_execute died on command=#{self.class.to_s}")
9
+ end
10
+ def warn(msg)
11
+ self.output("[WARN] #{msg}")
12
+ end
13
+
14
+ def started(msg='')
15
+ self.output("[started t=#{self.client_class.otm_time_elapsed_since_otm_timestamp}]#{msg}")
16
+ end
17
+
18
+ def finished(msg='')
19
+ self.output("[finished t=#{self.client_class.otm_time_elapsed_since_otm_timestamp}]#{msg}")
20
+ end
21
+
22
+ # USE THE ones above if possible, it makes the output more uniform and easier to read/parse
23
+ # Stuff funnels to here, which propogates up to the client class
24
+ def output(msg)
25
+ self.client_class.otm_output("[#{self.class.to_s}]#{msg}")
26
+ end
27
+
28
+ # Client classes call this method
29
+ def execute
30
+ self.started
31
+ self.execute_internal
32
+ self.finished
33
+ end
34
+
35
+ # Commands should override this if they use temp files that need to be cleaned up
36
+ # The cleanup_temp_files_and_tables command reflects on all commands to find and delete things
37
+ # referenced in here
38
+ def temp_file_symbols
39
+ []
40
+ end
41
+
42
+ def execute_internal
43
+ raise "CHILDREN MUST OVERRIDE THIS"
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ module OracleToMysql
2
+ class Command
3
+ class DeleteTempFiles < OracleToMysql::Command
4
+ def execute_internal
5
+ self.client_class.otm_all_temp_files.each do |temp_file|
6
+ begin
7
+ self.info("Deleting temp file, #{temp_file}")
8
+ File.unlink(temp_file)
9
+ rescue Errno::ENOENT => e
10
+ self.warn("Could not remove temp file #{temp_file}")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,61 @@
1
+ module OracleToMysql
2
+ class Command
3
+ class ForkAndExecuteSqlplusCommand < OracleToMysql::Command
4
+ def command_line_invocation
5
+ username = self.client_class.otm_source_config_hash['username']
6
+ password = self.client_class.otm_source_config_hash['password']
7
+ tns = OracleToMysql.tns_string_from_config(self.client_class.otm_source_config_hash) #self.client_class.otm_source_config_hash['tns']
8
+ "sqlplus -S '#{username}/#{password}@#{tns}' @#{self.client_class.otm_get_file_name_for(:oracle_commands)}"
9
+ end
10
+
11
+ # overridden from parent to help CleanupTempFilesAndTables do it's job
12
+ #
13
+ def temp_file_symbols
14
+ [:oracle_output]
15
+ end
16
+
17
+ def execute_internal
18
+ stderr_str = ""
19
+ stdout_str = ""
20
+ the_command = self.command_line_invocation
21
+ self.started("sqlplus child being spawned")
22
+ unless
23
+ Open4::popen4(the_command) { |pid, stdin, stdout, stderr|
24
+ stderr_str = stderr.read
25
+ stdout_str = stdout.read
26
+ true
27
+ }
28
+ self.error("Could not execute #{the_command}")
29
+ # raise "[#{self.to_s}][create_#{deriv_type.to_s}_with_convert][MBF##{mbf.id.to_s}] Couldn't execute #{convert_cmd}"
30
+ end
31
+ self.finished("sqlplus process terminated")
32
+
33
+ if stderr_str.length > 0
34
+ self.error("sqlplus had stderr output: #{stderr_str}")
35
+ end
36
+ if stdout_str.length > 0
37
+ self.warn("sqlplus had stdout output: #{stdout_str}")
38
+ end
39
+
40
+ if File.exists?(self.client_class.otm_get_file_name_for(:oracle_output))
41
+ spooled_file_size = File.size(self.client_class.otm_get_file_name_for(:oracle_output))
42
+ sqlplus_file_size = File.size(self.client_class.otm_get_file_name_for(:oracle_commands))
43
+
44
+ # This sqlplus file will have the oracle message
45
+ if spooled_file_size > sqlplus_file_size
46
+ self.info("sqlplus spooled #{spooled_file_size} bytes of output to #{self.client_class.otm_get_file_name_for(:oracle_output)}")
47
+ else
48
+ source_output_contents = IO.read(self.client_class.otm_get_file_name_for(:oracle_output))
49
+ self.warn("tiny data output: smaller than the sqlplus commands file, #{spooled_file_size} bytes, it might contains errors rather than data, I will check for Oracle error strings in #{self.client_class.otm_get_file_name_for(:oracle_output)} and die if so")
50
+ if source_output_contents.match(/^ERROR at line/) && source_output_contents.match(/^ORA-/)
51
+ self.error("sqlplus error: #{self.client_class.otm_get_file_name_for(:oracle_output)} contains both \"ERROR at line\" and \"ORA-\" in it, check it out, contents=#{source_output_contents}")
52
+ end
53
+ end
54
+ else
55
+ self.error("#{self.client_class.otm_get_file_name_for(:oracle_output)} does not exist")
56
+ end
57
+ self.info("sqlplus returned successfully")
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,136 @@
1
+ module OracleToMysql
2
+ class Command
3
+ class WriteAndExecuteMysqlCommandsToBashFile < OracleToMysql::Command
4
+
5
+ def tables_to_retain
6
+ self.client_class.otm_retain_options[:n]
7
+ end
8
+
9
+ # Done this way so the child, _in_replace_mode, can override this
10
+ #
11
+ def mysql_command_order
12
+ [:execute_otm_target_sql,
13
+ :execute_temp_file_modded_otm_target_sql,
14
+ :load_data_infile,
15
+ :drop_expired_retained_tables,
16
+ :perform_atomic_rename_of_tables
17
+ ]
18
+ end
19
+
20
+ ########## MYSQL queryies/commands BEGIN
21
+
22
+ def execute_otm_target_sql
23
+ self.client_class.otm_target_sql
24
+ end
25
+
26
+ # The client class should not have to know about otm_temp_target_table
27
+ # this overrides it just for this instance so the otm_target_sql
28
+ # gets the temp table name interpolated into it instead of the actual
29
+ # this is done this way to support the non-atomic rename strategy as well (which does not create a temp table)
30
+ #
31
+ def execute_temp_file_modded_otm_target_sql
32
+ the_modded_client_class_inst = self.client_class.clone
33
+ the_modded_client_class_inst.instance_eval(<<-EOS, __FILE__,__LINE__)
34
+ def otm_target_table
35
+ "#{self.client_class.otm_temp_target_table}"
36
+ end
37
+ EOS
38
+ the_modded_client_class_inst.otm_target_sql
39
+ end
40
+
41
+ def load_data_infile
42
+ "-- Rip through the oracle output data and insert into mysql
43
+ load data local infile '#{self.client_class.otm_get_file_name_for(:oracle_output)}'
44
+ into table #{self.client_class.otm_temp_target_table}
45
+ fields terminated by '\\t'
46
+ lines terminated by '\\n'
47
+ "
48
+ end
49
+
50
+ def create_actual_target_table_if_it_doesnt_exist
51
+ # If this is the first run, the destination table won't exist yet. If that's the
52
+ # case, create an empty table so the atomic rename works
53
+ "create table if not exists #{self.client_class.otm_target_table}
54
+ select * from #{self.client_class.otm_temp_target_table} where 1=0"
55
+ end
56
+
57
+ def drop_expired_retained_tables
58
+ raise "TODO: not implemented yet for retention :n != 1" if tables_to_retain != 1
59
+ "drop table if exists #{self.client_class.otm_retained_target_table(tables_to_retain)}"
60
+ end
61
+
62
+ def perform_atomic_rename_of_tables
63
+ raise "TODO: not implemented yet for retention :n != 1" if tables_to_retain != 1
64
+ "RENAME table
65
+ #{self.client_class.otm_target_table} TO #{self.client_class.otm_retained_target_table(tables_to_retain)},
66
+ #{self.client_class.otm_temp_target_table} TO #{self.client_class.otm_target_table}"
67
+ # rename table #{self.otm_retained_target_table}
68
+ #
69
+ #
70
+ # #{self.client_class.otm_target_table} to #{self.client_class.otm_target_table}_old, #{self.client_class.otm_temp_target_table} to #{self.client_class.otm_target_table};
71
+ #
72
+ end
73
+
74
+
75
+ ########## MYSQL queryies/commands END
76
+
77
+ # -- Reflect the table retain options
78
+ # #{drop_expired_retained_tables_sql};
79
+
80
+ def the_mysql_commands
81
+ the_queries = ''
82
+ self.mysql_command_order.each do |mysql_command|
83
+ the_queries << "-- #{mysql_command.to_s}\n" # to make debugging easier
84
+ the_queries << self.send(mysql_command)
85
+ the_queries << ";\n"
86
+ end
87
+ the_queries
88
+ end
89
+
90
+ def command_line_invocation
91
+ h = self.client_class.otm_target_config_hash
92
+ "mysql -u'#{h['username']}' -h'#{h['host']}' -p'#{h['password']}' #{h['database']} < '#{self.client_class.otm_get_file_name_for(:mysql_commands)}'"
93
+ end
94
+
95
+ # overridden from parent to help CleanupTempFilesAndTables do it's job
96
+ #
97
+ def temp_file_symbols
98
+ [:mysql_commands]
99
+ end
100
+
101
+ def execute_internal
102
+ bytes_written = 0
103
+ the_mysql_commands_file = self.client_class.otm_get_file_name_for(:mysql_commands)
104
+ File.open(the_mysql_commands_file,'w') do |f|
105
+ bytes_written = f.write(self.the_mysql_commands)
106
+ end
107
+ if bytes_written > 0
108
+ self.info("#{bytes_written} bytes written to #{the_mysql_commands_file}")
109
+ else
110
+ self.error("Could not write to #{the_mysql_commands_file}")
111
+ end
112
+
113
+ stderr_str = ""
114
+ stdout_str = ""
115
+ the_command = self.command_line_invocation
116
+ self.started("mysql child being spawned")
117
+ unless
118
+ Open4::popen4(the_command) { |pid, stdin, stdout, stderr|
119
+ stderr_str = stderr.read
120
+ stdout_str = stdout.read
121
+ true
122
+ }
123
+ self.error("Could not execute #{the_command}")
124
+ end
125
+ self.finished("mysql child terminated")
126
+
127
+ if stderr_str.length > 0
128
+ self.error("mysql child process had stderr output: #{stderr_str}")
129
+ end
130
+ if stdout_str.length > 0
131
+ self.warn("mysql child process: #{stdout_str}")
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,51 @@
1
+ module OracleToMysql
2
+ class Command
3
+ class WriteAndExecuteMysqlCommandsToBashFileInReplaceMode < OracleToMysql::Command::WriteAndExecuteMysqlCommandsToBashFile
4
+
5
+ # OVERRIDDEN
6
+ def mysql_command_order
7
+ [:execute_otm_target_sql,
8
+ :drop_expired_retained_tables, # defined in parent
9
+ :retention_policy_create_and_populate_tables,
10
+ :load_data_infile # overridden in this class
11
+ ]
12
+ end
13
+
14
+ ### MYSQL COMMANDS BEGIN
15
+
16
+ def retention_policy_create_and_populate_tables
17
+ raise "TODO: not implemented yet for retention :n != 1" if self.tables_to_retain != 1
18
+ the_modded_client_class_inst = self.client_class.clone
19
+ the_modded_client_class_inst.instance_eval(<<-EOS, __FILE__,__LINE__)
20
+ def otm_target_table
21
+ "#{self.client_class.otm_retained_target_table(tables_to_retain)}"
22
+ end
23
+ EOS
24
+
25
+ return_this = ''
26
+ return_this << the_modded_client_class_inst.otm_target_sql
27
+ return_this << ";\n"
28
+ return_this << "-- Insert existing rows into the retention table\n"
29
+ return_this << "INSERT INTO #{the_modded_client_class_inst.otm_target_table} SELECT * FROM #{self.client_class.otm_target_table}\n"
30
+ return_this
31
+ end
32
+
33
+ # Mostly the same as parent class except:
34
+ # * the replace key word which will replace existing rows on the target when there are mysql duplicate key errors
35
+ # possible todo: support "ignore" instead of "replace" (i have no reason to do this)
36
+ # * it copied into the otm_target_table directly rather than the temp table
37
+ #
38
+ def load_data_infile
39
+ "-- Rip through the oracle output data and insert into mysql
40
+ load data local infile '#{self.client_class.otm_get_file_name_for(:oracle_output)}'
41
+ replace
42
+ into table #{self.client_class.otm_target_table}
43
+ fields terminated by '\\t'
44
+ lines terminated by '\\n'
45
+ "
46
+ end
47
+
48
+ ### MYSQL COMMANDS END
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,64 @@
1
+ module OracleToMysql
2
+ class Command
3
+ class WriterSqlplusCommandsToFile < OracleToMysql::Command
4
+ class MaxLineExceeded < Exception; end
5
+ # Oracle and sqlplus is so finicky, this captures some of the errors I've come across,
6
+ # it is not exhuastive
7
+ def validate_otm_source_sql
8
+ the_source_sql = self.client_class.otm_source_sql
9
+
10
+ # last character minus white space must be a ";"
11
+ raise "otm_source_sql not terminated with a ';'" if the_source_sql.strip[-1..-1] != ';'
12
+
13
+ # Detect and prevent sqlplus from SP2-0027: Input is too long (> 2499 characters) - line ignored
14
+ the_source_sql.each_line {|x| raise MaxLineExceeded.new("Ruby detection and prevention of Oracle error SP2-0027: Input is too long (> 2499 characters) - line ignored") if x.length > 2499}
15
+
16
+ # other sqlplus CAVEATS (that could be programmed, TODO-ish)
17
+ # If you're "IN" statements has more than 1000 elements, Oracle will barf out a ORA-01795 error
18
+ #
19
+ end
20
+
21
+
22
+ def string_to_write_commands_file_name
23
+ self.validate_otm_source_sql
24
+ the_file_to_spool_output_to = self.client_class.otm_get_file_name_for(:oracle_output)
25
+ return "
26
+ WHENEVER SQLERROR EXIT 2;
27
+ spool on;
28
+ set heading off
29
+ set echo off
30
+ set verify off
31
+ set termout off
32
+ SET NEWPAGE 0;
33
+ SET SPACE 0;
34
+ SET PAGESIZE 0;
35
+ SET FEEDBACK OFF;
36
+ SET TRIMSPOOL ON;
37
+ SET TAB OFF;
38
+ set linesize 2000;
39
+ spool #{the_file_to_spool_output_to}
40
+ #{self.client_class.otm_source_sql}
41
+ spool off;
42
+ exit;
43
+ "
44
+ end
45
+ # overridden from parent to help CleanupTempFilesAndTables do it's job
46
+ #
47
+ def temp_file_symbols
48
+ [:oracle_commands]
49
+ end
50
+
51
+ def execute_internal
52
+ bytes_written = 0
53
+ File.open(self.client_class.otm_get_file_name_for(:oracle_commands),'w') do |f|
54
+ bytes_written = f.write(self.string_to_write_commands_file_name)
55
+ end
56
+ if bytes_written > 0
57
+ self.info("#{bytes_written} bytes written to #{self.client_class.otm_get_file_name_for(:oracle_commands)}")
58
+ else
59
+ self.error("could not write to #{self.client_class.otm_get_file_name_for(:oracle_commands)}, bytes_written=#{bytes_written}")
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,16 @@
1
+ module OracleToMysql
2
+ module MustOverrideInstanceMethods
3
+
4
+
5
+ #### MUST BE OVERRIDEN
6
+ def otm_source_sql
7
+ raise "YOU MUST OVERRIDE THIS"
8
+ end
9
+ def otm_target_sql
10
+ raise "YOU MUST OVERRIDE THIS"
11
+ end
12
+ def otm_target_table
13
+ raise "YOU MUST OVERRIDE THIS"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,65 @@
1
+ module OracleToMysql
2
+ module OptionalOverrideInstanceMethods
3
+
4
+ # YOU WILL OFTEN WANT TO OVERRIDE THIS,
5
+ def otm_strategy
6
+ if @otm_strategy.nil?
7
+ @otm_strategy = self.class.otm_default_strategy
8
+ end
9
+ @otm_strategy
10
+ end
11
+
12
+ # TO CHANGE the retain options, override this method
13
+ #
14
+ def otm_retain_options
15
+ if @otm_retain_options.nil?
16
+ @otm_retain_options = self.class.otm_default_retain_options
17
+ end
18
+ @otm_retain_options
19
+ end
20
+
21
+ def otm_source_config_hash
22
+ if self.otm_config_hash.has_key?('oracle_source')
23
+ return otm_config_hash['oracle_source']
24
+ else
25
+ raise "Could not find oracle_source key in config file #{self.otm_config_file}, you should override this method"
26
+ end
27
+ end
28
+ def otm_target_config_hash
29
+ if self.otm_config_hash.has_key?('mysql_target')
30
+ return otm_config_hash['mysql_target']
31
+ else
32
+ raise "Could not find mysql_target key in config file #{self.otm_config_file}, you should override this method"
33
+ end
34
+ end
35
+
36
+ # Override this if you have zany rules about where your config file is, there is a handy otm_config_hash method in the ApiInstanceMethods
37
+ # you can use to get stuff outta here
38
+ #
39
+ def otm_config_file
40
+ non_rails_path = File.join(Dir.pwd, 'oracle_to_mysql.yml')
41
+ if defined?(RAILS_ROOT)
42
+ rails_path = File.join(RAILS_ROOT,'config','database.yml')
43
+ if rails_path
44
+ @otm_db_config_yml_file_name = rails_path
45
+ else
46
+ raise "Weird, RAILS_ROOT detected but no databases.yml, something is amiss!"
47
+ end
48
+ elsif File.exists?(non_rails_path)
49
+ @otm_db_config_yml_file_name = non_rails_path
50
+ else
51
+ raise "ERROR: No otm config file found"
52
+ end
53
+ end
54
+
55
+ # This is the table name that is created first on the server for the :atomic_rename strategy
56
+ #
57
+ def otm_temp_target_table
58
+ "#{self.otm_target_table}_#{self.otm_timestamp.to_i}_temp"
59
+ end
60
+
61
+ def tmp_directory
62
+ "/tmp"
63
+ end
64
+ end
65
+ end