dbgeni 0.10.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.
- 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
|