nando 1.0.6

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.
@@ -0,0 +1,369 @@
1
+ require 'pg'
2
+ require 'dotenv'
3
+ require 'awesome_print'
4
+ require 'singleton'
5
+
6
+ begin
7
+ require 'byebug'
8
+ rescue LoadError
9
+ end
10
+
11
+ Dotenv.load('.env')
12
+
13
+ class NandoMigrator
14
+ include Singleton
15
+
16
+ def initialize
17
+ @migration_table = ENV['MIGRATION_TABLE_NAME'] || 'schema_migrations'
18
+ @migration_field = ENV['MIGRATION_TABLE_FIELD'] || 'version'
19
+ @migration_dir = ENV['MIGRATION_DIR'] || 'db/migrate'
20
+
21
+ # accepts urls in the same format as dbmate => protocol://username:password@host:port/database_name
22
+ match = /([a-zA-Z]+)\:\/\/(\w+)\:(\w+)\@([\w\.]+)\:(\d+)\/(\w+)/.match(ENV['DATABASE_URL'])
23
+
24
+ raise Nando::GenericError.new('No .env file was found, or no valid DATABASE_URL variable was found in it') if match.nil?
25
+
26
+ @db_protocol = match[1]
27
+ @db_username = match[2]
28
+ @db_password = match[3]
29
+ @db_host = match[4]
30
+ @db_port = match[5]
31
+ @db_name = match[6]
32
+
33
+ @working_dir = ENV['WORKING_DIR'] || '.'
34
+ @schema_variable = ENV['SCHEMA_VARIABLE'] || '#{schema_name}'
35
+ end
36
+
37
+ attr_accessor :migration_table, :migration_field, :migration_dir, :working_dir, :schema_variable
38
+
39
+ # --------------------------------------------------------
40
+
41
+ # creates a new migration for the tool
42
+ def new_migration (options = {}, args = [])
43
+ migration_name = args[0].underscore
44
+ migration_type = options[:type] || Nando::Migration.name.demodulize # default type is migration with transaction
45
+ migration_timestamp = Time.now.strftime("%Y%m%d%H%M%S") # same format as ActiveRecord: year-month-day-hour-minute-second
46
+
47
+ final_migration_type = camelize_migration_type(migration_type)
48
+
49
+ migration_file_name = "#{migration_timestamp}_#{migration_name}"
50
+ migration_file_path = "#{@migration_dir}/#{migration_file_name}.rb"
51
+
52
+ MigrationGenerator::create_migration_file(migration_file_path, migration_name, final_migration_type)
53
+ end
54
+
55
+ # migrates all missing migrations
56
+ def migrate (options = {})
57
+ _debug 'Migrating!'
58
+
59
+ migration_files = get_migration_files(@migration_dir)
60
+
61
+ if migration_files.length == 0
62
+ raise Nando::GenericError.new("No migration files were found in '#{@migration_dir}'")
63
+ end
64
+
65
+ @db_connection = get_database_connection()
66
+ create_schema_migrations_table_if_not_exists()
67
+ applied_migrations = get_applied_migrations()
68
+
69
+ for filename in migration_files do
70
+ migration_version, migration_name = NandoUtils.get_migration_version_and_name_from_file_path(filename)
71
+
72
+ if applied_migrations[migration_version]
73
+ next
74
+ end
75
+
76
+ execute_migration_method(:up, filename, migration_name, migration_version)
77
+ end
78
+
79
+ end
80
+
81
+ # applies specific migration
82
+ def apply (options = {}, args = [])
83
+ _debug 'Applying!'
84
+
85
+ migration_version_to_apply = args[0].to_s
86
+ migration_files = get_migration_files(@migration_dir)
87
+
88
+ if migration_files.length == 0
89
+ raise Nando::GenericError.new("No migration files were found in '#{@migration_dir}'")
90
+ end
91
+
92
+ @db_connection = get_database_connection()
93
+ create_schema_migrations_table_if_not_exists()
94
+ applied_migrations = get_applied_migrations()
95
+
96
+ migration_has_run = applied_migrations.include?(migration_version_to_apply)
97
+ found_migration = false
98
+
99
+ for filename in migration_files do
100
+ migration_version, migration_name = NandoUtils.get_migration_version_and_name_from_file_path(filename)
101
+
102
+ if migration_version.to_s != migration_version_to_apply.to_s
103
+ next
104
+ end
105
+
106
+ found_migration = true
107
+ execute_migration_method(:up, filename, migration_name, migration_version, migration_has_run)
108
+ _debug 'There should only be 1 migration with each version, so we can break'
109
+ break
110
+ end
111
+
112
+ if !found_migration
113
+ _error "No migration file with version '#{migration_version_to_apply}' was found!"
114
+ end
115
+ end
116
+
117
+ # rollbacks 1 migration (or more depending on argument)
118
+ def rollback (options = {})
119
+ _debug 'Rollback!'
120
+
121
+ rollback_count = 1 # TODO: temporary constant, add option in command interface
122
+
123
+ @db_connection = get_database_connection()
124
+ create_schema_migrations_table_if_not_exists()
125
+ migrations_to_revert = get_migrations_to_revert(rollback_count)
126
+
127
+ if migrations_to_revert.length == 0
128
+ raise Nando::GenericError.new("There are no migrations to revert")
129
+ end
130
+
131
+ migration_files = get_migration_files_to_rollback(@migration_dir, migrations_to_revert)
132
+ if migration_files.length == 0
133
+ # TODO: this won't work as expected if we start accepting rollbacks of multiple files, since as long as 1 file is valid it will be rollbacked
134
+ raise Nando::GenericError.new("Could not find any valid files in '#{@migration_dir}' that match the migrations to revert #{migrations_to_revert}")
135
+ end
136
+
137
+ for migration_index in 0...migration_files.length do
138
+ filename = migration_files[migration_index]
139
+ migration_version, migration_name = NandoUtils.get_migration_version_and_name_from_file_path(filename)
140
+
141
+ execute_migration_method(:down, filename, migration_name, migration_version)
142
+ end
143
+ end
144
+
145
+ # TODO: might add a migrate:down to distinguish from rollback, similarly to ActiveRecord
146
+
147
+ # parses migrations from dbmate to nando
148
+ def parse (options = {}, args = [])
149
+ _debug 'Parsing!'
150
+
151
+ NandoParser.parse_from_dbmate(args[0], args[1])
152
+ end
153
+
154
+ def baseline ()
155
+ _debug 'Creating Baseline!'
156
+
157
+ migration_name = "baseline".underscore
158
+ migration_timestamp = Time.now.strftime("%Y%m%d%H%M%S") # same format as ActiveRecord: year-month-day-hour-minute-second
159
+
160
+ migration_file_name = "#{migration_timestamp}_#{migration_name}"
161
+ migration_file_path = "#{@migration_dir}/#{migration_file_name}.rb"
162
+
163
+ MigrationGenerator::create_baseline_file(migration_file_path, migration_name)
164
+ end
165
+
166
+ def update_migration (options = {}, args = [])
167
+ _debug 'Updating!'
168
+ functions_to_add = options[:functions_to_add]
169
+
170
+ MigrationUpdater.update_migration(args[0], @working_dir, functions_to_add)
171
+ end
172
+
173
+ def diff_schemas (options = {}, args = [])
174
+ _debug 'Schema Diff'
175
+
176
+ NandoSchemaDiff.diff_schemas(args[0], args[1])
177
+ end
178
+
179
+ # --------------------------------------------------------
180
+
181
+ def get_migration_files (directory)
182
+ if !File.directory?(directory)
183
+ raise Nando::GenericError.new("No directory '#{directory}' was found")
184
+ end
185
+ files = Dir.children(directory)
186
+
187
+ migration_files = []
188
+ for filename in files do
189
+ if !/^(\d+)\_(.*)\.rb$/.match(filename)
190
+ _warn "#{filename} does not have a valid migration name. Skipping!"
191
+ next
192
+ end
193
+
194
+ migration_files.push(filename)
195
+ end
196
+
197
+ migration_files.sort! # sort to ensure the migrations are executed chronologically
198
+ end
199
+
200
+ def get_migration_files_to_rollback (directory, versions_to_rollback)
201
+ if !File.directory?(directory)
202
+ raise Nando::GenericError.new("No directory '#{directory}' was found")
203
+ end
204
+ files = Dir.children(directory)
205
+
206
+ migration_files = []
207
+ for filename in files do
208
+ match = /^(\d+)\_(.*)\.rb$/.match(filename)
209
+ if match.nil?
210
+ _warn "#{filename} does not have a valid migration name. Skipping!"
211
+ next
212
+ end
213
+
214
+ if versions_to_rollback.include?(match[1])
215
+ migration_files.push(filename)
216
+ end
217
+ end
218
+
219
+ migration_files.sort.reverse # sort and reverse to ensure the migrations are executed chronologically (backwards)
220
+ end
221
+
222
+ def get_applied_migrations ()
223
+ # run the query
224
+ results = @db_connection.exec("SELECT * FROM #{@migration_table} ORDER BY #{@migration_field} asc")
225
+
226
+ applied_migrations = {}
227
+ # puts "---------------------------------"
228
+ # puts "Applied migrations:"
229
+ results.each{ |row|
230
+ # puts "#{row[@migration_field]}"
231
+ applied_migrations[row[@migration_field]] = true
232
+ }
233
+ # puts "---------------------------------"
234
+ return applied_migrations
235
+ end
236
+
237
+ def get_migrations_to_revert (count)
238
+ # run the query
239
+ results = @db_connection.exec("SELECT * FROM #{@migration_table} ORDER BY #{@migration_field} desc LIMIT #{count}")
240
+
241
+ migrations_to_rollback = []
242
+ # puts "---------------------------------"
243
+ # puts "Rollbacked migrations:"
244
+ results.each{ |row|
245
+ # puts "#{row[@migration_field]}"
246
+ migrations_to_rollback.push(row[@migration_field])
247
+ }
248
+ # puts "---------------------------------"
249
+ return migrations_to_rollback
250
+ end
251
+
252
+ def execute_migration_method (method, filename, migration_name, migration_version, skip_insert_version = false)
253
+ if method == :up
254
+ migrating = true
255
+ else
256
+ migrating = false
257
+ end
258
+
259
+ puts migrating ? "Applying: #{filename}" : "Reverting: #{filename}"
260
+
261
+ require "./#{@migration_dir}/#{filename}"
262
+
263
+ class_const = get_migration_class(migration_name)
264
+
265
+ migration_class = class_const.new(@db_connection, migration_version)
266
+ begin
267
+ migration_class.execute_migration(method)
268
+ rescue => exception
269
+ raise Nando::GenericError.new(exception)
270
+ end
271
+
272
+ if !skip_insert_version
273
+ update_migration_table(migration_version, migrating)
274
+ else
275
+ puts "Migration '#{migration_version}' was already in '#{@migration_table}', applying but not re-inserting it into the table"
276
+ end
277
+ end
278
+
279
+ def update_migration_table (version, to_apply = true)
280
+ if to_apply
281
+ @db_connection.exec("INSERT INTO #{@migration_table} (#{@migration_field}) VALUES (#{version})")
282
+ else
283
+ @db_connection.exec("DELETE FROM #{@migration_table} WHERE #{@migration_field} = '#{version}'")
284
+ end
285
+ end
286
+
287
+ def camelize_migration_type (migration_type)
288
+ camelize_migration_type = migration_type.camelize
289
+ if !['Migration', 'MigrationWithoutTransaction'].include?(camelize_migration_type)
290
+ raise Nando::GenericError.new("Invalid migration type '#{migration_type}'")
291
+ end
292
+ return camelize_migration_type
293
+ end
294
+
295
+ def get_migration_class (filename)
296
+ name = filename.camelize
297
+ Object.const_defined?(name) ? Object.const_get(name) : Object.const_missing(name) # if the constant does not exist, raise error
298
+ end
299
+
300
+ def create_schema_migrations_table_if_not_exists
301
+ results = @db_connection.exec("SELECT EXISTS (
302
+ SELECT FROM information_schema.tables
303
+ WHERE table_schema = 'public'
304
+ AND table_name = '#{@migration_table}')")
305
+
306
+ if results[0]["exists"] == 'f'
307
+ _warn "Table '#{@migration_table}' does not exist, creating one"
308
+ @db_connection.exec("CREATE TABLE public.#{@migration_table} (
309
+ #{@migration_field} VARCHAR(255) PRIMARY KEY,
310
+ executed_at timestamp DEFAULT NOW()
311
+ )")
312
+ end
313
+ end
314
+
315
+ def get_database_connection
316
+ begin
317
+ conn = PG::Connection.open(:host => @db_host,
318
+ :port => @db_port,
319
+ :dbname => @db_name,
320
+ :user => @db_username,
321
+ :password => @db_password)
322
+ rescue => exception
323
+ raise Nando::GenericError.new(exception)
324
+ end
325
+
326
+ return conn
327
+ end
328
+
329
+ end
330
+
331
+
332
+ class String
333
+ # used to convert to snake case (Rails)
334
+ def underscore
335
+ self.gsub(/::/, '/')
336
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
337
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2')
338
+ .tr("-", "_")
339
+ .downcase
340
+ end
341
+
342
+ # used to convert to camel or Pascal case (Rails)
343
+ def camelize (uppercase_first_letter = true)
344
+ string = self
345
+ if uppercase_first_letter
346
+ string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
347
+ else
348
+ string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
349
+ end
350
+ string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub("/", "::")
351
+ end
352
+
353
+ # gets the class/module name with the previous class/module names
354
+ def demodulize
355
+ self.split('::').last
356
+ end
357
+
358
+ # converts a string to boolean
359
+ def to_b
360
+ case self.downcase.strip
361
+ when 'true', 'yes', 'on', 't', '1', 'y', '=='
362
+ return true
363
+ when 'nil', 'null'
364
+ return nil
365
+ else
366
+ return false
367
+ end
368
+ end
369
+ end
@@ -0,0 +1,68 @@
1
+ require 'fileutils'
2
+
3
+ module NandoParser
4
+
5
+ def self.parse_from_dbmate (source_path, destination_path)
6
+ source_files = Dir.children(source_path)
7
+ source_files.sort! # this sort is not really necessary, but ensures the files are created in the same order of their name
8
+
9
+ puts "Found #{source_files.length} source files"
10
+
11
+ FileUtils.mkdir_p(destination_path)
12
+ clear_directory(destination_path)
13
+
14
+ for filename in source_files do
15
+ match = /^(\d+)\_([\w\_]+)\./.match(filename)
16
+ migration_version = match[1]
17
+ migration_name = match[2]
18
+ new_filename = "#{migration_version}_#{migration_name}.rb"
19
+ new_file = File.new(File.join(destination_path, new_filename), 'w')
20
+
21
+ source_file_lines = File.readlines(File.join(source_path, filename))
22
+ current_section = nil
23
+ up_method, down_method = '', ''
24
+ with_transaction = true
25
+ for line in source_file_lines do
26
+ next if /^--[\s|\\\/_]*$/.match(line) # if it's just a up/down comment, ignore
27
+
28
+ case current_section
29
+ when 'up'
30
+ if match = /--\smigrate:down(.*)/.match(line)
31
+ with_transaction = false if match[1].include?('transaction:false')
32
+ current_section = 'down'
33
+ else
34
+ up_method += " #{line}".rstrip + "\n"
35
+ end
36
+ when 'down'
37
+ down_method += " #{line}".rstrip + "\n"
38
+ else
39
+ if match = /--\smigrate:up(.*)/.match(line)
40
+ with_transaction = false if match[1].include?('transaction:false')
41
+ current_section = 'up'
42
+ end
43
+ end
44
+ end
45
+
46
+ # binding
47
+ migration_class_name = migration_name.camelize
48
+ migration_type = with_transaction ? Nando::Migration.name.demodulize : Nando::MigrationWithoutTransaction.name.demodulize
49
+ migration_up_code = up_method
50
+ migration_down_code = down_method
51
+ # TODO: check if binding logic is correct, and if pathing changes when it's a gem
52
+ MigrationGenerator.render_to_file(File.join(File.dirname(File.expand_path(__FILE__)), 'parser_templates/migration.rb'), binding, new_file)
53
+ end
54
+
55
+ dest_files = Dir.children(destination_path)
56
+ puts "Created #{dest_files.length} migrations in the destination folder"
57
+ end
58
+
59
+ def self.clear_directory (path)
60
+ Dir.foreach(path) do |f|
61
+ fn = File.join(path, f)
62
+ if f != '.' && f != '..'
63
+ File.delete(fn)
64
+ end
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,13 @@
1
+ class <%= migration_class_name %> < Nando::<%= migration_type %>
2
+ def up
3
+ execute <<-'SQL'
4
+ <%= migration_up_code %>
5
+ SQL
6
+ end
7
+
8
+ def down
9
+ execute <<-'SQL'
10
+ <%= migration_down_code %>
11
+ SQL
12
+ end
13
+ end