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.
- 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
|