dbgeni 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +29 -0
  3. data/bin/dbgeni +2 -0
  4. data/lib/dbgeni/base.rb +146 -0
  5. data/lib/dbgeni/base_code.rb +143 -0
  6. data/lib/dbgeni/blank_slate.rb +24 -0
  7. data/lib/dbgeni/cli.rb +96 -0
  8. data/lib/dbgeni/code.rb +235 -0
  9. data/lib/dbgeni/code_list.rb +60 -0
  10. data/lib/dbgeni/commands/code.rb +151 -0
  11. data/lib/dbgeni/commands/commands.rb +41 -0
  12. data/lib/dbgeni/commands/config.rb +36 -0
  13. data/lib/dbgeni/commands/dmls.rb +244 -0
  14. data/lib/dbgeni/commands/generate.rb +257 -0
  15. data/lib/dbgeni/commands/initialize.rb +41 -0
  16. data/lib/dbgeni/commands/migrations.rb +243 -0
  17. data/lib/dbgeni/commands/milestones.rb +52 -0
  18. data/lib/dbgeni/commands/new.rb +178 -0
  19. data/lib/dbgeni/config.rb +325 -0
  20. data/lib/dbgeni/connectors/connector.rb +59 -0
  21. data/lib/dbgeni/connectors/mysql.rb +146 -0
  22. data/lib/dbgeni/connectors/oracle.rb +149 -0
  23. data/lib/dbgeni/connectors/sqlite.rb +166 -0
  24. data/lib/dbgeni/connectors/sybase.rb +97 -0
  25. data/lib/dbgeni/dml_cli.rb +35 -0
  26. data/lib/dbgeni/environment.rb +161 -0
  27. data/lib/dbgeni/exceptions/exception.rb +69 -0
  28. data/lib/dbgeni/file_converter.rb +44 -0
  29. data/lib/dbgeni/initializers/initializer.rb +44 -0
  30. data/lib/dbgeni/initializers/mysql.rb +36 -0
  31. data/lib/dbgeni/initializers/oracle.rb +38 -0
  32. data/lib/dbgeni/initializers/sqlite.rb +33 -0
  33. data/lib/dbgeni/initializers/sybase.rb +34 -0
  34. data/lib/dbgeni/logger.rb +60 -0
  35. data/lib/dbgeni/migration.rb +302 -0
  36. data/lib/dbgeni/migration_cli.rb +204 -0
  37. data/lib/dbgeni/migration_list.rb +91 -0
  38. data/lib/dbgeni/migrators/migrator.rb +40 -0
  39. data/lib/dbgeni/migrators/migrator_interface.rb +51 -0
  40. data/lib/dbgeni/migrators/mysql.rb +82 -0
  41. data/lib/dbgeni/migrators/oracle.rb +211 -0
  42. data/lib/dbgeni/migrators/sqlite.rb +90 -0
  43. data/lib/dbgeni/migrators/sybase.rb +118 -0
  44. data/lib/dbgeni/plugin.rb +92 -0
  45. data/lib/dbgeni.rb +52 -0
  46. metadata +87 -0
@@ -0,0 +1,44 @@
1
+ module DBGeni
2
+
3
+ class FileConverter
4
+
5
+ def self.convert(directory, file, config)
6
+ fc = new(directory, file, config)
7
+ fc.convert
8
+ end
9
+
10
+ def initialize(directory, file, config)
11
+ @directory = directory
12
+ @file = file
13
+ @config = config
14
+ create_temp
15
+ end
16
+
17
+ def convert
18
+ original_file = File.join(@directory, @file)
19
+ output_file = File.join(@temp_dir, @file)
20
+ begin
21
+ of = File.open(output_file, 'w')
22
+ File.foreach(original_file) do |line|
23
+ # remove potential \r\n from dos files. isql chokes on these on linux
24
+ # but not on windows.
25
+ line.chomp!
26
+ of.print line
27
+ of.print "\n"
28
+ end
29
+ ensure
30
+ of.close
31
+ end
32
+ output_file
33
+ end
34
+
35
+ private
36
+
37
+ def create_temp
38
+ @temp_dir = File.join(@config.base_directory, 'log', 'temp')
39
+ FileUtils.mkdir_p(@temp_dir)
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,44 @@
1
+ module DBGeni
2
+ module Initializer
3
+
4
+ def self.initialize(db_connection, config)
5
+ required_module = setup(config.db_type)
6
+ begin
7
+ required_method = required_module.method("initialize")
8
+ rescue NameError
9
+ raise DBGeni::InvalidInitializerForDBType, config.db_type
10
+ end
11
+ required_method.call(db_connection, config)
12
+ end
13
+
14
+ def self.initialized?(db_connection, config)
15
+ required_module = setup(config.db_type)
16
+ begin
17
+ required_method = required_module.method("initialized?")
18
+ rescue NameError
19
+ raise DBGeni::InvalidInitializerForDBType, config.db_type
20
+ end
21
+ required_method.call(db_connection, config)
22
+ end
23
+
24
+ private
25
+
26
+ def self.setup(db_type)
27
+ begin
28
+ require "dbgeni/initializers/#{db_type}"
29
+ rescue
30
+ raise DBGeni::NoInitializerForDBType, db_type
31
+ end
32
+
33
+ required_module = nil
34
+ if Initializer.const_defined?(db_type.capitalize)
35
+ required_module = Initializer.const_get(db_type.capitalize)
36
+ else
37
+ raise raise DBGeni::NoInitializerForDBType, db_type
38
+ end
39
+ required_module
40
+ end
41
+
42
+ end
43
+ end
44
+
@@ -0,0 +1,36 @@
1
+ module DBGeni
2
+ module Initializer
3
+ module Mysql
4
+
5
+ def self.initialize(db_connection, config)
6
+ raise DBGeni::DatabaseAlreadyInitialized if self.initialized?(db_connection, config)
7
+ db_connection.execute("create table #{config.db_table}
8
+ (
9
+ sequence_or_hash varchar(100) not null,
10
+ migration_name varchar(4000) not null,
11
+ migration_type varchar(20) not null,
12
+ migration_state varchar(20) not null,
13
+ start_dtm datetime,
14
+ completed_dtm datetime
15
+ )")
16
+ db_connection.execute("create unique index #{config.db_table}_uk1 on #{config.db_table} (sequence_or_hash, migration_name(500), migration_type)")
17
+ db_connection.execute("create index #{config.db_table}_idx2 on #{config.db_table} (migration_name)")
18
+ end
19
+
20
+ def self.initialized?(db_connection, config)
21
+ # it is initialized if a table called dbgeni_migrations or whatever is
22
+ # defined in config exists
23
+ results = db_connection.execute("show tables like '#{config.db_table.downcase}'")
24
+ if 0 == results.length
25
+ false
26
+ else
27
+ true
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+
@@ -0,0 +1,38 @@
1
+ module DBGeni
2
+ module Initializer
3
+ module Oracle
4
+
5
+ def self.initialize(db_connection, config)
6
+ raise DBGeni::DatabaseAlreadyInitialized if self.initialized?(db_connection, config)
7
+ db_connection.execute("create table #{config.db_table}
8
+ (
9
+ sequence_or_hash varchar2(100) not null,
10
+ migration_name varchar2(4000) not null,
11
+ migration_type varchar2(20) not null,
12
+ migration_state varchar2(20) not null,
13
+ start_dtm date,
14
+ completed_dtm date
15
+ )")
16
+ db_connection.execute("create unique index #{config.db_table}_uk1 on #{config.db_table} (sequence_or_hash, migration_name, migration_type)")
17
+ db_connection.execute("create index #{config.db_table}_idx2 on #{config.db_table} (migration_name)")
18
+ end
19
+
20
+ def self.initialized?(db_connection, config)
21
+ # it is initialized if a table called dbgeni_migrations or whatever is
22
+ # defined in config exists
23
+ results = db_connection.execute("select table_name from all_tables where table_name = :t and owner = :o",
24
+ config.db_table.upcase,
25
+ config.env.install_schema ? config.env.install_schema.upcase : config.env.username.upcase)
26
+ if 0 == results.length
27
+ false
28
+ else
29
+ true
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+
37
+
38
+
@@ -0,0 +1,33 @@
1
+ module DBGeni
2
+ module Initializer
3
+ module Sqlite
4
+
5
+ def self.initialize(db_connection, config)
6
+ raise DBGeni::DatabaseAlreadyInitialized if self.initialized?(db_connection, config)
7
+ db_connection.execute("create table #{config.db_table}
8
+ (
9
+ sequence_or_hash varchar2(100) not null,
10
+ migration_name varchar2(4000) not null,
11
+ migration_type varchar2(20) not null,
12
+ migration_state varchar2(20) not null,
13
+ start_dtm date,
14
+ completed_dtm date
15
+ )")
16
+ db_connection.execute("create unique index #{config.db_table}_uk1 on #{config.db_table} (sequence_or_hash, migration_name, migration_type)")
17
+ db_connection.execute("create index #{config.db_table}_idx2 on #{config.db_table} (migration_name)")
18
+ end
19
+
20
+ def self.initialized?(db_connection, config)
21
+ # it is initialized if a table called dbgeni_migrations or whatever is
22
+ # defined in config exists
23
+ results = db_connection.execute("SELECT name FROM sqlite_master WHERE name = :t", config.db_table.downcase)
24
+ if 0 == results.length
25
+ false
26
+ else
27
+ true
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ module DBGeni
2
+ module Initializer
3
+ module Sybase
4
+
5
+ def self.initialize(db_connection, config)
6
+ raise DBGeni::DatabaseAlreadyInitialized if self.initialized?(db_connection, config)
7
+ db_connection.execute("create table #{config.db_table}
8
+ (
9
+ sequence_or_hash varchar(100) not null,
10
+ migration_name varchar(1100) not null,
11
+ migration_type varchar(20) not null,
12
+ migration_state varchar(20) not null,
13
+ start_dtm datetime null,
14
+ completed_dtm datetime null
15
+ )")
16
+ db_connection.execute("create unique index #{config.db_table}_uk1 on #{config.db_table} (sequence_or_hash, migration_name, migration_type)")
17
+ db_connection.execute("create index #{config.db_table}_idx2 on #{config.db_table} (migration_name)")
18
+ end
19
+
20
+ def self.initialized?(db_connection, config)
21
+ # it is initialized if a table called dbgeni_migrations or whatever is
22
+ # defined in config exists
23
+ results = db_connection.execute("select name from sysobjects where name = \?", config.db_table.downcase)
24
+ if 0 == results.length
25
+ false
26
+ else
27
+ true
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,60 @@
1
+ module DBGeni
2
+
3
+ class Logger
4
+
5
+ def self.instance(location=nil)
6
+ @@singleton_instance ||= self.new(location)
7
+ end
8
+
9
+ def info(msg)
10
+ write_msg(msg)
11
+ end
12
+
13
+ def error(msg)
14
+ write_msg("ERROR - #{msg}")
15
+ end
16
+
17
+ def close
18
+ if @fh && !@fh.closed?
19
+ @fh.close
20
+ end
21
+ @@singleton_instance = nil
22
+ end
23
+
24
+ # This could be done in the initialize block, but then even for
25
+ # non destructive commands, there would be a detailed log dir
26
+ # created, so only create the dir when the directory is asked for.
27
+ def detailed_log_dir
28
+ FileUtils.mkdir_p(File.join(@log_location, @detailed_log_dir))
29
+ File.join(@log_location, @detailed_log_dir)
30
+ end
31
+
32
+ def reset_detailed_log_dir
33
+ @detailed_log_dir = Time.now.strftime('%Y%m%d%H%M%S')
34
+ end
35
+
36
+ private
37
+
38
+ def write_msg(msg, echo=true)
39
+ if @fh && !@fh.closed?
40
+ @fh.puts "#{Time.now.strftime('%Y%m%d %H:%M:%S')} - #{msg}"
41
+ end
42
+ puts msg
43
+ end
44
+
45
+ def initialize(location=nil)
46
+ # If location is nil, then error
47
+ @log_location = File.expand_path(location)
48
+ if @log_location
49
+ FileUtils.mkdir_p(location)
50
+ @fh = File.open("#{location}/log.txt", 'a')
51
+ @fh.puts("\n\n\n###################################################")
52
+ @fh.puts("dbgeni initialized")
53
+ reset_detailed_log_dir
54
+ @fh.puts("Detailed log files will be written in #{File.join(@log_location, @detailed_log_dir)}")
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,302 @@
1
+ module DBGeni
2
+
3
+ class Migration
4
+
5
+ # These are all the states a migration can be in. The NEW status is never in the
6
+ # database, as that is the default state when it has been created as a file,
7
+ # but never applied to the database.
8
+ #
9
+ # PENDING - this is the state a migration goes into before the migration is runin
10
+ # and while it is running.
11
+ # COMPLETED - after the migration completes, if it was successful it gets moved to this state
12
+ # FAILED - after the migration completes, if it failed it gets moved to this state
13
+ # ROLLEDBACK - if a migration has been rolledback, it goes into this state.
14
+ NEW = 'New'
15
+ PENDING = 'Pending'
16
+ FAILED = 'Failed'
17
+ COMPLETED = 'Completed'
18
+ ROLLEDBACK = 'Rolledback'
19
+ # TODO - add verified state?
20
+
21
+ attr_reader :directory, :migration_file, :rollback_file, :name, :sequence, :logfile, :error_messages
22
+ attr_accessor :migration_type
23
+
24
+ def self.internal_name_from_filename(filename)
25
+ filename =~ /^(\d{12})_(up|down)_(.+)\.sql$/
26
+ "#{$1}::#{$3}"
27
+ end
28
+
29
+ def self.filename_from_internal_name(internal_name)
30
+ internal_name =~ /^(\d{12})::(.+)$/
31
+ "#{$1}_up_#{$2}.sql"
32
+ end
33
+
34
+ def self.initialize_from_internal_name(directory, name)
35
+ self.new(directory, Migration.filename_from_internal_name(name))
36
+ end
37
+
38
+ def self.get_milestone_migration(directory, name)
39
+ migration = ''
40
+ begin
41
+ f = File.open(File.join(directory,name), 'r')
42
+ migration = f.readline.chomp
43
+ rescue EOFError
44
+ ensure
45
+ f.close if f
46
+ end
47
+ unless migration =~ /^(\d{12})_(up|down)_(.+)\.sql$/
48
+ raise DBGeni::MilestoneHasNoMigration, name
49
+ end
50
+ migration
51
+ end
52
+
53
+ def initialize(directory, migration)
54
+ @migration_type = 'Migration'
55
+ @directory = directory
56
+ @migration_file = migration
57
+ parse_file
58
+ @rollback_file = "#{sequence}_down_#{name}.sql"
59
+ @runnable_migration = nil
60
+ @runnable_rollback = nil
61
+ end
62
+
63
+ def migration_file(dir='up')
64
+ "#{@sequence}_#{dir}_#{name}.sql"
65
+ end
66
+
67
+ def ==(other)
68
+ if other.migration_file == @migration_file and other.directory == @directory
69
+ true
70
+ else
71
+ false
72
+ end
73
+ end
74
+
75
+ def applied?(config, connection)
76
+ result = status(config, connection)
77
+ result == COMPLETED ? true : false
78
+ end
79
+
80
+ def status(config, connection)
81
+ set_env(config, connection)
82
+ results = connection.execute("select migration_state
83
+ from #{@config.db_table}
84
+ where sequence_or_hash = ?
85
+ and migration_name = ?
86
+ and migration_type = ?", @sequence, @name, @migration_type)
87
+ results.length == 1 ? results[0][0] : NEW
88
+ end
89
+ #"
90
+
91
+ def apply!(config, connection, force=nil)
92
+ set_env(config, connection)
93
+ if applied?(config, connection) and force != true
94
+ raise DBGeni::MigrationAlreadyApplied, self.to_s
95
+ end
96
+ ensure_file_exists
97
+ migrator = DBGeni::Migrator.initialize(config, connection)
98
+ convert_migration(config)
99
+ set_pending!
100
+ begin
101
+ migrator.apply(self, force)
102
+ set_completed!
103
+ rescue Exception => e
104
+ set_failed!
105
+ if e.class == DBGeni::MigratorCouldNotConnect
106
+ raise e
107
+ else
108
+ raise DBGeni::MigrationApplyFailed, self.to_s
109
+ end
110
+ ensure
111
+ @logfile = migrator.logfile
112
+ @error_messages = migrator.migration_errors
113
+ end
114
+ end
115
+
116
+ def rollback!(config, connection, force=nil)
117
+ set_env(config, connection)
118
+ if [NEW, ROLLEDBACK].include? status(config, connection) and force != true
119
+ raise DBGeni::MigrationNotApplied, self.to_s
120
+ end
121
+ ensure_file_exists('down')
122
+ migrator = DBGeni::Migrator.initialize(config, connection)
123
+ convert_rollback(config)
124
+ set_pending!
125
+ begin
126
+ migrator.rollback(self, force)
127
+ set_rolledback!()
128
+
129
+ rescue Exception => e
130
+ set_failed!
131
+ if e.class == DBGeni::MigratorCouldNotConnect
132
+ raise e
133
+ else
134
+ raise DBGeni::MigrationApplyFailed, self.to_s
135
+ end
136
+ ensure
137
+ @logfile = migrator.logfile
138
+ @error_messages = migrator.migration_errors
139
+ end
140
+ end
141
+
142
+ def verify!(config, connection)
143
+ end
144
+
145
+ def set_pending(config, connection)
146
+ set_env(config, connection)
147
+ set_pending!
148
+ end
149
+
150
+ def set_completed(config, connection)
151
+ set_env(config, connection)
152
+ set_completed!
153
+ end
154
+
155
+ def set_failed(config, connection)
156
+ set_env(config, connection)
157
+ set_failed!
158
+ end
159
+
160
+ def set_rolledback(config, connection)
161
+ set_env(config, connection)
162
+ set_rolledback!
163
+ end
164
+
165
+ def to_s
166
+ "#{@sequence}::#{@name}"
167
+ end
168
+
169
+ def convert_migration(config)
170
+ @runnable_migration = FileConverter.convert(@directory, @migration_file, config)
171
+ end
172
+
173
+ def convert_rollback(config)
174
+ @runnable_rollback = FileConverter.convert(@directory, @rollback_file, config)
175
+ end
176
+
177
+ def runnable_migration
178
+ if @runnable_migration
179
+ @runnable_migration
180
+ else
181
+ File.join(@directory, @migration_file)
182
+ end
183
+ end
184
+
185
+ def runnable_rollback
186
+ if @runnable_rollback
187
+ @runnable_rollback
188
+ else
189
+ File.join(@directory, @rollback_file)
190
+ end
191
+ end
192
+
193
+
194
+ private
195
+
196
+ def set_pending!
197
+ insert_or_set_state(PENDING)
198
+ end
199
+
200
+ def set_completed!
201
+ insert_or_set_state(COMPLETED)
202
+ end
203
+
204
+ def set_failed!
205
+ insert_or_set_state(FAILED)
206
+ end
207
+
208
+ def set_rolledback!
209
+ insert_or_set_state(ROLLEDBACK)
210
+ end
211
+
212
+ def set_env(config, connection)
213
+ @config = config
214
+ @connection = connection
215
+ end
216
+
217
+ def insert_or_set_state(state)
218
+ results = existing_db_record
219
+ if results.length == 0 then
220
+ add_db_record(state)
221
+ else
222
+ update_db_state(state)
223
+ end
224
+ end
225
+
226
+ def existing_db_record
227
+ results = @connection.execute("select sequence_or_hash, migration_name, migration_type, migration_state, start_dtm, completed_dtm
228
+ from #{@config.db_table}
229
+ where sequence_or_hash = ?
230
+ and migration_name = ?
231
+ and migration_type = ?", @sequence, @name, @migration_type)
232
+ end
233
+ #"
234
+
235
+ def add_db_record(state)
236
+ results = @connection.execute("insert into #{@config.db_table}
237
+ (
238
+ sequence_or_hash,
239
+ migration_name,
240
+ migration_type,
241
+ migration_state,
242
+ start_dtm
243
+ )
244
+ values
245
+ (
246
+ ?,
247
+ ?,
248
+ ?,
249
+ ?,
250
+ #{@connection.date_placeholder('sdtm')}
251
+ )", @sequence, @name, @migration_type, state, @connection.date_as_string(Time.now))
252
+ end
253
+
254
+
255
+ def update_db_state(state)
256
+ # What to set the dates to? If going to PENDING, then you want to make
257
+ # completed_dtm null and reset start_dtm to now.
258
+ #
259
+ # If going to anything else, then set completed_dtm to now
260
+ if state == PENDING
261
+ results = @connection.execute("update #{@config.db_table}
262
+ set migration_state = ?,
263
+ completed_dtm = null,
264
+ start_dtm = #{@connection.date_placeholder('sdtm')}
265
+ where sequence_or_hash = ?
266
+ and migration_name = ?
267
+ and migration_type = ?", state, @connection.date_as_string(Time.now), @sequence, @name, @migration_type)
268
+ else
269
+ results = @connection.execute("update #{@config.db_table}
270
+ set migration_state = ?,
271
+ completed_dtm = #{@connection.date_placeholder('sdtm')}
272
+ where sequence_or_hash = ?
273
+ and migration_name = ?
274
+ and migration_type = ?", state, @connection.date_as_string(Time.now), @sequence, @name, @migration_type)
275
+ end
276
+ end
277
+
278
+
279
+ def parse_file
280
+ # filename is made up of 3 parts
281
+ # Sequence - YYYYMMDDHH(24)MI - ie datestamp down to minute
282
+ # Operation - allowed are up, down, verify
283
+ # Migration_name - any amount of text
284
+ # eg
285
+ # 201107011644_up_my_shiny_new_table.sql
286
+ #
287
+ unless @migration_file =~ /^(\d{12})_up_(.+)\.sql$/
288
+ raise DBGeni::MigrationFilenameInvalid, self.migration_file
289
+ end
290
+ @sequence = $1
291
+ @name = $2
292
+ end
293
+
294
+ def ensure_file_exists(dir='up')
295
+ unless File.exists? File.join(@directory, self.migration_file(dir))
296
+ raise DBGeni::MigrationFileNotExist, File.join(@directory, migration_file(dir))
297
+ end
298
+ end
299
+
300
+ end
301
+
302
+ end