nandi 0.9.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 (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
+ )