nandi 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +477 -0
  3. data/Rakefile +8 -0
  4. data/exe/nandi-enforce +36 -0
  5. data/lib/generators/nandi/check_constraint/USAGE +19 -0
  6. data/lib/generators/nandi/check_constraint/check_constraint_generator.rb +52 -0
  7. data/lib/generators/nandi/check_constraint/templates/add_check_constraint.rb +15 -0
  8. data/lib/generators/nandi/check_constraint/templates/validate_check_constraint.rb +9 -0
  9. data/lib/generators/nandi/compile/USAGE +19 -0
  10. data/lib/generators/nandi/compile/compile_generator.rb +62 -0
  11. data/lib/generators/nandi/foreign_key/USAGE +47 -0
  12. data/lib/generators/nandi/foreign_key/foreign_key_generator.rb +91 -0
  13. data/lib/generators/nandi/foreign_key/templates/add_foreign_key.rb +13 -0
  14. data/lib/generators/nandi/foreign_key/templates/add_reference.rb +11 -0
  15. data/lib/generators/nandi/foreign_key/templates/validate_foreign_key.rb +9 -0
  16. data/lib/generators/nandi/migration/USAGE +9 -0
  17. data/lib/generators/nandi/migration/migration_generator.rb +24 -0
  18. data/lib/generators/nandi/migration/templates/migration.rb +13 -0
  19. data/lib/generators/nandi/not_null_check/USAGE +19 -0
  20. data/lib/generators/nandi/not_null_check/not_null_check_generator.rb +56 -0
  21. data/lib/generators/nandi/not_null_check/templates/add_not_null_check.rb +11 -0
  22. data/lib/generators/nandi/not_null_check/templates/validate_not_null_check.rb +9 -0
  23. data/lib/nandi.rb +35 -0
  24. data/lib/nandi/compiled_migration.rb +86 -0
  25. data/lib/nandi/config.rb +126 -0
  26. data/lib/nandi/file_diff.rb +32 -0
  27. data/lib/nandi/file_matcher.rb +72 -0
  28. data/lib/nandi/formatting.rb +79 -0
  29. data/lib/nandi/instructions.rb +19 -0
  30. data/lib/nandi/instructions/add_check_constraint.rb +23 -0
  31. data/lib/nandi/instructions/add_column.rb +24 -0
  32. data/lib/nandi/instructions/add_foreign_key.rb +40 -0
  33. data/lib/nandi/instructions/add_index.rb +50 -0
  34. data/lib/nandi/instructions/change_column_default.rb +23 -0
  35. data/lib/nandi/instructions/create_table.rb +83 -0
  36. data/lib/nandi/instructions/drop_constraint.rb +22 -0
  37. data/lib/nandi/instructions/drop_table.rb +21 -0
  38. data/lib/nandi/instructions/irreversible_migration.rb +15 -0
  39. data/lib/nandi/instructions/remove_column.rb +23 -0
  40. data/lib/nandi/instructions/remove_index.rb +41 -0
  41. data/lib/nandi/instructions/remove_not_null_constraint.rb +22 -0
  42. data/lib/nandi/instructions/validate_constraint.rb +22 -0
  43. data/lib/nandi/lockfile.rb +58 -0
  44. data/lib/nandi/migration.rb +363 -0
  45. data/lib/nandi/renderers.rb +7 -0
  46. data/lib/nandi/renderers/active_record.rb +13 -0
  47. data/lib/nandi/renderers/active_record/generate.rb +59 -0
  48. data/lib/nandi/renderers/active_record/instructions.rb +134 -0
  49. data/lib/nandi/safe_migration_enforcer.rb +143 -0
  50. data/lib/nandi/timeout_policies.rb +38 -0
  51. data/lib/nandi/timeout_policies/access_exclusive.rb +54 -0
  52. data/lib/nandi/timeout_policies/concurrent.rb +64 -0
  53. data/lib/nandi/validation.rb +10 -0
  54. data/lib/nandi/validation/add_column_validator.rb +43 -0
  55. data/lib/nandi/validation/each_validator.rb +32 -0
  56. data/lib/nandi/validation/failure_helpers.rb +35 -0
  57. data/lib/nandi/validation/remove_index_validator.rb +30 -0
  58. data/lib/nandi/validation/result.rb +30 -0
  59. data/lib/nandi/validation/timeout_validator.rb +37 -0
  60. data/lib/nandi/validator.rb +102 -0
  61. data/lib/nandi/version.rb +5 -0
  62. data/lib/templates/nandi/renderers/active_record/generate/show.rb.erb +27 -0
  63. data/lib/templates/nandi/renderers/active_record/instructions/add_check_constraint/show.rb.erb +7 -0
  64. data/lib/templates/nandi/renderers/active_record/instructions/add_column/show.rb.erb +6 -0
  65. data/lib/templates/nandi/renderers/active_record/instructions/add_foreign_key/show.rb.erb +5 -0
  66. data/lib/templates/nandi/renderers/active_record/instructions/add_index/show.rb.erb +5 -0
  67. data/lib/templates/nandi/renderers/active_record/instructions/change_column_default/show.rb.erb +1 -0
  68. data/lib/templates/nandi/renderers/active_record/instructions/create_table/show.rb.erb +8 -0
  69. data/lib/templates/nandi/renderers/active_record/instructions/drop_constraint/show.rb.erb +4 -0
  70. data/lib/templates/nandi/renderers/active_record/instructions/drop_table/show.rb.erb +1 -0
  71. data/lib/templates/nandi/renderers/active_record/instructions/irreversible_migration/show.rb.erb +1 -0
  72. data/lib/templates/nandi/renderers/active_record/instructions/remove_column/show.rb.erb +5 -0
  73. data/lib/templates/nandi/renderers/active_record/instructions/remove_index/show.rb.erb +4 -0
  74. data/lib/templates/nandi/renderers/active_record/instructions/remove_not_null_constraint/show.rb.erb +1 -0
  75. data/lib/templates/nandi/renderers/active_record/instructions/validate_constraint/show.rb.erb +3 -0
  76. metadata +320 -0
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi"
4
+ require "nandi/validation"
5
+ require "nandi/validation/failure_helpers"
6
+
7
+ module Nandi
8
+ module TimeoutPolicies
9
+ class Concurrent
10
+ include Nandi::Validation::FailureHelpers
11
+
12
+ def self.validate(migration)
13
+ new(migration).validate
14
+ end
15
+
16
+ def initialize(migration)
17
+ @migration = migration
18
+ end
19
+
20
+ def validate
21
+ collect_errors(
22
+ validate_statement_timeout,
23
+ validate_lock_timeout,
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :migration
30
+
31
+ def validate_statement_timeout
32
+ assert(
33
+ migration.disable_statement_timeout? || statement_timeout_high_enough,
34
+ "statement timeout for concurrent operations "\
35
+ "must be at least #{minimum_statement_timeout}",
36
+ )
37
+ end
38
+
39
+ def validate_lock_timeout
40
+ assert(
41
+ migration.disable_lock_timeout? || lock_timeout_high_enough,
42
+ "lock timeout for concurrent operations "\
43
+ "must be at least #{minimum_lock_timeout}",
44
+ )
45
+ end
46
+
47
+ def statement_timeout_high_enough
48
+ migration.statement_timeout >= minimum_statement_timeout
49
+ end
50
+
51
+ def lock_timeout_high_enough
52
+ migration.lock_timeout >= minimum_lock_timeout
53
+ end
54
+
55
+ def minimum_lock_timeout
56
+ Nandi.config.concurrent_lock_timeout_limit
57
+ end
58
+
59
+ def minimum_statement_timeout
60
+ Nandi.config.concurrent_statement_timeout_limit
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/validation/add_column_validator"
4
+ require "nandi/validation/remove_index_validator"
5
+ require "nandi/validation/each_validator"
6
+ require "nandi/validation/result"
7
+ require "nandi/validation/failure_helpers"
8
+ require "nandi/validation/timeout_validator"
9
+
10
+ module Validation; end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/validation/failure_helpers"
4
+
5
+ module Nandi
6
+ module Validation
7
+ class AddColumnValidator
8
+ include Nandi::Validation::FailureHelpers
9
+
10
+ def self.call(instruction)
11
+ new(instruction).call
12
+ end
13
+
14
+ def initialize(instruction)
15
+ @instruction = instruction
16
+ end
17
+
18
+ def call
19
+ collect_errors(
20
+ assert(nullable? || default_value?,
21
+ "add_column: non-null column lacks default"),
22
+ assert(!unique?, "add_column: column is unique"),
23
+ )
24
+ end
25
+
26
+ attr_reader :instruction
27
+
28
+ private
29
+
30
+ def default_value?
31
+ !instruction.extra_args[:default].nil?
32
+ end
33
+
34
+ def nullable?
35
+ instruction.extra_args.fetch(:null, true)
36
+ end
37
+
38
+ def unique?
39
+ instruction.extra_args.fetch(:unique, false)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/validation/failure_helpers"
4
+
5
+ module Nandi
6
+ module Validation
7
+ class EachValidator
8
+ include Nandi::Validation::FailureHelpers
9
+
10
+ def self.call(instruction)
11
+ new(instruction).call
12
+ end
13
+
14
+ def initialize(instruction)
15
+ @instruction = instruction
16
+ end
17
+
18
+ def call
19
+ case instruction.procedure
20
+ when :remove_index
21
+ RemoveIndexValidator.call(instruction)
22
+ when :add_column
23
+ AddColumnValidator.call(instruction)
24
+ else
25
+ success
26
+ end
27
+ end
28
+
29
+ attr_reader :instruction
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads/result"
4
+
5
+ module Nandi
6
+ module Validation
7
+ module FailureHelpers
8
+ def collect_errors(new, old)
9
+ return success if new.success? && old.success?
10
+
11
+ if old.failure?
12
+ failure(Array(old.failure) + Array(new.failure))
13
+ else
14
+ failure(Array(new.failure))
15
+ end
16
+ end
17
+
18
+ def success
19
+ Dry::Monads::Result::Success.new(nil)
20
+ end
21
+
22
+ def failure(value)
23
+ Dry::Monads::Result::Failure.new(value)
24
+ end
25
+
26
+ def assert(condition, message)
27
+ if condition
28
+ success
29
+ else
30
+ failure(message)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/validation/failure_helpers"
4
+
5
+ module Nandi
6
+ module Validation
7
+ class RemoveIndexValidator
8
+ include Nandi::Validation::FailureHelpers
9
+
10
+ def self.call(instruction)
11
+ new(instruction).call
12
+ end
13
+
14
+ def initialize(instruction)
15
+ @instruction = instruction
16
+ end
17
+
18
+ def call
19
+ opts = instruction.extra_args
20
+
21
+ assert(
22
+ opts.key?(:name) || opts.key?(:column),
23
+ "remove_index: requires a `name` or `column` argument",
24
+ )
25
+ end
26
+
27
+ attr_reader :instruction
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/validation/failure_helpers"
4
+ module Nandi
5
+ module Validation
6
+ class Result
7
+ include Nandi::Validation::FailureHelpers
8
+
9
+ attr_reader :errors
10
+
11
+ def initialize(instruction = nil)
12
+ @instruction = instruction
13
+ @errors = success
14
+ end
15
+
16
+ def valid?
17
+ @errors.success?
18
+ end
19
+
20
+ def <<(error)
21
+ @errors = collect_errors(error, @errors)
22
+ self
23
+ end
24
+
25
+ def error_list
26
+ @errors.failure.join("\n")
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/validation/failure_helpers"
4
+
5
+ module Nandi
6
+ module Validation
7
+ class TimeoutValidator
8
+ include Nandi::Validation::FailureHelpers
9
+
10
+ def self.call(migration)
11
+ new(migration).call
12
+ end
13
+
14
+ def initialize(migration)
15
+ @migration = migration
16
+ end
17
+
18
+ def call
19
+ timeout_policies.inject(success) do |result, policy|
20
+ collect_errors(policy.validate(migration), result)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def timeout_policies
27
+ instructions.map(&Nandi::TimeoutPolicies.method(:policy_for)).uniq
28
+ end
29
+
30
+ def instructions
31
+ [*migration.up_instructions, *migration.down_instructions]
32
+ end
33
+
34
+ attr_reader :migration
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require "nandi/validation"
5
+ require "nandi/timeout_policies"
6
+
7
+ module Nandi
8
+ class Validator
9
+ include Nandi::Validation::FailureHelpers
10
+
11
+ class InstructionValidator
12
+ def self.call(instruction)
13
+ new(instruction).call
14
+ end
15
+
16
+ def initialize(instruction)
17
+ @instruction = instruction
18
+ end
19
+
20
+ def call
21
+ raise NotImplementedError
22
+ end
23
+
24
+ attr_reader :instruction
25
+ end
26
+
27
+ def self.call(migration)
28
+ new(migration).call
29
+ end
30
+
31
+ def initialize(migration)
32
+ @migration = migration
33
+ end
34
+
35
+ def call
36
+ migration_invariants_respected << each_instruction_validation
37
+ end
38
+
39
+ private
40
+
41
+ def migration_invariants_respected
42
+ Validation::Result.new.tap do |result|
43
+ result << assert(
44
+ at_most_one_object_modified,
45
+ "modifying more than one table per migration",
46
+ )
47
+
48
+ result << assert(
49
+ new_indexes_are_separated_from_other_migrations,
50
+ "creating more than one index per migration",
51
+ )
52
+
53
+ result << validate_timeouts
54
+ end
55
+ end
56
+
57
+ def at_most_one_object_modified
58
+ [migration.up_instructions, migration.down_instructions].all? do |instructions|
59
+ affected_tables = instructions.map do |instruction|
60
+ instruction.respond_to?(:table) && instruction.table.to_sym
61
+ end
62
+
63
+ affected_tables.uniq.count <= 1
64
+ end
65
+ end
66
+
67
+ def new_indexes_are_separated_from_other_migrations
68
+ [migration.up_instructions, migration.down_instructions].map do |instructions|
69
+ instructions.none? { |i| i.procedure == :add_index } ||
70
+ instructions.count == 1
71
+ end.all?
72
+ end
73
+
74
+ def statement_timeout_is_within_acceptable_bounds
75
+ migration.strictest_lock != Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE ||
76
+ migration.statement_timeout <=
77
+ Nandi.config.access_exclusive_statement_timeout_limit
78
+ end
79
+
80
+ def lock_timeout_is_within_acceptable_bounds
81
+ migration.strictest_lock != Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE ||
82
+ migration.lock_timeout <=
83
+ Nandi.config.access_exclusive_lock_timeout_limit
84
+ end
85
+
86
+ def each_instruction_validation
87
+ instructions.inject(success) do |result, instruction|
88
+ collect_errors(Validation::EachValidator.call(instruction), result)
89
+ end
90
+ end
91
+
92
+ def validate_timeouts
93
+ Nandi::Validation::TimeoutValidator.call(migration)
94
+ end
95
+
96
+ def instructions
97
+ [*migration.up_instructions, *migration.down_instructions]
98
+ end
99
+
100
+ attr_reader :migration
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nandi
4
+ VERSION = "0.9.0"
5
+ end
@@ -0,0 +1,27 @@
1
+ class <%= name %> < ActiveRecord::Migration[<%= activerecord_version %>]
2
+ <%= mixins.map { |m| "include #{m.name}"}.join("\n") %>
3
+ <% if disable_lock_timeout? %>
4
+ disable_lock_timeout!
5
+ <% else %>
6
+ set_lock_timeout(<%= lock_timeout %>)
7
+ <% end %>
8
+ <% if disable_statement_timeout? %>
9
+ disable_statement_timeout!
10
+ <% else %>
11
+ set_statement_timeout(<%= statement_timeout %>)
12
+ <% end %>
13
+
14
+ <% if should_disable_ddl_transaction? %>disable_ddl_transaction!<% end %>
15
+ def up
16
+ <% up_instructions.each do |i| %>
17
+ <%= render_partial(i) %>
18
+ <% end %>
19
+ end
20
+ <% if down_instructions&.any? %>
21
+ def down
22
+ <% down_instructions.each do |i| %>
23
+ <%= render_partial(i) %>
24
+ <% end %>
25
+ end
26
+ <% end %>
27
+ end
@@ -0,0 +1,7 @@
1
+ execute <<-SQL
2
+ ALTER TABLE <%= table %>
3
+ ADD CONSTRAINT <%= name %>
4
+ CHECK (<%= check %>)
5
+ NOT VALID
6
+ SQL
7
+
@@ -0,0 +1,6 @@
1
+ add_column(
2
+ <%= table %>,
3
+ <%= name %>,
4
+ <%= type %>,
5
+ <%= extra_args %>
6
+ )