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