nandi 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +477 -0
- data/Rakefile +8 -0
- data/exe/nandi-enforce +36 -0
- data/lib/generators/nandi/check_constraint/USAGE +19 -0
- data/lib/generators/nandi/check_constraint/check_constraint_generator.rb +52 -0
- data/lib/generators/nandi/check_constraint/templates/add_check_constraint.rb +15 -0
- data/lib/generators/nandi/check_constraint/templates/validate_check_constraint.rb +9 -0
- data/lib/generators/nandi/compile/USAGE +19 -0
- data/lib/generators/nandi/compile/compile_generator.rb +62 -0
- data/lib/generators/nandi/foreign_key/USAGE +47 -0
- data/lib/generators/nandi/foreign_key/foreign_key_generator.rb +91 -0
- data/lib/generators/nandi/foreign_key/templates/add_foreign_key.rb +13 -0
- data/lib/generators/nandi/foreign_key/templates/add_reference.rb +11 -0
- data/lib/generators/nandi/foreign_key/templates/validate_foreign_key.rb +9 -0
- data/lib/generators/nandi/migration/USAGE +9 -0
- data/lib/generators/nandi/migration/migration_generator.rb +24 -0
- data/lib/generators/nandi/migration/templates/migration.rb +13 -0
- data/lib/generators/nandi/not_null_check/USAGE +19 -0
- data/lib/generators/nandi/not_null_check/not_null_check_generator.rb +56 -0
- data/lib/generators/nandi/not_null_check/templates/add_not_null_check.rb +11 -0
- data/lib/generators/nandi/not_null_check/templates/validate_not_null_check.rb +9 -0
- data/lib/nandi.rb +35 -0
- data/lib/nandi/compiled_migration.rb +86 -0
- data/lib/nandi/config.rb +126 -0
- data/lib/nandi/file_diff.rb +32 -0
- data/lib/nandi/file_matcher.rb +72 -0
- data/lib/nandi/formatting.rb +79 -0
- data/lib/nandi/instructions.rb +19 -0
- data/lib/nandi/instructions/add_check_constraint.rb +23 -0
- data/lib/nandi/instructions/add_column.rb +24 -0
- data/lib/nandi/instructions/add_foreign_key.rb +40 -0
- data/lib/nandi/instructions/add_index.rb +50 -0
- data/lib/nandi/instructions/change_column_default.rb +23 -0
- data/lib/nandi/instructions/create_table.rb +83 -0
- data/lib/nandi/instructions/drop_constraint.rb +22 -0
- data/lib/nandi/instructions/drop_table.rb +21 -0
- data/lib/nandi/instructions/irreversible_migration.rb +15 -0
- data/lib/nandi/instructions/remove_column.rb +23 -0
- data/lib/nandi/instructions/remove_index.rb +41 -0
- data/lib/nandi/instructions/remove_not_null_constraint.rb +22 -0
- data/lib/nandi/instructions/validate_constraint.rb +22 -0
- data/lib/nandi/lockfile.rb +58 -0
- data/lib/nandi/migration.rb +363 -0
- data/lib/nandi/renderers.rb +7 -0
- data/lib/nandi/renderers/active_record.rb +13 -0
- data/lib/nandi/renderers/active_record/generate.rb +59 -0
- data/lib/nandi/renderers/active_record/instructions.rb +134 -0
- data/lib/nandi/safe_migration_enforcer.rb +143 -0
- data/lib/nandi/timeout_policies.rb +38 -0
- data/lib/nandi/timeout_policies/access_exclusive.rb +54 -0
- data/lib/nandi/timeout_policies/concurrent.rb +64 -0
- data/lib/nandi/validation.rb +10 -0
- data/lib/nandi/validation/add_column_validator.rb +43 -0
- data/lib/nandi/validation/each_validator.rb +32 -0
- data/lib/nandi/validation/failure_helpers.rb +35 -0
- data/lib/nandi/validation/remove_index_validator.rb +30 -0
- data/lib/nandi/validation/result.rb +30 -0
- data/lib/nandi/validation/timeout_validator.rb +37 -0
- data/lib/nandi/validator.rb +102 -0
- data/lib/nandi/version.rb +5 -0
- data/lib/templates/nandi/renderers/active_record/generate/show.rb.erb +27 -0
- data/lib/templates/nandi/renderers/active_record/instructions/add_check_constraint/show.rb.erb +7 -0
- data/lib/templates/nandi/renderers/active_record/instructions/add_column/show.rb.erb +6 -0
- data/lib/templates/nandi/renderers/active_record/instructions/add_foreign_key/show.rb.erb +5 -0
- data/lib/templates/nandi/renderers/active_record/instructions/add_index/show.rb.erb +5 -0
- data/lib/templates/nandi/renderers/active_record/instructions/change_column_default/show.rb.erb +1 -0
- data/lib/templates/nandi/renderers/active_record/instructions/create_table/show.rb.erb +8 -0
- data/lib/templates/nandi/renderers/active_record/instructions/drop_constraint/show.rb.erb +4 -0
- data/lib/templates/nandi/renderers/active_record/instructions/drop_table/show.rb.erb +1 -0
- data/lib/templates/nandi/renderers/active_record/instructions/irreversible_migration/show.rb.erb +1 -0
- data/lib/templates/nandi/renderers/active_record/instructions/remove_column/show.rb.erb +5 -0
- data/lib/templates/nandi/renderers/active_record/instructions/remove_index/show.rb.erb +4 -0
- data/lib/templates/nandi/renderers/active_record/instructions/remove_not_null_constraint/show.rb.erb +1 -0
- data/lib/templates/nandi/renderers/active_record/instructions/validate_constraint/show.rb.erb +3 -0
- 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,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
|