dbgeni 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +29 -0
- data/bin/dbgeni +2 -0
- data/lib/dbgeni/base.rb +146 -0
- data/lib/dbgeni/base_code.rb +143 -0
- data/lib/dbgeni/blank_slate.rb +24 -0
- data/lib/dbgeni/cli.rb +96 -0
- data/lib/dbgeni/code.rb +235 -0
- data/lib/dbgeni/code_list.rb +60 -0
- data/lib/dbgeni/commands/code.rb +151 -0
- data/lib/dbgeni/commands/commands.rb +41 -0
- data/lib/dbgeni/commands/config.rb +36 -0
- data/lib/dbgeni/commands/dmls.rb +244 -0
- data/lib/dbgeni/commands/generate.rb +257 -0
- data/lib/dbgeni/commands/initialize.rb +41 -0
- data/lib/dbgeni/commands/migrations.rb +243 -0
- data/lib/dbgeni/commands/milestones.rb +52 -0
- data/lib/dbgeni/commands/new.rb +178 -0
- data/lib/dbgeni/config.rb +325 -0
- data/lib/dbgeni/connectors/connector.rb +59 -0
- data/lib/dbgeni/connectors/mysql.rb +146 -0
- data/lib/dbgeni/connectors/oracle.rb +149 -0
- data/lib/dbgeni/connectors/sqlite.rb +166 -0
- data/lib/dbgeni/connectors/sybase.rb +97 -0
- data/lib/dbgeni/dml_cli.rb +35 -0
- data/lib/dbgeni/environment.rb +161 -0
- data/lib/dbgeni/exceptions/exception.rb +69 -0
- data/lib/dbgeni/file_converter.rb +44 -0
- data/lib/dbgeni/initializers/initializer.rb +44 -0
- data/lib/dbgeni/initializers/mysql.rb +36 -0
- data/lib/dbgeni/initializers/oracle.rb +38 -0
- data/lib/dbgeni/initializers/sqlite.rb +33 -0
- data/lib/dbgeni/initializers/sybase.rb +34 -0
- data/lib/dbgeni/logger.rb +60 -0
- data/lib/dbgeni/migration.rb +302 -0
- data/lib/dbgeni/migration_cli.rb +204 -0
- data/lib/dbgeni/migration_list.rb +91 -0
- data/lib/dbgeni/migrators/migrator.rb +40 -0
- data/lib/dbgeni/migrators/migrator_interface.rb +51 -0
- data/lib/dbgeni/migrators/mysql.rb +82 -0
- data/lib/dbgeni/migrators/oracle.rb +211 -0
- data/lib/dbgeni/migrators/sqlite.rb +90 -0
- data/lib/dbgeni/migrators/sybase.rb +118 -0
- data/lib/dbgeni/plugin.rb +92 -0
- data/lib/dbgeni.rb +52 -0
- metadata +87 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
module DBGeni
|
2
|
+
class MigrationCLI
|
3
|
+
|
4
|
+
def initialize(base_installer, config, logger)
|
5
|
+
@base = base_installer
|
6
|
+
@config = config
|
7
|
+
@logger = logger
|
8
|
+
set_plugin_hooks
|
9
|
+
end
|
10
|
+
|
11
|
+
def migrations
|
12
|
+
@migration_list ||= DBGeni::MigrationList.new(@config.migration_directory) unless @migration_list
|
13
|
+
@migration_list.migrations
|
14
|
+
end
|
15
|
+
|
16
|
+
def outstanding_migrations
|
17
|
+
ensure_initialized
|
18
|
+
migrations
|
19
|
+
@migration_list.outstanding(@config, connection)
|
20
|
+
end
|
21
|
+
|
22
|
+
def applied_migrations
|
23
|
+
ensure_initialized
|
24
|
+
migrations
|
25
|
+
@migration_list.applied(@config, connection)
|
26
|
+
end
|
27
|
+
|
28
|
+
def applied_and_broken_migrations
|
29
|
+
ensure_initialized
|
30
|
+
migrations
|
31
|
+
@migration_list.applied_and_broken(@config, connection)
|
32
|
+
end
|
33
|
+
|
34
|
+
def list_of_migrations(list)
|
35
|
+
ensure_initialized
|
36
|
+
migrations
|
37
|
+
@migration_list.list(list, @config, connection)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Applying
|
41
|
+
|
42
|
+
def apply_all_migrations(force=nil)
|
43
|
+
ensure_initialized
|
44
|
+
migrations = outstanding_migrations
|
45
|
+
if migrations.length == 0
|
46
|
+
raise DBGeni::NoOutstandingMigrations
|
47
|
+
end
|
48
|
+
apply_migration_list(migrations, force)
|
49
|
+
end
|
50
|
+
|
51
|
+
def apply_next_migration(force=nil)
|
52
|
+
ensure_initialized
|
53
|
+
migrations = outstanding_migrations
|
54
|
+
if migrations.length == 0
|
55
|
+
raise DBGeni::NoOutstandingMigrations
|
56
|
+
end
|
57
|
+
apply_migration_list([migrations.first], force)
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply_until_migration(migration_name, force=nil)
|
61
|
+
ensure_initialized
|
62
|
+
milestone = find_migration(migration_name)
|
63
|
+
outstanding = outstanding_migrations
|
64
|
+
index = outstanding.index milestone
|
65
|
+
unless index
|
66
|
+
# milestone migration doesn't exist or is already applied.
|
67
|
+
raise MigrationNotOutstanding, milestone.to_s
|
68
|
+
end
|
69
|
+
apply_migration_list(outstanding[0..index], force)
|
70
|
+
end
|
71
|
+
|
72
|
+
def apply_list_of_migrations(migration_list, force=nil)
|
73
|
+
ensure_initialized
|
74
|
+
migration_files = list_of_migrations(migration_list)
|
75
|
+
apply_migration_list(migration_files, force)
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def apply_migration(migration, force=nil)
|
80
|
+
ensure_initialized
|
81
|
+
begin
|
82
|
+
run_plugin(@before_up_run_plugin, migration)
|
83
|
+
migration.apply!(@config, connection, force)
|
84
|
+
@logger.info "Applied #{migration.to_s}"
|
85
|
+
run_plugin(@after_up_run_plugin, migration)
|
86
|
+
rescue DBGeni::MigratorCouldNotConnect
|
87
|
+
@logger.error "Failed to connect to the database CLI"
|
88
|
+
raise DBGeni::MigrationApplyFailed, migration.to_s
|
89
|
+
rescue DBGeni::MigrationApplyFailed
|
90
|
+
@logger.error "Failed #{migration.to_s}. Errors in #{migration.logfile}\n\n#{migration.error_messages}\n\n"
|
91
|
+
raise DBGeni::MigrationApplyFailed, migration.to_s
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# rolling back
|
96
|
+
|
97
|
+
def rollback_all_migrations(force=nil)
|
98
|
+
ensure_initialized
|
99
|
+
migrations = applied_and_broken_migrations.reverse
|
100
|
+
if migrations.length == 0
|
101
|
+
raise DBGeni::NoAppliedMigrations
|
102
|
+
end
|
103
|
+
apply_migration_list(migrations, force, false)
|
104
|
+
end
|
105
|
+
|
106
|
+
def rollback_last_migration(force=nil)
|
107
|
+
ensure_initialized
|
108
|
+
migrations = applied_and_broken_migrations
|
109
|
+
if migrations.length == 0
|
110
|
+
raise DBGeni::NoAppliedMigrations
|
111
|
+
end
|
112
|
+
# the most recent one is at the end of the array!!
|
113
|
+
apply_migration_list([migrations.last], force, false)
|
114
|
+
end
|
115
|
+
|
116
|
+
def rollback_until_migration(migration_name, force=nil)
|
117
|
+
ensure_initialized
|
118
|
+
milestone = find_migration(migration_name)
|
119
|
+
applied = applied_and_broken_migrations.reverse
|
120
|
+
index = applied.index milestone
|
121
|
+
unless index
|
122
|
+
# milestone migration doesn't exist or is already applied.
|
123
|
+
raise DBGeni::MigrationNotApplied, milestone.to_s
|
124
|
+
end
|
125
|
+
# Note the triple ... in the range to exclude the end element
|
126
|
+
# This is because we don't want to rollback the final migration as its upto but not including
|
127
|
+
apply_migration_list(applied[0...index], force, false)
|
128
|
+
end
|
129
|
+
|
130
|
+
def rollback_list_of_migrations(migration_list, force=nil)
|
131
|
+
ensure_initialized
|
132
|
+
migration_files = list_of_migrations(migration_list).reverse
|
133
|
+
apply_migration_list(migration_files, force, false)
|
134
|
+
end
|
135
|
+
|
136
|
+
def rollback_migration(migration, force=nil)
|
137
|
+
ensure_initialized
|
138
|
+
begin
|
139
|
+
run_plugin(@before_down_run_plugin, migration)
|
140
|
+
migration.rollback!(@config, connection, force)
|
141
|
+
@logger.info "Rolledback #{migration.to_s}"
|
142
|
+
run_plugin(@after_down_run_plugin, migration)
|
143
|
+
rescue DBGeni::MigratorCouldNotConnect
|
144
|
+
@logger.error "Failed to connect to the database CLI"
|
145
|
+
raise DBGeni::MigrationApplyFailed, migration.to_s
|
146
|
+
rescue DBGeni::MigrationApplyFailed
|
147
|
+
@logger.error "Failed #{migration.to_s}. Errors in #{migration.logfile}\n\n#{migration.error_messages}\n\n"
|
148
|
+
raise DBGeni::MigrationApplyFailed, migration.to_s
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def apply_migration_list(migration_list, force, up=true)
|
155
|
+
# TODO - conflicted about how to handle exceptions.
|
156
|
+
# If before_running_migrations throws an exception no
|
157
|
+
# migrations will be run.
|
158
|
+
# If a migration throws an exception, then after_running_migrations
|
159
|
+
# will not be run.
|
160
|
+
params = {
|
161
|
+
:operation => up == true ? 'apply' : 'remove'
|
162
|
+
}
|
163
|
+
run_plugin(@before_running_plugin, migration_list, params)
|
164
|
+
migration_list.each do |m|
|
165
|
+
if up
|
166
|
+
apply_migration(m, force)
|
167
|
+
else
|
168
|
+
rollback_migration(m, force)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
run_plugin(@after_running_plugin, migration_list, params)
|
172
|
+
end
|
173
|
+
|
174
|
+
def run_plugin(hook, object, params={})
|
175
|
+
@base.run_plugin(hook, object, params)
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def find_migration(migration_name)
|
181
|
+
m = Migration.initialize_from_internal_name(@config.migration_directory, migration_name)
|
182
|
+
end
|
183
|
+
|
184
|
+
def set_plugin_hooks
|
185
|
+
@before_up_run_plugin = :before_migration_up
|
186
|
+
@after_up_run_plugin = :after_migration_up
|
187
|
+
@before_down_run_plugin = :before_migration_down
|
188
|
+
@after_down_run_plugin = :after_migration_down
|
189
|
+
@before_running_plugin = :before_running_migrations
|
190
|
+
@after_running_plugin = :after_running_migrations
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
def ensure_initialized
|
195
|
+
@base.ensure_initialized
|
196
|
+
end
|
197
|
+
|
198
|
+
def connection
|
199
|
+
@base.connection
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module DBGeni
|
2
|
+
|
3
|
+
class MigrationList
|
4
|
+
|
5
|
+
attr_reader :migrations
|
6
|
+
attr_reader :migration_directory
|
7
|
+
|
8
|
+
# This is a bit of a hack. DML migrations reuse all of migrations, infact they were forced in
|
9
|
+
# later as an afterthought, so each migration will initially have a type of 'Migration'. This
|
10
|
+
# changes it to DML.
|
11
|
+
def self.new_dml_migrations(migration_directory)
|
12
|
+
obj = self.new(migration_directory)
|
13
|
+
obj.migrations.each do |m|
|
14
|
+
m.migration_type = 'DML'
|
15
|
+
end
|
16
|
+
obj
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(migration_directory)
|
20
|
+
@migration_directory = migration_directory
|
21
|
+
file_list
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns an array of MIGRATIONS that have been applied
|
25
|
+
# ordered by oldest first
|
26
|
+
def applied(config, connection)
|
27
|
+
migrations_with_status(config, connection, DBGeni::Migration::COMPLETED)
|
28
|
+
end
|
29
|
+
|
30
|
+
def outstanding(config, connection)
|
31
|
+
migrations_with_status(config, connection,
|
32
|
+
DBGeni::Migration::NEW,
|
33
|
+
DBGeni::Migration::ROLLEDBACK,
|
34
|
+
DBGeni::Migration::FAILED,
|
35
|
+
DBGeni::Migration::PENDING)
|
36
|
+
end
|
37
|
+
|
38
|
+
def applied_and_broken(config, connection)
|
39
|
+
migrations_with_status(config, connection,
|
40
|
+
DBGeni::Migration::COMPLETED,
|
41
|
+
DBGeni::Migration::FAILED,
|
42
|
+
DBGeni::Migration::PENDING)
|
43
|
+
end
|
44
|
+
|
45
|
+
def list(list_of_migrations, config, connection)
|
46
|
+
valid_migrations = []
|
47
|
+
list_of_migrations.each do |m|
|
48
|
+
mig_obj = Migration.initialize_from_internal_name(@migration_directory, m)
|
49
|
+
if i = @migrations.index(mig_obj)
|
50
|
+
valid_migrations.push @migrations[i]
|
51
|
+
else
|
52
|
+
raise DBGeni::MigrationFileNotExist, m
|
53
|
+
end
|
54
|
+
end
|
55
|
+
valid_migrations.sort {|x,y|
|
56
|
+
x.migration_file <=> y.migration_file
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def migrations_with_status(config, connection, *args)
|
62
|
+
migrations = @migrations.select{|m|
|
63
|
+
args.include? m.status(config, connection)
|
64
|
+
}.sort {|x,y|
|
65
|
+
x.migration_file <=> y.migration_file
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def file_list
|
72
|
+
begin
|
73
|
+
# Migrations usually come in pairs, so need to find just the 'up'
|
74
|
+
# ones here, otherwise there will be too many!
|
75
|
+
# The migration filename format is YYYYMMDDHHMM_<up / down >_title.sql
|
76
|
+
files = Dir.entries(@migration_directory).grep(/^\d{12}_up_.+\.sql$/).sort
|
77
|
+
rescue Exception => e
|
78
|
+
puts "Migrations directory: #{@migrations_directory}"
|
79
|
+
raise DBGeni::MigrationDirectoryNotExist, "Migrations directory: #{@migrations_directory}"
|
80
|
+
end
|
81
|
+
@migrations = Array.new
|
82
|
+
files.each do |f|
|
83
|
+
@migrations.push Migration.new(@migration_directory, f)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module DBGeni
|
2
|
+
|
3
|
+
module Migrator
|
4
|
+
|
5
|
+
def self.initialize(config, connection)
|
6
|
+
required_class = setup(config.db_type)
|
7
|
+
begin
|
8
|
+
required_method = required_class.method("new")
|
9
|
+
rescue NameError
|
10
|
+
raise DBGeni::InvalidMigratorForDBType, config.db_type
|
11
|
+
end
|
12
|
+
required_method.call(config, connection)
|
13
|
+
end
|
14
|
+
|
15
|
+
# def self.logfile(filename)
|
16
|
+
# name = File.basename(filename)
|
17
|
+
# "#{Time.now.strftime('%Y%m%d%H%M%S')}_#{name}"
|
18
|
+
# end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.setup(db_type)
|
23
|
+
begin
|
24
|
+
require "dbgeni/migrators/#{db_type}"
|
25
|
+
rescue
|
26
|
+
raise DBGeni::NoMigratorForDBType, db_type
|
27
|
+
end
|
28
|
+
|
29
|
+
required_class = nil
|
30
|
+
if Migrator.const_defined?(db_type.capitalize)
|
31
|
+
required_class = Migrator.const_get(db_type.capitalize)
|
32
|
+
else
|
33
|
+
raise DBGeni::NoMigratorForDBType, db_type
|
34
|
+
end
|
35
|
+
required_class
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module DBGeni
|
2
|
+
module Migrator
|
3
|
+
|
4
|
+
class MigratorInterface
|
5
|
+
|
6
|
+
attr_reader :logfile
|
7
|
+
|
8
|
+
def apply(migration, force=nil)
|
9
|
+
run_in_client(migration.runnable_migration, force)
|
10
|
+
end
|
11
|
+
|
12
|
+
def rollback(migration, force=nil)
|
13
|
+
run_in_client(migration.runnable_rollback, force)
|
14
|
+
end
|
15
|
+
|
16
|
+
def migration_errors
|
17
|
+
end
|
18
|
+
|
19
|
+
def verify(migration)
|
20
|
+
raise DBGeni::NotImplemented
|
21
|
+
end
|
22
|
+
|
23
|
+
def compile(code, force=false)
|
24
|
+
run_in_client(code.runnable_code, force, true)
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove(code, force=false)
|
28
|
+
raise DBGeni::NotImplemented
|
29
|
+
end
|
30
|
+
|
31
|
+
def code_errors
|
32
|
+
raise DBGeni::NotImplemented
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def run_in_client(file, force, is_proc=false)
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(config, connection)
|
41
|
+
@config = config
|
42
|
+
# this is not actually used to run in the sql script
|
43
|
+
@connection = connection
|
44
|
+
@logfile = nil
|
45
|
+
@log_dir = DBGeni::Logger.instance("#{@config.base_directory}/log").detailed_log_dir
|
46
|
+
ensure_executable_exists
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module DBGeni
|
2
|
+
module Migrator
|
3
|
+
|
4
|
+
class Mysql < DBGeni::Migrator::MigratorInterface
|
5
|
+
|
6
|
+
def initialize(config, connection)
|
7
|
+
super(config, connection)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Defined in super ...
|
11
|
+
# def apply(migration, force=nil)
|
12
|
+
# end
|
13
|
+
|
14
|
+
# def rollback(migration, force=nil)
|
15
|
+
# end
|
16
|
+
|
17
|
+
def migration_errors
|
18
|
+
error = ''
|
19
|
+
# MYSQL prints the errors at the start of the log file
|
20
|
+
begin
|
21
|
+
fh = File.open(@logfile, 'r')
|
22
|
+
error = fh.readline
|
23
|
+
unless error =~ /^ERROR/
|
24
|
+
error = ''
|
25
|
+
end
|
26
|
+
ensure
|
27
|
+
fh.close if fh
|
28
|
+
end
|
29
|
+
error
|
30
|
+
end
|
31
|
+
|
32
|
+
def remove(code, force=false)
|
33
|
+
begin
|
34
|
+
@connection.execute(drop_command(code))
|
35
|
+
rescue Exception => e
|
36
|
+
unless e.to_s =~ /(procedure|function|trigger).+does not exist/i
|
37
|
+
raise DBGeni::CodeRemoveFailed, e.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def code_errors
|
43
|
+
# In mysql the code errors ar just the same as migration errors
|
44
|
+
errors = migration_errors
|
45
|
+
if errors == ''
|
46
|
+
errors = nil
|
47
|
+
end
|
48
|
+
errors
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def run_in_client(file, force, is_proc=false)
|
54
|
+
@logfile = "#{@log_dir}/#{File.basename(file)}"
|
55
|
+
|
56
|
+
z = @config.env
|
57
|
+
response = system("mysql -u#{z.username} -p#{z.password} -h#{z.hostname} -P#{z.port} -D#{z.database} -vvv #{force ? '--force' : ''} <#{file} >#{logfile} 2>&1")
|
58
|
+
unless response
|
59
|
+
raise DBGeni::MigrationContainsErrors
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def ensure_executable_exists
|
64
|
+
unless Kernel.executable_exists?('mysql')
|
65
|
+
raise DBGeni::DBCLINotOnPath
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def drop_command(code)
|
70
|
+
case code.type
|
71
|
+
when DBGeni::Code::TRIGGER
|
72
|
+
"drop trigger #{code.name.downcase}"
|
73
|
+
when DBGeni::Code::FUNCTION
|
74
|
+
"drop function #{code.name.downcase}"
|
75
|
+
when DBGeni::Code::PROCEDURE
|
76
|
+
"drop procedure #{code.name.downcase}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
module DBGeni
|
2
|
+
module Migrator
|
3
|
+
|
4
|
+
class Oracle < DBGeni::Migrator::MigratorInterface
|
5
|
+
|
6
|
+
def initialize(config, connection)
|
7
|
+
super(config, connection)
|
8
|
+
end
|
9
|
+
|
10
|
+
# def apply(migration, force=nil)
|
11
|
+
# end
|
12
|
+
|
13
|
+
# def rollback(migration, force=nil)
|
14
|
+
# end
|
15
|
+
|
16
|
+
def migration_errors
|
17
|
+
has_errors = false
|
18
|
+
buffer = []
|
19
|
+
|
20
|
+
begin
|
21
|
+
File.open(@logfile, 'r').each_line do |l|
|
22
|
+
buffer.push l
|
23
|
+
if buffer.length > 10
|
24
|
+
buffer.shift
|
25
|
+
end
|
26
|
+
if !has_errors && l =~ /^ERROR at line/
|
27
|
+
has_errors = true
|
28
|
+
next
|
29
|
+
end
|
30
|
+
# After we find the ERROR at line, the next line contains the error
|
31
|
+
# message, so we just want to consume it and then exit.
|
32
|
+
# The line we care about will be in the buffer, so just break and join
|
33
|
+
# the buffer.
|
34
|
+
if has_errors
|
35
|
+
break
|
36
|
+
end
|
37
|
+
end
|
38
|
+
rescue Errno::ENOENT
|
39
|
+
# assume this means the log was never written as, generally because
|
40
|
+
# sqlplus didn't connect to Oracle. In this case do nothing
|
41
|
+
end
|
42
|
+
buffer.join("")
|
43
|
+
end
|
44
|
+
|
45
|
+
# def verify(migration)
|
46
|
+
# end
|
47
|
+
|
48
|
+
def compile(code, force=false)
|
49
|
+
run_in_client(code.runnable_code, force, true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def remove(code, force=false)
|
53
|
+
begin
|
54
|
+
@connection.execute(drop_command(code))
|
55
|
+
rescue Exception => e
|
56
|
+
unless e.to_s =~ /(object|trigger) .+ does not exist/
|
57
|
+
raise DBGeni::CodeRemoveFailed, e.to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def code_errors
|
63
|
+
# The error part of the file file either looks like:
|
64
|
+
|
65
|
+
# SQL> show err
|
66
|
+
# No errors.
|
67
|
+
# SQL> spool off
|
68
|
+
|
69
|
+
# or
|
70
|
+
|
71
|
+
# SQL> show err
|
72
|
+
# Errors for PACKAGE BODY PKG1:
|
73
|
+
|
74
|
+
# LINE/COL ERROR
|
75
|
+
# -------- -----------------------------------------------------------------
|
76
|
+
# 5/1 PLS-00103: Encountered the symbol "END" when expecting one of the
|
77
|
+
# following:
|
78
|
+
# Error messages here
|
79
|
+
# SQL> spool off
|
80
|
+
|
81
|
+
# In the first case, return nil, but in the second case get everything after show err
|
82
|
+
|
83
|
+
error_msg = ''
|
84
|
+
start_search = false
|
85
|
+
File.open(@logfile, 'r').each_line do |l|
|
86
|
+
if !start_search && l =~ /^SQL> show err/
|
87
|
+
start_search = true
|
88
|
+
next
|
89
|
+
end
|
90
|
+
if start_search
|
91
|
+
if l =~ /^No errors\./
|
92
|
+
error_msg = nil
|
93
|
+
break
|
94
|
+
elsif l =~ /^SQL> spool off/
|
95
|
+
break
|
96
|
+
else
|
97
|
+
error_msg << l
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
error_msg
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def run_in_client(file, force, is_proc=false)
|
107
|
+
null_device = '/dev/null'
|
108
|
+
if Kernel.is_windows?
|
109
|
+
null_device = 'NUL:'
|
110
|
+
end
|
111
|
+
|
112
|
+
@logfile = "#{@log_dir}/#{File.basename(file)}"
|
113
|
+
|
114
|
+
# sql_parameters = parameters
|
115
|
+
# unless parameters
|
116
|
+
# sql_parameters = checkSQLPlusParameters(file)
|
117
|
+
# end
|
118
|
+
|
119
|
+
add_terminator = true
|
120
|
+
if is_proc
|
121
|
+
add_terminator = !file_contains_terminator?(file)
|
122
|
+
end
|
123
|
+
|
124
|
+
IO.popen("sqlplus -L #{@config.env.username}/#{@config.env.password}@#{@config.env.database} > #{null_device}", "w") do |p|
|
125
|
+
p.puts "set TERM on"
|
126
|
+
p.puts "set ECHO on"
|
127
|
+
# if sql_parameters == ''
|
128
|
+
p.puts "set define off"
|
129
|
+
# end
|
130
|
+
unless force
|
131
|
+
p.puts "whenever sqlerror exit 200"
|
132
|
+
end
|
133
|
+
# p.puts "START #{File.basename(file)} #{sql_parameters}"
|
134
|
+
p.puts "spool #{@logfile}"
|
135
|
+
# Switch the current schema but only if the override is in the config file
|
136
|
+
if @config.env.install_schema
|
137
|
+
p.puts "alter session set current_schema=#{@config.env.install_schema};"
|
138
|
+
end
|
139
|
+
p.puts "START #{file}"
|
140
|
+
if is_proc
|
141
|
+
p.puts "/" if add_terminator
|
142
|
+
p.puts "show err"
|
143
|
+
end
|
144
|
+
p.puts "spool off"
|
145
|
+
p.puts "exit"
|
146
|
+
end
|
147
|
+
# When the pipe block ends, ruby sets $? with the exit status. A
|
148
|
+
# good exit status is 0 (zero) anything else means it went wrong
|
149
|
+
# If $? is anything but zero, raise an exception.
|
150
|
+
if $?.exitstatus != 0
|
151
|
+
if $?.exitstatus != 200
|
152
|
+
raise DBGeni::MigratorCouldNotConnect
|
153
|
+
end
|
154
|
+
# Code compile errors never get here as they don't make sqlplus abort.
|
155
|
+
# But if the user does not have privs to create the proc / trigger etc,
|
156
|
+
# the code will abort to here via a insufficient privs error. Or if the code
|
157
|
+
# file doesn't contain 'create or replace .... ' and just garbage it can get here.
|
158
|
+
raise DBGeni::MigrationContainsErrors
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def ensure_executable_exists
|
163
|
+
unless Kernel.executable_exists?('sqlplus')
|
164
|
+
raise DBGeni::DBCLINotOnPath
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def drop_command(code)
|
169
|
+
case code.type
|
170
|
+
when DBGeni::Code::PACKAGE_SPEC
|
171
|
+
"drop package #{code.name}"
|
172
|
+
when DBGeni::Code::PACKAGE_BODY
|
173
|
+
"drop package body #{code.name}"
|
174
|
+
when DBGeni::Code::TRIGGER
|
175
|
+
"drop trigger #{code.name}"
|
176
|
+
when DBGeni::Code::FUNCTION
|
177
|
+
"drop function #{code.name}"
|
178
|
+
when DBGeni::Code::PROCEDURE
|
179
|
+
"drop procedure #{code.name}"
|
180
|
+
when DBGeni::Code::TYPE
|
181
|
+
"drop type #{code.name}"
|
182
|
+
when DBGeni::Code::UNKNOWN
|
183
|
+
derive_drop_command_from_db(code.name)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def file_contains_terminator?(filename)
|
188
|
+
has_slash = false
|
189
|
+
File.open(filename, 'r').each_line do |l|
|
190
|
+
if l =~ /^\s*\/\s*$/
|
191
|
+
has_slash = true
|
192
|
+
break
|
193
|
+
end
|
194
|
+
end
|
195
|
+
has_slash
|
196
|
+
end
|
197
|
+
|
198
|
+
def derive_drop_command_from_db(object_name)
|
199
|
+
# This is tricky because of packages - will give two objects with the same name :-/
|
200
|
+
raise CannotRemoveUnknownObject
|
201
|
+
|
202
|
+
# owner = @config.env.install_schema ? @config.env.install_schema : @config.env.username
|
203
|
+
# results = @connection.execute ("select object_type
|
204
|
+
# from all_objects
|
205
|
+
# where owner = :b1 and object_name = b2", owner.upcase, object_name.upcase)
|
206
|
+
# unless results
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|