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,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "cell"
|
5
|
+
require "tilt"
|
6
|
+
require "nandi/renderers/active_record/instructions"
|
7
|
+
|
8
|
+
module Nandi
|
9
|
+
module Renderers
|
10
|
+
module ActiveRecord
|
11
|
+
class Generate < ::Cell::ViewModel
|
12
|
+
def self.call(*args)
|
13
|
+
super.call
|
14
|
+
end
|
15
|
+
|
16
|
+
def partials_base
|
17
|
+
"nandi/renderers/active_record/instructions"
|
18
|
+
end
|
19
|
+
|
20
|
+
def template_options_for(_options)
|
21
|
+
{
|
22
|
+
suffix: "rb.erb",
|
23
|
+
template_class: Tilt,
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
self.view_paths = [
|
28
|
+
File.expand_path("../../../templates", __dir__),
|
29
|
+
]
|
30
|
+
|
31
|
+
def should_disable_ddl_transaction?
|
32
|
+
[*up_instructions, *down_instructions].
|
33
|
+
select { |i| i.procedure =~ /index/ }.any?
|
34
|
+
end
|
35
|
+
|
36
|
+
def activerecord_version
|
37
|
+
::ActiveRecord::Migration.current_version
|
38
|
+
end
|
39
|
+
|
40
|
+
def render_partial(instruction)
|
41
|
+
if instruction.respond_to?(:template)
|
42
|
+
cell(instruction.template, instruction)
|
43
|
+
else
|
44
|
+
cell("#{partials_base}/#{instruction.procedure}", instruction)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
property :up_instructions
|
49
|
+
property :down_instructions
|
50
|
+
property :name
|
51
|
+
property :mixins
|
52
|
+
property :disable_lock_timeout?
|
53
|
+
property :disable_statement_timeout?
|
54
|
+
property :lock_timeout
|
55
|
+
property :statement_timeout
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cells"
|
4
|
+
require "tilt"
|
5
|
+
require "nandi/formatting"
|
6
|
+
require "ostruct"
|
7
|
+
|
8
|
+
module Nandi
|
9
|
+
module Renderers
|
10
|
+
module ActiveRecord
|
11
|
+
module Instructions
|
12
|
+
class Base < ::Cell::ViewModel
|
13
|
+
include Nandi::Formatting
|
14
|
+
|
15
|
+
def template_options_for(_options)
|
16
|
+
{
|
17
|
+
suffix: "rb.erb",
|
18
|
+
template_class: Tilt,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
self.view_paths = [
|
23
|
+
File.join(__dir__, "../../../templates"),
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
class RemoveIndexCell < Base
|
28
|
+
formatted_property :table
|
29
|
+
formatted_property :extra_args
|
30
|
+
end
|
31
|
+
|
32
|
+
class AddIndexCell < Base
|
33
|
+
formatted_property :table
|
34
|
+
formatted_property :fields
|
35
|
+
formatted_property :extra_args
|
36
|
+
end
|
37
|
+
|
38
|
+
class CreateTableCell < Base
|
39
|
+
formatted_property :table
|
40
|
+
formatted_property :timestamps_args
|
41
|
+
|
42
|
+
def timestamps?
|
43
|
+
!model.timestamps_args.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
def timestamps_args?
|
47
|
+
!model.timestamps_args&.empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
def extra_args?
|
51
|
+
model.extra_args&.any?
|
52
|
+
end
|
53
|
+
|
54
|
+
def timestamps_args
|
55
|
+
format_value(model.timestamps_args, as_argument: true)
|
56
|
+
end
|
57
|
+
|
58
|
+
def extra_args
|
59
|
+
format_value(model.extra_args, as_argument: true)
|
60
|
+
end
|
61
|
+
|
62
|
+
def columns
|
63
|
+
model.columns.map do |c|
|
64
|
+
OpenStruct.new(
|
65
|
+
name: format_value(c.name),
|
66
|
+
type: format_value(c.type),
|
67
|
+
).tap do |col|
|
68
|
+
col.args = format_value(c.args, as_argument: true) unless c.args.empty?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class DropTableCell < Base
|
75
|
+
formatted_property :table
|
76
|
+
end
|
77
|
+
|
78
|
+
class AddColumnCell < Base
|
79
|
+
formatted_property :table
|
80
|
+
formatted_property :name
|
81
|
+
formatted_property :type
|
82
|
+
formatted_property :extra_args
|
83
|
+
end
|
84
|
+
|
85
|
+
class RemoveColumnCell < Base
|
86
|
+
formatted_property :table
|
87
|
+
formatted_property :name
|
88
|
+
formatted_property :extra_args
|
89
|
+
end
|
90
|
+
|
91
|
+
class ChangeColumnDefaultCell < Base
|
92
|
+
formatted_property :table
|
93
|
+
formatted_property :column
|
94
|
+
formatted_property :value
|
95
|
+
end
|
96
|
+
|
97
|
+
class RemoveNotNullConstraintCell < Base
|
98
|
+
formatted_property :table
|
99
|
+
formatted_property :column
|
100
|
+
end
|
101
|
+
|
102
|
+
class AddForeignKeyCell < Base
|
103
|
+
formatted_property :table
|
104
|
+
formatted_property :target
|
105
|
+
formatted_property :extra_args
|
106
|
+
end
|
107
|
+
|
108
|
+
class AddCheckConstraintCell < Base
|
109
|
+
# Because all this stuff goes into a SQL string, we don't need to format
|
110
|
+
# the values.
|
111
|
+
property :table
|
112
|
+
property :name
|
113
|
+
property :check
|
114
|
+
end
|
115
|
+
|
116
|
+
class ValidateConstraintCell < Base
|
117
|
+
# Because all this stuff goes into a SQL string, we don't need to format
|
118
|
+
# the values.
|
119
|
+
property :table
|
120
|
+
property :name
|
121
|
+
end
|
122
|
+
|
123
|
+
class DropConstraintCell < Base
|
124
|
+
# Because all this stuff goes into a SQL string, we don't need to format
|
125
|
+
# the values.
|
126
|
+
property :table
|
127
|
+
property :name
|
128
|
+
end
|
129
|
+
|
130
|
+
class IrreversibleMigrationCell < Base; end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
require "rails"
|
5
|
+
require "rails/generators"
|
6
|
+
|
7
|
+
require "nandi/file_diff"
|
8
|
+
require "nandi/file_matcher"
|
9
|
+
require "nandi/lockfile"
|
10
|
+
|
11
|
+
module Nandi
|
12
|
+
class SafeMigrationEnforcer
|
13
|
+
class MigrationLintingFailed < StandardError; end
|
14
|
+
|
15
|
+
DEFAULT_SAFE_MIGRATION_DIR = "db/safe_migrations"
|
16
|
+
DEFAULT_AR_MIGRATION_DIR = "db/migrate"
|
17
|
+
DEFAULT_FILE_SPEC = "all"
|
18
|
+
|
19
|
+
def initialize(require_path: nil,
|
20
|
+
safe_migration_dir: DEFAULT_SAFE_MIGRATION_DIR,
|
21
|
+
ar_migration_dir: DEFAULT_AR_MIGRATION_DIR,
|
22
|
+
files: DEFAULT_FILE_SPEC)
|
23
|
+
@safe_migration_dir = safe_migration_dir
|
24
|
+
@ar_migration_dir = ar_migration_dir
|
25
|
+
@files = files
|
26
|
+
|
27
|
+
require require_path unless require_path.nil?
|
28
|
+
|
29
|
+
Nandi.configure do |c|
|
30
|
+
c.migration_directory = @safe_migration_dir
|
31
|
+
c.output_directory = @ar_migration_dir
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def run
|
36
|
+
safe_migrations = matching_migrations(@safe_migration_dir)
|
37
|
+
ar_migrations = matching_migrations(@ar_migration_dir)
|
38
|
+
|
39
|
+
return true if safe_migrations.none? && ar_migrations.none?
|
40
|
+
|
41
|
+
enforce_no_ungenerated_migrations!(safe_migrations, ar_migrations)
|
42
|
+
enforce_no_hand_written_migrations!(safe_migrations, ar_migrations)
|
43
|
+
enforce_no_hand_edited_migrations!(ar_migrations)
|
44
|
+
enforce_no_out_of_date_migrations!(safe_migrations)
|
45
|
+
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def matching_migrations(dir)
|
52
|
+
names = Dir.glob(File.join(dir, "*.rb")).map { |path| File.basename(path) }
|
53
|
+
FileMatcher.call(files: names, spec: @files)
|
54
|
+
end
|
55
|
+
|
56
|
+
def enforce_no_ungenerated_migrations!(safe_migrations, ar_migrations)
|
57
|
+
ungenerated_migrations = safe_migrations - ar_migrations
|
58
|
+
if ungenerated_migrations.any?
|
59
|
+
error = <<~ERROR
|
60
|
+
The following migrations are pending generation:
|
61
|
+
|
62
|
+
- #{ungenerated_migrations.sort.join("\n - ")}
|
63
|
+
|
64
|
+
Please run `rails generate nandi:compile` to generate your migrations.
|
65
|
+
ERROR
|
66
|
+
|
67
|
+
raise MigrationLintingFailed, error
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def enforce_no_hand_written_migrations!(safe_migrations, ar_migrations)
|
72
|
+
handwritten_migrations = ar_migrations - safe_migrations
|
73
|
+
handwritten_migration_paths = names_to_paths(handwritten_migrations)
|
74
|
+
|
75
|
+
if handwritten_migration_paths.any?
|
76
|
+
error = <<~ERROR
|
77
|
+
The following migrations have been written by hand, not generated:
|
78
|
+
|
79
|
+
- #{handwritten_migration_paths.sort.join("\n - ")}
|
80
|
+
|
81
|
+
Please use Nandi to generate your migrations. In exeptional cases, hand-written
|
82
|
+
ActiveRecord migrations can be added to the .nandiignore file. Doing so will
|
83
|
+
require additional review that will slow your PR down.
|
84
|
+
ERROR
|
85
|
+
|
86
|
+
raise MigrationLintingFailed, error
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def enforce_no_out_of_date_migrations!(safe_migrations)
|
91
|
+
out_of_date_migrations = safe_migrations.
|
92
|
+
map { |m| [m, Nandi::Lockfile.get(m)] }.
|
93
|
+
select do |filename, digests|
|
94
|
+
Nandi::FileDiff.new(
|
95
|
+
file_path: File.join(@safe_migration_dir, filename),
|
96
|
+
known_digest: digests[:source_digest],
|
97
|
+
).changed?
|
98
|
+
end
|
99
|
+
|
100
|
+
if out_of_date_migrations.any?
|
101
|
+
error = <<~ERROR
|
102
|
+
The following migrations have changed but not been recompiled:
|
103
|
+
|
104
|
+
- #{out_of_date_migrations.sort.join("\n - ")}
|
105
|
+
|
106
|
+
Please recompile your migrations to make sure that the changes you expect are
|
107
|
+
applied.
|
108
|
+
ERROR
|
109
|
+
|
110
|
+
raise MigrationLintingFailed, error
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def enforce_no_hand_edited_migrations!(ar_migrations)
|
115
|
+
hand_altered_migrations = ar_migrations.
|
116
|
+
map { |m| [m, Nandi::Lockfile.get(m)] }.
|
117
|
+
select do |filename, digests|
|
118
|
+
Nandi::FileDiff.new(
|
119
|
+
file_path: File.join(@ar_migration_dir, filename),
|
120
|
+
known_digest: digests[:compiled_digest],
|
121
|
+
).changed?
|
122
|
+
end
|
123
|
+
|
124
|
+
if hand_altered_migrations.any?
|
125
|
+
error = <<~ERROR
|
126
|
+
The following migrations have had their generated content altered:
|
127
|
+
|
128
|
+
- #{hand_altered_migrations.sort.join("\n - ")}
|
129
|
+
|
130
|
+
Please don't hand-edit generated migrations. If you want to write a regular
|
131
|
+
ActiveRecord::Migration, please do so and add it to .nandiignore. Note that
|
132
|
+
this will require additional review that will slow your PR down.
|
133
|
+
ERROR
|
134
|
+
|
135
|
+
raise MigrationLintingFailed, error
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def names_to_paths(names)
|
140
|
+
names.map { |name| File.join(@ar_migration_dir, name) }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nandi/validation/failure_helpers"
|
4
|
+
require "nandi/migration"
|
5
|
+
require "nandi/timeout_policies/access_exclusive"
|
6
|
+
require "nandi/timeout_policies/concurrent"
|
7
|
+
|
8
|
+
module Nandi
|
9
|
+
module TimeoutPolicies
|
10
|
+
CONCURRENT_OPERATIONS = %i[add_index remove_index].freeze
|
11
|
+
class Noop
|
12
|
+
class << self
|
13
|
+
include Nandi::Validation::FailureHelpers
|
14
|
+
|
15
|
+
def validate(_)
|
16
|
+
success
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.policy_for(instruction)
|
22
|
+
case instruction.lock
|
23
|
+
when Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE
|
24
|
+
AccessExclusive
|
25
|
+
else
|
26
|
+
share_policy_for(instruction)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.share_policy_for(instruction)
|
31
|
+
if CONCURRENT_OPERATIONS.include?(instruction.procedure)
|
32
|
+
Concurrent
|
33
|
+
else
|
34
|
+
Noop
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nandi"
|
4
|
+
require "nandi/validation/failure_helpers"
|
5
|
+
|
6
|
+
module Nandi
|
7
|
+
module TimeoutPolicies
|
8
|
+
class AccessExclusive
|
9
|
+
include Nandi::Validation::FailureHelpers
|
10
|
+
|
11
|
+
def self.validate(migration)
|
12
|
+
new(migration).validate
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(migration)
|
16
|
+
@migration = migration
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate
|
20
|
+
collect_errors(validate_statement_timeout, validate_lock_timeout)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :migration
|
26
|
+
|
27
|
+
def validate_statement_timeout
|
28
|
+
assert(
|
29
|
+
!migration.disable_statement_timeout? &&
|
30
|
+
migration.statement_timeout <= statement_timeout_maximum,
|
31
|
+
"statement timeout must be at most #{statement_timeout_maximum}ms" \
|
32
|
+
" as it takes an ACCESS EXCLUSIVE lock",
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_lock_timeout
|
37
|
+
assert(
|
38
|
+
!migration.disable_lock_timeout? &&
|
39
|
+
migration.lock_timeout <= lock_timeout_maximum,
|
40
|
+
"lock timeout must be at most #{lock_timeout_maximum}ms" \
|
41
|
+
" as it takes an ACCESS EXCLUSIVE lock",
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def statement_timeout_maximum
|
46
|
+
Nandi.config.access_exclusive_statement_timeout_limit
|
47
|
+
end
|
48
|
+
|
49
|
+
def lock_timeout_maximum
|
50
|
+
Nandi.config.access_exclusive_lock_timeout_limit
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|