oracle_to_mysql 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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