Module: MigrationUpdater

Defined in:
lib/nando/updater.rb

Class Method Summary collapse

Class Method Details

.add_new_annotations_to_file_lines(functions_to_add) ⇒ Object

adds new annotations to bottom of “up” method



308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/nando/updater.rb', line 308

def self.add_new_annotations_to_file_lines (functions_to_add)
  migration_file_lines = @lines
  _, up_end_index, _, _ = get_migration_file_up_and_down_limits(migration_file_lines)

  # insert annotations at the bottom of the "up" method
  functions_to_add.each do |curr_function_path|
    _debug curr_function_path
    annotation = NandoUtils.get_annotation_from_file_path(curr_function_path)
    migration_file_lines.insert(up_end_index, annotation)
    migration_file_lines.insert(up_end_index, "\n") # insert empty line to separate annotations
  end

  @lines = migration_file_lines
end

.find_and_updateObject

iterates the file, finds annotations and updates them



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/nando/updater.rb', line 31

def self.find_and_update
  do_another_loop = false
  prepend_append_execute = false

  starting_sql_index = ending_sql_index = nil

  line_match = nil

  execute_match = nil
  ending_execute_match = nil

  annotation_file = nil
  duplicate_annotation = false

  @lines.each_with_index do |line, line_index|
    line_match = line.match(@up_annotation_trigger)

    # found a annotation that has not been updated
    if !line_match.nil? && line_index > @last_scanned_index
      @last_scanned_index = line_index
      do_another_loop = true
      annotation_file = line_match[2]

      if @source_files_copied.include?(annotation_file)
        _warn "The file '#{annotation_file}' has already been updated in the current migration, remove the duplicate annotation! Skipping!"
        duplicate_annotation = true
        break
      else
        @source_files_copied.push(annotation_file)
      end

      # find beginning of block
      if execute_match = @lines[line_index+1].match("(.*)update_function(.*)SQL(.*)\n")
        starting_sql_index = line_index + 1
        ending_trigger = execute_match[1] + 'SQL' + "\n"

        # find ending of block
        for ending_block_index in line_index+2..@lines.length-1 do
          if ending_execute_match = @lines[ending_block_index].match(ending_trigger)
            ending_sql_index = ending_block_index
            break
          end
        end
      # we need to create an update_function block, since one does not exist
      else
        starting_sql_index = line_index + 1
        ending_sql_index = starting_sql_index - 1
        prepend_append_execute = true
      end
      break
    end
  end

  if do_another_loop
    # update the block for the current annotation (if not a duplicate)
    if !(starting_sql_index.nil? && ending_sql_index.nil?) && !duplicate_annotation
      curr_source_file = "#{@working_directory}/#{annotation_file}"

      if File.file?(curr_source_file)
        # delete from array lines for current update_function block (if there is any)
        @lines.slice!(starting_sql_index, (ending_sql_index - starting_sql_index) + 1)
        # insert into array new update_function block
        curr_file_lines = File.readlines(curr_source_file)
        # create execute block
        if prepend_append_execute
          curr_file_lines.map! { |line| line == "\n" ? line : ("  " + line_match[1] + line) }
          curr_file_lines[curr_file_lines.length - 1].rstrip!
          curr_file_lines.insert(0, line_match[1] + "update_function <<-'SQL'\n")
          curr_file_lines.push("\n" + line_match[1] + "SQL\n")
        else
          curr_file_lines.map! { |line| line == "\n" ? line : ("  " + execute_match[1] + line) }
          curr_file_lines[curr_file_lines.length - 1].rstrip!
          curr_file_lines.insert(0, execute_match[0])
          curr_file_lines.push("\n" + ending_execute_match[0])
        end
        @lines.insert(starting_sql_index, *curr_file_lines)

        find_and_update_respective_down_directive(annotation_file, curr_source_file, line_match[1])

        @last_scanned_index = starting_sql_index + curr_file_lines.length - 1
        @changed_file = true
        _success "Updated content for #{curr_source_file}"
      else
        _warn "Couldn't find file: '#{curr_source_file}'! Skipping that one!"
      end
    end
    find_and_update()
  end

end

.find_and_update_respective_down_directive(source_file, source_file_full_path, indent_space) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/nando/updater.rb', line 122

def self.find_and_update_respective_down_directive (source_file, source_file_full_path, indent_space)
  # if a NANDO directive is being updated, then we need to find the respetive down (X)
  # start from the top of the file, try and find the directive (may try to optmize this later, to start at "def down") (X)
  # if the directive is found, update it.
  # if not, create one at the bottom of the file and update it
  # matching is done using the source_file, but the code to fill the down comes from previous migrations (NOT THE FILE)

  down_keyword = 'NANDO_DOWN'
  down_annotation_index = nil
  down_annotation_trigger = "(\s*)(?:#\s(?:#{down_keyword}:)(?:\s)?)(?:#{source_file})" # match 1 is the space to indent

  down_method_index = nil
  down_method_trigger = "(\s*)def(?:\s*)down(.*)"
  down_method_end_trigger = nil # to find respective "end"

  line_match = nil

  # find down annotation
  @lines.each_with_index do |line, line_index|
    # find start of down method (ignore before that)
    if down_method_index.nil?
      line_match = line.match(down_method_trigger)
      if !line_match.nil?
        # _debug 'Found beginning of down method'
        down_method_index = line_index
        down_method_indent = line_match[1]
        down_method_end_trigger = "^(?:#{line_match[1]}end).*"
      end
      next
    end

    # start looking for an annotation
    line_match = line.match(down_annotation_trigger)

    # found a annotation that has not been updated
    if !line_match.nil?
      # _debug "Found matching annotation for: '#{source_file}'"
      down_annotation_index = line_index
      break
    end
  end

  # no annotation found, create one
  if down_annotation_index.nil?
    # _debug "Did not find respective down annotation for: '#{source_file}'"

    @lines.each_with_index do |line, line_index|
      # ignore before "def down"
      if line_index <= down_method_index
        next
      end

      # look for the "end" of "def down"
      line_match = line.match(down_method_end_trigger)
      if !line_match.nil?
        # _debug "Found the end of 'def down' at index: #{line_index}"
        @lines.insert(line_index, "\n") # insert empty line to keep annotations 1 line apart
        @lines.insert(line_index, indent_space + "# #{down_keyword}: #{source_file}\n")
        down_annotation_index = line_index
        break
      end
    end
  end

  # update annotation
  source_file_text = File.readlines(source_file_full_path).join(' ')
  # all capture groups are non-greedy, and include any character since names may have '.' for example
  function_info_match = /CREATE (?:OR REPLACE)? FUNCTION (.*?)\((.*?)\) RETURNS (.*?) AS \$\w*\$/im.match(source_file_text) # case insenstive and multi-line

  if function_info_match.nil?
    raise Nando::GenericError.new("No function definition was found in '#{source_file_full_path}'")
  end

  function_name = function_info_match[1].strip
  # function_args = function_info_match[2].strip
  # function_return = function_info_match[3].strip

  file_regex = "CREATE \\(OR REPLACE\\)\\? FUNCTION #{function_name}"

  files_with_function = %x[grep -irl -e "#{file_regex}" #{NandoMigrator.instance.working_dir}/#{NandoMigrator.instance.migration_dir}].split("\n").sort().reverse()

  function_previous_block = nil

  for curr_file_path in files_with_function do
    # _debug curr_file_path

    if curr_file_path.include?(@curr_migration_version)
      _debug 'Ignore self while updating'
      next
    end

    curr_file_version, _ = NandoUtils.get_migration_version_and_name_from_file_path(curr_file_path)
    if curr_file_version.to_i > @curr_migration_version.to_i
      _debug 'Skipping migrations more recent than the current one'
      next
    end

    up_line_index = nil
    down_line_index = nil
    function_line_index = nil

    curr_file_lines = File.readlines(curr_file_path)

    # find up, down and line with definition
    curr_file_lines.each_with_index do |line, line_index|
      if up_line_index.nil? && line.match(/(?:\s*)def(?:\s*)up/) then up_line_index = line_index; end
      if down_line_index.nil? && line.match(/(?:\s*)def(?:\s*)down/) then down_line_index = line_index; end
      if function_line_index.nil? && line.match(/CREATE (?:OR REPLACE)? FUNCTION #{function_name}/i) then function_line_index = line_index; end

      if !up_line_index.nil? && !down_line_index.nil? && !function_line_index.nil?
        # _debug "Found all 3 lines"
        break
      end
    end

    # TODO: only catch definition between up and down indexes

    # _debug "up: #{up_line_index} | down: #{down_line_index} | function: #{function_line_index}"

    # TODO: add some validations over current block
    # TODO: match function with correct parameters/return value
    # TODO: isolate into function that extracts block

    block_indent = nil
    block_start_index = nil
    block_end_index = nil

    # get block around function
    for block_line_index in (0..function_line_index).to_a.reverse() do
      block_line = curr_file_lines[block_line_index]
      if block_match = block_line.match("(.*)update_function(?:.*)SQL(?:.*)\n")
        block_indent = block_match[1]
        block_start_index = block_line_index
        break
      end
    end

    for block_line_index in function_line_index..curr_file_lines.length do
      block_line = curr_file_lines[block_line_index]
      if block_match = block_line.match("^#{block_indent}SQL(?:.*)\n")
        block_end_index = block_line_index
        break
      end
    end

    function_block = []
    for block_line_index in block_start_index..block_end_index do
      function_block.push(curr_file_lines[block_line_index])
    end

    function_previous_block = function_block.join('')
    break

  end

  if function_previous_block.nil?
    # TODO: decide if I need to do anything more when I don't find a previous definition (like add a DROP)
    _warn "No previous definition was found for function '#{function_name}'"
    return
  end

  # erase previous block (if one exists)
  # TODO: there is similar logic above, maybe resolve to a single function
  if curr_down_block_start = @lines[down_annotation_index+1].match("(.*)update_function(.*)SQL(.*)\n")
    ending_trigger = curr_down_block_start[1] + 'SQL' + "\n"
    starting_sql_index = down_annotation_index + 1
    ending_sql_index = nil

    # find ending of block
    for ending_down_block_index in down_annotation_index+2..@lines.length-1 do
      if ending_execute_match = @lines[ending_down_block_index].match(ending_trigger)
        ending_sql_index = ending_down_block_index
        break
      end
    end

    # TODO: add protections here if it does not find the end of the block
    # delete from array lines for current update_function block
    @lines.slice!(starting_sql_index, (ending_sql_index - starting_sql_index) + 1)
  end

  @lines.insert(down_annotation_index + 1, function_previous_block)

end

.get_migration_file_up_and_down_limits(file_lines) ⇒ Object



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/nando/updater.rb', line 323

def self.get_migration_file_up_and_down_limits (file_lines)
  up_start_index = nil
  up_end_index = nil
  down_start_index = nil
  down_end_index = nil

  curr_state = nil
  def_indent = nil

  # find up, down (beggining and end of functions are done by finding an "end" with the same indentation)
  file_lines.each_with_index do |line, line_index|
    case curr_state
    when 'up', 'down'
      # look for end of up/down
      if line_match = line.match(/^#{def_indent}end$/)
        if curr_state == 'up'
          up_end_index = line_index
        else
          down_end_index = line_index
        end
        curr_state = nil
        def_indent = nil
      end
    else
      # read line trying to find beggining of "up" or "down"
      if line_match = line.match(/(\s*)def(?:\s*)up/) then
        curr_state = 'up'
        def_indent = line_match[1]
        up_start_index = line_index
        next
      end
      if line_match = line.match(/(\s*)def(?:\s*)down/) then
        curr_state = 'down'
        def_indent = line_match[1]
        down_start_index = line_index
        next
      end
    end

    if !up_end_index.nil? && !down_end_index.nil?
      # _debug "Found up and down"
      break
    end
  end

  # TODO: might add some checks if the index values don't make sense
  return up_start_index, up_end_index, down_start_index, down_end_index
end

.update_migration(migration_file_path, working_directory, functions_to_add) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/nando/updater.rb', line 3

def self.update_migration (migration_file_path, working_directory, functions_to_add)

  if !File.file?(migration_file_path)
    raise Nando::GenericError.new("No file '#{migration_file_path}' was found")
  end

  @working_directory = working_directory
  @lines = File.readlines(migration_file_path)
  up_keyword = 'NANDO'
  @up_annotation_trigger = "(\s*)(?:#\s(?:#{up_keyword}:)(?:\s)?)(.*)" # match 1 is the space to indent, and match 2 is the file being linked
  @last_scanned_index = 0

  @changed_file = false
  @source_files_copied = []

  if !functions_to_add.nil?
    add_new_annotations_to_file_lines(functions_to_add)
  end

  @curr_migration_version, _ = NandoUtils.get_migration_version_and_name_from_file_path(migration_file_path)
  find_and_update()

  if @changed_file
    File.write(migration_file_path, @lines.join(''))
  end
end