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,9 @@
1
+ class <%= migration_class_name %> < Nando::Migration
2
+ def up
3
+
4
+ end
5
+
6
+ def down
7
+
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class <%= migration_class_name %> < Nando::MigrationWithoutTransaction
2
+ def up
3
+
4
+ end
5
+
6
+ def down
7
+
8
+ end
9
+ end
@@ -0,0 +1,372 @@
1
+ module MigrationUpdater
2
+
3
+ def self.update_migration (migration_file_path, working_directory, functions_to_add)
4
+
5
+ if !File.file?(migration_file_path)
6
+ raise Nando::GenericError.new("No file '#{migration_file_path}' was found")
7
+ end
8
+
9
+ @working_directory = working_directory
10
+ @lines = File.readlines(migration_file_path)
11
+ up_keyword = 'NANDO'
12
+ @up_annotation_trigger = "(\s*)(?:#\s(?:#{up_keyword}:)(?:\s)?)(.*)" # match 1 is the space to indent, and match 2 is the file being linked
13
+ @last_scanned_index = 0
14
+
15
+ @changed_file = false
16
+ @source_files_copied = []
17
+
18
+ if !functions_to_add.nil?
19
+ add_new_annotations_to_file_lines(functions_to_add)
20
+ end
21
+
22
+ @curr_migration_version, _ = NandoUtils.get_migration_version_and_name_from_file_path(migration_file_path)
23
+ find_and_update()
24
+
25
+ if @changed_file
26
+ File.write(migration_file_path, @lines.join(''))
27
+ end
28
+ end
29
+
30
+ # iterates the file, finds annotations and updates them
31
+ def self.find_and_update
32
+ do_another_loop = false
33
+ prepend_append_execute = false
34
+
35
+ starting_sql_index = ending_sql_index = nil
36
+
37
+ line_match = nil
38
+
39
+ execute_match = nil
40
+ ending_execute_match = nil
41
+
42
+ annotation_file = nil
43
+ duplicate_annotation = false
44
+
45
+ @lines.each_with_index do |line, line_index|
46
+ line_match = line.match(@up_annotation_trigger)
47
+
48
+ # found a annotation that has not been updated
49
+ if !line_match.nil? && line_index > @last_scanned_index
50
+ @last_scanned_index = line_index
51
+ do_another_loop = true
52
+ annotation_file = line_match[2]
53
+
54
+ if @source_files_copied.include?(annotation_file)
55
+ _warn "The file '#{annotation_file}' has already been updated in the current migration, remove the duplicate annotation! Skipping!"
56
+ duplicate_annotation = true
57
+ break
58
+ else
59
+ @source_files_copied.push(annotation_file)
60
+ end
61
+
62
+ # find beginning of block
63
+ if execute_match = @lines[line_index+1].match("(.*)update_function(.*)SQL(.*)\n")
64
+ starting_sql_index = line_index + 1
65
+ ending_trigger = execute_match[1] + 'SQL' + "\n"
66
+
67
+ # find ending of block
68
+ for ending_block_index in line_index+2..@lines.length-1 do
69
+ if ending_execute_match = @lines[ending_block_index].match(ending_trigger)
70
+ ending_sql_index = ending_block_index
71
+ break
72
+ end
73
+ end
74
+ # we need to create an update_function block, since one does not exist
75
+ else
76
+ starting_sql_index = line_index + 1
77
+ ending_sql_index = starting_sql_index - 1
78
+ prepend_append_execute = true
79
+ end
80
+ break
81
+ end
82
+ end
83
+
84
+ if do_another_loop
85
+ # update the block for the current annotation (if not a duplicate)
86
+ if !(starting_sql_index.nil? && ending_sql_index.nil?) && !duplicate_annotation
87
+ curr_source_file = "#{@working_directory}/#{annotation_file}"
88
+
89
+ if File.file?(curr_source_file)
90
+ # delete from array lines for current update_function block (if there is any)
91
+ @lines.slice!(starting_sql_index, (ending_sql_index - starting_sql_index) + 1)
92
+ # insert into array new update_function block
93
+ curr_file_lines = File.readlines(curr_source_file)
94
+ # create execute block
95
+ if prepend_append_execute
96
+ curr_file_lines.map! { |line| line == "\n" ? line : (" " + line_match[1] + line) }
97
+ curr_file_lines[curr_file_lines.length - 1].rstrip!
98
+ curr_file_lines.insert(0, line_match[1] + "update_function <<-'SQL'\n")
99
+ curr_file_lines.push("\n" + line_match[1] + "SQL\n")
100
+ else
101
+ curr_file_lines.map! { |line| line == "\n" ? line : (" " + execute_match[1] + line) }
102
+ curr_file_lines[curr_file_lines.length - 1].rstrip!
103
+ curr_file_lines.insert(0, execute_match[0])
104
+ curr_file_lines.push("\n" + ending_execute_match[0])
105
+ end
106
+ @lines.insert(starting_sql_index, *curr_file_lines)
107
+
108
+ find_and_update_respective_down_directive(annotation_file, curr_source_file, line_match[1])
109
+
110
+ @last_scanned_index = starting_sql_index + curr_file_lines.length - 1
111
+ @changed_file = true
112
+ _success "Updated content for #{curr_source_file}"
113
+ else
114
+ _warn "Couldn't find file: '#{curr_source_file}'! Skipping that one!"
115
+ end
116
+ end
117
+ find_and_update()
118
+ end
119
+
120
+ end
121
+
122
+ def self.find_and_update_respective_down_directive (source_file, source_file_full_path, indent_space)
123
+ # if a NANDO directive is being updated, then we need to find the respetive down (X)
124
+ # start from the top of the file, try and find the directive (may try to optmize this later, to start at "def down") (X)
125
+ # if the directive is found, update it.
126
+ # if not, create one at the bottom of the file and update it
127
+ # matching is done using the source_file, but the code to fill the down comes from previous migrations (NOT THE FILE)
128
+
129
+ down_keyword = 'NANDO_DOWN'
130
+ down_annotation_index = nil
131
+ down_annotation_trigger = "(\s*)(?:#\s(?:#{down_keyword}:)(?:\s)?)(?:#{source_file})" # match 1 is the space to indent
132
+
133
+ down_method_index = nil
134
+ down_method_trigger = "(\s*)def(?:\s*)down(.*)"
135
+ down_method_end_trigger = nil # to find respective "end"
136
+
137
+ line_match = nil
138
+
139
+ # find down annotation
140
+ @lines.each_with_index do |line, line_index|
141
+ # find start of down method (ignore before that)
142
+ if down_method_index.nil?
143
+ line_match = line.match(down_method_trigger)
144
+ if !line_match.nil?
145
+ # _debug 'Found beginning of down method'
146
+ down_method_index = line_index
147
+ down_method_indent = line_match[1]
148
+ down_method_end_trigger = "^(?:#{line_match[1]}end).*"
149
+ end
150
+ next
151
+ end
152
+
153
+ # start looking for an annotation
154
+ line_match = line.match(down_annotation_trigger)
155
+
156
+ # found a annotation that has not been updated
157
+ if !line_match.nil?
158
+ # _debug "Found matching annotation for: '#{source_file}'"
159
+ down_annotation_index = line_index
160
+ break
161
+ end
162
+ end
163
+
164
+ # no annotation found, create one
165
+ if down_annotation_index.nil?
166
+ # _debug "Did not find respective down annotation for: '#{source_file}'"
167
+
168
+ @lines.each_with_index do |line, line_index|
169
+ # ignore before "def down"
170
+ if line_index <= down_method_index
171
+ next
172
+ end
173
+
174
+ # look for the "end" of "def down"
175
+ line_match = line.match(down_method_end_trigger)
176
+ if !line_match.nil?
177
+ # _debug "Found the end of 'def down' at index: #{line_index}"
178
+ @lines.insert(line_index, "\n") # insert empty line to keep annotations 1 line apart
179
+ @lines.insert(line_index, indent_space + "# #{down_keyword}: #{source_file}\n")
180
+ down_annotation_index = line_index
181
+ break
182
+ end
183
+ end
184
+ end
185
+
186
+ # update annotation
187
+ source_file_text = File.readlines(source_file_full_path).join(' ')
188
+ # all capture groups are non-greedy, and include any character since names may have '.' for example
189
+ function_info_match = /CREATE (?:OR REPLACE)? FUNCTION (.*?)\((.*?)\) RETURNS (.*?) AS \$\w*\$/im.match(source_file_text) # case insenstive and multi-line
190
+
191
+ if function_info_match.nil?
192
+ raise Nando::GenericError.new("No function definition was found in '#{source_file_full_path}'")
193
+ end
194
+
195
+ function_name = function_info_match[1].strip
196
+ # function_args = function_info_match[2].strip
197
+ # function_return = function_info_match[3].strip
198
+
199
+ file_regex = "CREATE \\(OR REPLACE\\)\\? FUNCTION #{function_name}"
200
+
201
+ files_with_function = %x[grep -irl -e "#{file_regex}" #{NandoMigrator.instance.working_dir}/#{NandoMigrator.instance.migration_dir}].split("\n").sort().reverse()
202
+
203
+ function_previous_block = nil
204
+
205
+ for curr_file_path in files_with_function do
206
+ # _debug curr_file_path
207
+
208
+ if curr_file_path.include?(@curr_migration_version)
209
+ _debug 'Ignore self while updating'
210
+ next
211
+ end
212
+
213
+ curr_file_version, _ = NandoUtils.get_migration_version_and_name_from_file_path(curr_file_path)
214
+ if curr_file_version.to_i > @curr_migration_version.to_i
215
+ _debug 'Skipping migrations more recent than the current one'
216
+ next
217
+ end
218
+
219
+ up_line_index = nil
220
+ down_line_index = nil
221
+ function_line_index = nil
222
+
223
+ curr_file_lines = File.readlines(curr_file_path)
224
+
225
+ # find up, down and line with definition
226
+ curr_file_lines.each_with_index do |line, line_index|
227
+ if up_line_index.nil? && line.match(/(?:\s*)def(?:\s*)up/) then up_line_index = line_index; end
228
+ if down_line_index.nil? && line.match(/(?:\s*)def(?:\s*)down/) then down_line_index = line_index; end
229
+ if function_line_index.nil? && line.match(/CREATE (?:OR REPLACE)? FUNCTION #{function_name}/i) then function_line_index = line_index; end
230
+
231
+ if !up_line_index.nil? && !down_line_index.nil? && !function_line_index.nil?
232
+ # _debug "Found all 3 lines"
233
+ break
234
+ end
235
+ end
236
+
237
+ # TODO: only catch definition between up and down indexes
238
+
239
+ # _debug "up: #{up_line_index} | down: #{down_line_index} | function: #{function_line_index}"
240
+
241
+ # TODO: add some validations over current block
242
+ # TODO: match function with correct parameters/return value
243
+ # TODO: isolate into function that extracts block
244
+
245
+ block_indent = nil
246
+ block_start_index = nil
247
+ block_end_index = nil
248
+
249
+ # get block around function
250
+ for block_line_index in (0..function_line_index).to_a.reverse() do
251
+ block_line = curr_file_lines[block_line_index]
252
+ if block_match = block_line.match("(.*)update_function(?:.*)SQL(?:.*)\n")
253
+ block_indent = block_match[1]
254
+ block_start_index = block_line_index
255
+ break
256
+ end
257
+ end
258
+
259
+ for block_line_index in function_line_index..curr_file_lines.length do
260
+ block_line = curr_file_lines[block_line_index]
261
+ if block_match = block_line.match("^#{block_indent}SQL(?:.*)\n")
262
+ block_end_index = block_line_index
263
+ break
264
+ end
265
+ end
266
+
267
+ function_block = []
268
+ for block_line_index in block_start_index..block_end_index do
269
+ function_block.push(curr_file_lines[block_line_index])
270
+ end
271
+
272
+ function_previous_block = function_block.join('')
273
+ break
274
+
275
+ end
276
+
277
+ if function_previous_block.nil?
278
+ # TODO: decide if I need to do anything more when I don't find a previous definition (like add a DROP)
279
+ _warn "No previous definition was found for function '#{function_name}'"
280
+ return
281
+ end
282
+
283
+ # erase previous block (if one exists)
284
+ # TODO: there is similar logic above, maybe resolve to a single function
285
+ if curr_down_block_start = @lines[down_annotation_index+1].match("(.*)update_function(.*)SQL(.*)\n")
286
+ ending_trigger = curr_down_block_start[1] + 'SQL' + "\n"
287
+ starting_sql_index = down_annotation_index + 1
288
+ ending_sql_index = nil
289
+
290
+ # find ending of block
291
+ for ending_down_block_index in down_annotation_index+2..@lines.length-1 do
292
+ if ending_execute_match = @lines[ending_down_block_index].match(ending_trigger)
293
+ ending_sql_index = ending_down_block_index
294
+ break
295
+ end
296
+ end
297
+
298
+ # TODO: add protections here if it does not find the end of the block
299
+ # delete from array lines for current update_function block
300
+ @lines.slice!(starting_sql_index, (ending_sql_index - starting_sql_index) + 1)
301
+ end
302
+
303
+ @lines.insert(down_annotation_index + 1, function_previous_block)
304
+
305
+ end
306
+
307
+ ## adds new annotations to bottom of "up" method
308
+ def self.add_new_annotations_to_file_lines (functions_to_add)
309
+ migration_file_lines = @lines
310
+ _, up_end_index, _, _ = get_migration_file_up_and_down_limits(migration_file_lines)
311
+
312
+ # insert annotations at the bottom of the "up" method
313
+ functions_to_add.each do |curr_function_path|
314
+ _debug curr_function_path
315
+ annotation = NandoUtils.get_annotation_from_file_path(curr_function_path)
316
+ migration_file_lines.insert(up_end_index, annotation)
317
+ migration_file_lines.insert(up_end_index, "\n") # insert empty line to separate annotations
318
+ end
319
+
320
+ @lines = migration_file_lines
321
+ end
322
+
323
+ def self.get_migration_file_up_and_down_limits (file_lines)
324
+ up_start_index = nil
325
+ up_end_index = nil
326
+ down_start_index = nil
327
+ down_end_index = nil
328
+
329
+ curr_state = nil
330
+ def_indent = nil
331
+
332
+ # find up, down (beggining and end of functions are done by finding an "end" with the same indentation)
333
+ file_lines.each_with_index do |line, line_index|
334
+ case curr_state
335
+ when 'up', 'down'
336
+ # look for end of up/down
337
+ if line_match = line.match(/^#{def_indent}end$/)
338
+ if curr_state == 'up'
339
+ up_end_index = line_index
340
+ else
341
+ down_end_index = line_index
342
+ end
343
+ curr_state = nil
344
+ def_indent = nil
345
+ end
346
+ else
347
+ # read line trying to find beggining of "up" or "down"
348
+ if line_match = line.match(/(\s*)def(?:\s*)up/) then
349
+ curr_state = 'up'
350
+ def_indent = line_match[1]
351
+ up_start_index = line_index
352
+ next
353
+ end
354
+ if line_match = line.match(/(\s*)def(?:\s*)down/) then
355
+ curr_state = 'down'
356
+ def_indent = line_match[1]
357
+ down_start_index = line_index
358
+ next
359
+ end
360
+ end
361
+
362
+ if !up_end_index.nil? && !down_end_index.nil?
363
+ # _debug "Found up and down"
364
+ break
365
+ end
366
+ end
367
+
368
+ # TODO: might add some checks if the index values don't make sense
369
+ return up_start_index, up_end_index, down_start_index, down_end_index
370
+ end
371
+
372
+ end
@@ -0,0 +1,22 @@
1
+ module NandoUtils
2
+
3
+ def self.get_annotation_from_file_path (file_path)
4
+ return " # NANDO: #{file_path}\n"
5
+ end
6
+
7
+ # accepts either a path or a file name
8
+ def self.get_migration_version_and_name_from_file_path (file_path)
9
+ file_name = file_path.split('/')[-1] # get last part of the file path
10
+ match = /^(\d+)\_(.*)\.rb$/.match(file_name)
11
+ if match.nil?
12
+ raise Nando::GenericError.new("'#{file_name}' is not a valid file name")
13
+ end
14
+ migration_version = match[1] # by this point, the file name has already been validated, so I don't need to double check
15
+ migration_name = match[2]
16
+ return migration_version, migration_name
17
+ end
18
+
19
+ # TODO: move helper methods here, to not fill the main files
20
+
21
+ end
22
+
@@ -0,0 +1,3 @@
1
+ module Nando
2
+ VERSION = "1.0.6"
3
+ end
data/lib/nando.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'nando'
2
+ require 'nando/errors'
3
+ require 'nando/version'
4
+ require 'nando/migrator'
5
+ require 'nando/logger'
6
+ require 'nando/migration'
7
+ require 'nando/generator'
8
+ require 'nando/parser'
9
+ require 'nando/updater'
10
+ require 'nando/interface'
11
+ require 'nando/schema_diff'
12
+ require 'nando/utils'
data/nando.gemspec ADDED
@@ -0,0 +1,44 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "nando/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "nando"
8
+ spec.version = Nando::VERSION
9
+ spec.authors = ["Fernando Alves"]
10
+ spec.email = ["fernando.alves@cldware.com"]
11
+
12
+ spec.summary = %q{Nando AdmiNs Database Objects}
13
+ spec.description = %q{NANDO - Nando AdmiNs Database Objects}
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
+ else
20
+ raise "RubyGems 2.0 or newer is required to protect against " \
21
+ "public gem pushes."
22
+ end
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_development_dependency "bundler", "~> 1.17.3"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "rspec", "~> 3.2"
36
+ spec.add_development_dependency "byebug"
37
+ # dependencies
38
+ # TODO: review versions
39
+ spec.add_dependency "pg", "~> 1.2.3"
40
+ spec.add_dependency "optparse", "~> 0.1.0"
41
+ spec.add_dependency "dotenv", "~> 2.7.6"
42
+ spec.add_dependency "colorize", "~> 0.8.1"
43
+ spec.add_dependency "awesome_print", "~> 1.8.0"
44
+ end