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,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nandi
|
4
|
+
module Instructions
|
5
|
+
class RemoveNotNullConstraint
|
6
|
+
attr_reader :table, :column
|
7
|
+
|
8
|
+
def initialize(table:, column:)
|
9
|
+
@table = table
|
10
|
+
@column = column
|
11
|
+
end
|
12
|
+
|
13
|
+
def procedure
|
14
|
+
:remove_not_null_constraint
|
15
|
+
end
|
16
|
+
|
17
|
+
def lock
|
18
|
+
Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nandi
|
4
|
+
module Instructions
|
5
|
+
class ValidateConstraint
|
6
|
+
attr_reader :table, :name
|
7
|
+
|
8
|
+
def initialize(table:, name:)
|
9
|
+
@table = table
|
10
|
+
@name = name
|
11
|
+
end
|
12
|
+
|
13
|
+
def lock
|
14
|
+
Nandi::Migration::LockWeights::SHARE
|
15
|
+
end
|
16
|
+
|
17
|
+
def procedure
|
18
|
+
:validate_constraint
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
4
|
+
|
5
|
+
module Nandi
|
6
|
+
class Lockfile
|
7
|
+
class << self
|
8
|
+
def file_present?
|
9
|
+
File.exist?(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def create!
|
13
|
+
return if file_present?
|
14
|
+
|
15
|
+
File.write(path, {}.to_yaml)
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(file_name:, source_digest:, compiled_digest:)
|
19
|
+
load!
|
20
|
+
|
21
|
+
lockfile[file_name] = {
|
22
|
+
source_digest: source_digest,
|
23
|
+
compiled_digest: compiled_digest,
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def get(file_name)
|
28
|
+
load!
|
29
|
+
|
30
|
+
{
|
31
|
+
source_digest: lockfile.dig(file_name, :source_digest),
|
32
|
+
compiled_digest: lockfile.dig(file_name, :compiled_digest),
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def load!
|
37
|
+
return lockfile if lockfile
|
38
|
+
|
39
|
+
Nandi::Lockfile.create! unless Nandi::Lockfile.file_present?
|
40
|
+
|
41
|
+
@lockfile = YAML.safe_load(File.read(path)).with_indifferent_access
|
42
|
+
end
|
43
|
+
|
44
|
+
def persist!
|
45
|
+
File.write(path, lockfile.to_h.deep_stringify_keys.to_yaml)
|
46
|
+
end
|
47
|
+
|
48
|
+
def path
|
49
|
+
File.join(
|
50
|
+
Nandi.config.lockfile_directory,
|
51
|
+
".nandilock.yml",
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_accessor :lockfile
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,363 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nandi/instructions"
|
4
|
+
require "nandi/validator"
|
5
|
+
require "nandi/validation/failure_helpers"
|
6
|
+
|
7
|
+
module Nandi
|
8
|
+
# @abstract A migration must implement #up (the forward migration), and may
|
9
|
+
# also implement #down (the rollback sequence).
|
10
|
+
# The base class for migrations; Nandi's equivalent of ActiveRecord::Migration.
|
11
|
+
# All the statements in the migration are statically analysed together to rule
|
12
|
+
# out migrations with a high risk of causing availability issues. Additionally,
|
13
|
+
# our implementations of some statements will rule out certain common footguns
|
14
|
+
# (for example, creating an index without using the `CONCURRENTLY` parameter.)
|
15
|
+
# @example
|
16
|
+
# class CreateWidgetsTable < Nandi::Migration
|
17
|
+
# def up
|
18
|
+
# create_table :widgets do |t|
|
19
|
+
# t.column :weight, :number
|
20
|
+
# t.column :name, :text, default: "Unknown widget"
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def down
|
25
|
+
# drop_table :widgets
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
class Migration
|
29
|
+
include Nandi::Validation::FailureHelpers
|
30
|
+
|
31
|
+
module LockWeights
|
32
|
+
ACCESS_EXCLUSIVE = 1
|
33
|
+
SHARE = 0
|
34
|
+
end
|
35
|
+
|
36
|
+
class InstructionSet < SimpleDelegator
|
37
|
+
def strictest_lock
|
38
|
+
return LockWeights::SHARE if empty?
|
39
|
+
|
40
|
+
map { |i| i.respond_to?(:lock) ? i.lock : LockWeights::ACCESS_EXCLUSIVE }.max
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
attr_reader :lock_timeout, :statement_timeout
|
46
|
+
# For sake both of correspondence with Postgres syntax and familiarity
|
47
|
+
# with activerecord-safe_migrations's identically named macros, we
|
48
|
+
# disable this cop.
|
49
|
+
|
50
|
+
# rubocop:disable Naming/AccessorMethodName
|
51
|
+
|
52
|
+
# Override the default lock timeout for the duration of the migration.
|
53
|
+
# This may be helpful when making changes to very busy tables, when a
|
54
|
+
# lock is less likely to be immediately available.
|
55
|
+
# @param timeout [Integer] New lock timeout in ms
|
56
|
+
def set_lock_timeout(timeout)
|
57
|
+
@lock_timeout = timeout
|
58
|
+
end
|
59
|
+
|
60
|
+
# Override the default statement timeout for the duration of the migration.
|
61
|
+
# This may be helpful when making changes that are likely to take a lot
|
62
|
+
# of time, like adding a new index on a large table.
|
63
|
+
# @param timeout [Integer] New lock timeout in ms
|
64
|
+
def set_statement_timeout(timeout)
|
65
|
+
@statement_timeout = timeout
|
66
|
+
end
|
67
|
+
# rubocop:enable Naming/AccessorMethodName
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param validator [Nandi::Validator]
|
71
|
+
def initialize(validator)
|
72
|
+
@validator = validator
|
73
|
+
@instructions = Hash.new { |h, k| h[k] = InstructionSet.new([]) }
|
74
|
+
validate
|
75
|
+
end
|
76
|
+
|
77
|
+
# @api private
|
78
|
+
def up_instructions
|
79
|
+
compile_instructions(:up)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
def down_instructions
|
84
|
+
compile_instructions(:down)
|
85
|
+
end
|
86
|
+
|
87
|
+
# The current lock timeout.
|
88
|
+
def lock_timeout
|
89
|
+
self.class.lock_timeout || default_lock_timeout
|
90
|
+
end
|
91
|
+
|
92
|
+
# The current statement timeout.
|
93
|
+
def statement_timeout
|
94
|
+
self.class.statement_timeout || default_statement_timeout
|
95
|
+
end
|
96
|
+
|
97
|
+
# @api private
|
98
|
+
def strictest_lock
|
99
|
+
@instructions.values.map(&:strictest_lock).max
|
100
|
+
end
|
101
|
+
|
102
|
+
# @abstract
|
103
|
+
def up
|
104
|
+
raise NotImplementedError
|
105
|
+
end
|
106
|
+
|
107
|
+
def down; end
|
108
|
+
|
109
|
+
# Adds a new index to the database.
|
110
|
+
#
|
111
|
+
# Nandi will:
|
112
|
+
# * add the `CONCURRENTLY` option, which means the change takes a less
|
113
|
+
# restrictive lock at the cost of not running in a DDL transaction
|
114
|
+
# * use the `BTREE` index type which is the safest to create.
|
115
|
+
#
|
116
|
+
# Because index creation is particularly failure-prone, and because
|
117
|
+
# we cannot run in a transaction and therefore risk partially applied
|
118
|
+
# migrations that (in a Rails environment) require manual intervention,
|
119
|
+
# Nandi Validates that, if there is a add_index statement in the
|
120
|
+
# migration, it must be the only statement.
|
121
|
+
# @param table [Symbol, String] The name of the table to add the index to
|
122
|
+
# @param fields [Symbol, String, Array] The field or fields to use in the
|
123
|
+
# index
|
124
|
+
# @param kwargs [Hash] Arbitrary options to pass to the backend adapter.
|
125
|
+
# Attempts to remove `CONCURRENTLY` or change the index type will be ignored.
|
126
|
+
def add_index(table, fields, **kwargs)
|
127
|
+
current_instructions << Instructions::AddIndex.new(
|
128
|
+
**kwargs,
|
129
|
+
table: table,
|
130
|
+
fields: fields,
|
131
|
+
)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Drop an index from the database.
|
135
|
+
#
|
136
|
+
# Nandi will add the `CONCURRENTLY` option, which means the change
|
137
|
+
# takes a less restrictive lock at the cost of not running in a DDL
|
138
|
+
# transaction.
|
139
|
+
#
|
140
|
+
# Because we cannot run in a transaction and therefore risk partially
|
141
|
+
# applied migrations that (in a Rails environment) require manual
|
142
|
+
# intervention, Nandi Validates that, if there is a remove_index statement
|
143
|
+
# in the migration, it must be the only statement.
|
144
|
+
# @param table [Symbol, String] The name of the table to add the index to
|
145
|
+
# @param target [Symbol, String, Array, Hash] This can be either the field (or
|
146
|
+
# array of fields) in the index to be dropped, or a hash of options, which
|
147
|
+
# must include either a `column` key (which is the same: a field or list
|
148
|
+
# of fields) or a `name` key, which is the name of the index to be dropped.
|
149
|
+
def remove_index(table, target)
|
150
|
+
current_instructions << Instructions::RemoveIndex.new(table: table, field: target)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Creates a new table. Yields a ColumnsReader object as a block, to allow adding
|
154
|
+
# columns.
|
155
|
+
# @example
|
156
|
+
# create_table :widgets do |t|
|
157
|
+
# t.text :foo, default: true
|
158
|
+
# end
|
159
|
+
# @param table [String, Symbol] The name of the new table
|
160
|
+
# @yieldparam columns_reader [Nandi::Instructions::CreateTable::ColumnsReader]
|
161
|
+
def create_table(table, **kwargs, &block)
|
162
|
+
current_instructions << Instructions::CreateTable.new(
|
163
|
+
**kwargs,
|
164
|
+
table: table,
|
165
|
+
columns_block: block,
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Drops an existing table
|
170
|
+
# @param table [String, Symbol] The name of the table to drop.
|
171
|
+
def drop_table(table)
|
172
|
+
current_instructions << Instructions::DropTable.new(table: table)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Adds a new column. Nandi will explicitly set the column to be NULL,
|
176
|
+
# as validating a new NOT NULL constraint can be very expensive on large
|
177
|
+
# tables and cause availability issues.
|
178
|
+
# @param table [Symbol, String] The name of the table to add the column to
|
179
|
+
# @param name [Symbol, String] The name of the column
|
180
|
+
# @param type [Symbol, String] The type of the column
|
181
|
+
# @param kwargs [Hash] Arbitrary options to be passed to the backend.
|
182
|
+
def add_column(table, name, type, **kwargs)
|
183
|
+
current_instructions << Instructions::AddColumn.new(
|
184
|
+
table: table,
|
185
|
+
name: name,
|
186
|
+
type: type,
|
187
|
+
**kwargs,
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Remove an existing column.
|
192
|
+
# @param table [Symbol, String] The name of the table to remove the column
|
193
|
+
# from.
|
194
|
+
# @param name [Symbol, String] The name of the column
|
195
|
+
# @param extra_args [Hash] Arbitrary options to be passed to the backend.
|
196
|
+
def remove_column(table, name, **extra_args)
|
197
|
+
current_instructions << Instructions::RemoveColumn.new(
|
198
|
+
**extra_args,
|
199
|
+
table: table,
|
200
|
+
name: name,
|
201
|
+
)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Add a foreign key constraint. The generated SQL will include the NOT VALID
|
205
|
+
# parameter, which will prevent immediate validation of the constraint, which
|
206
|
+
# locks the target table for writes potentially for a long time. Use the separate
|
207
|
+
# #validate_constraint method, in a separate migration; this only takes a row-level
|
208
|
+
# lock as it scans through.
|
209
|
+
# @param table [Symbol, String] The name of the table with the reference column
|
210
|
+
# @param target [Symbol, String] The name of the referenced table
|
211
|
+
# @param column [Symbol, String] The name of the reference column. If omitted, will
|
212
|
+
# default to the singular of target + "_id"
|
213
|
+
# @param name [Symbol, String] The name of the constraint to create. Defaults to
|
214
|
+
# table_target_fk
|
215
|
+
def add_foreign_key(table, target, column: nil, name: nil)
|
216
|
+
current_instructions << Instructions::AddForeignKey.new(
|
217
|
+
table: table,
|
218
|
+
target: target,
|
219
|
+
column: column,
|
220
|
+
name: name,
|
221
|
+
)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Add a check constraint, in the NOT VALID state.
|
225
|
+
# @param table [Symbol, String] The name of the table with the column
|
226
|
+
# @param name [Symbol, String] The name of the constraint to create
|
227
|
+
# @param check [Symbol, String] The predicate to check
|
228
|
+
def add_check_constraint(table, name, check)
|
229
|
+
current_instructions << Instructions::AddCheckConstraint.new(
|
230
|
+
table: table,
|
231
|
+
name: name,
|
232
|
+
check: check,
|
233
|
+
)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Validates an existing foreign key constraint.
|
237
|
+
# @param table [Symbol, String] The name of the table with the constraint
|
238
|
+
# @param name [Symbol, String] The name of the constraint
|
239
|
+
def validate_constraint(table, name)
|
240
|
+
current_instructions << Instructions::ValidateConstraint.new(
|
241
|
+
table: table,
|
242
|
+
name: name,
|
243
|
+
)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Drops an existing constraint.
|
247
|
+
# @param table [Symbol, String] The name of the table with the constraint
|
248
|
+
# @param name [Symbol, String] The name of the constraint
|
249
|
+
def drop_constraint(table, name)
|
250
|
+
current_instructions << Instructions::DropConstraint.new(
|
251
|
+
table: table,
|
252
|
+
name: name,
|
253
|
+
)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Drops an existing NOT NULL constraint. Please note that this migration is
|
257
|
+
# not safely reversible; to enforce NOT NULL like behaviour, use a CHECK
|
258
|
+
# constraint and validate it in a separate migration.
|
259
|
+
# @param table [Symbol, String] The name of the table with the constraint
|
260
|
+
# @param column [Symbol, String] The name of the column to remove NOT NULL
|
261
|
+
# constraint from
|
262
|
+
def remove_not_null_constraint(table, column)
|
263
|
+
current_instructions << Instructions::RemoveNotNullConstraint.new(
|
264
|
+
table: table,
|
265
|
+
column: column,
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
# Changes the default value for this column when new rows are inserted into
|
270
|
+
# the table.
|
271
|
+
# @param table [Symbol, String] The name of the table with the column
|
272
|
+
# @param column [Symbol, String] The name of the column to change
|
273
|
+
# @param value [Object] The new default value
|
274
|
+
def change_column_default(table, column, value)
|
275
|
+
current_instructions << Instructions::ChangeColumnDefault.new(
|
276
|
+
table: table,
|
277
|
+
column: column,
|
278
|
+
value: value,
|
279
|
+
)
|
280
|
+
end
|
281
|
+
|
282
|
+
# Raises an `ActiveRecord::IrreversibleMigration` error for use in
|
283
|
+
# irreversible migrations
|
284
|
+
def irreversible_migration
|
285
|
+
current_instructions << Instructions::IrreversibleMigration.new
|
286
|
+
end
|
287
|
+
|
288
|
+
# @api private
|
289
|
+
def compile_instructions(direction)
|
290
|
+
@direction = direction
|
291
|
+
|
292
|
+
public_send(direction) unless current_instructions.any?
|
293
|
+
|
294
|
+
current_instructions
|
295
|
+
end
|
296
|
+
|
297
|
+
# @api private
|
298
|
+
def validate
|
299
|
+
validator.call(self)
|
300
|
+
rescue NotImplementedError => e
|
301
|
+
Validation::Result.new << failure(e.message)
|
302
|
+
end
|
303
|
+
|
304
|
+
def disable_lock_timeout?
|
305
|
+
if self.class.lock_timeout.nil?
|
306
|
+
strictest_lock == LockWeights::SHARE
|
307
|
+
else
|
308
|
+
false
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def disable_statement_timeout?
|
313
|
+
if self.class.statement_timeout.nil?
|
314
|
+
strictest_lock == LockWeights::SHARE
|
315
|
+
else
|
316
|
+
false
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def name
|
321
|
+
self.class.name
|
322
|
+
end
|
323
|
+
|
324
|
+
def respond_to_missing?(name)
|
325
|
+
Nandi.config.custom_methods.key?(name) || super
|
326
|
+
end
|
327
|
+
|
328
|
+
def mixins
|
329
|
+
(up_instructions + down_instructions).inject([]) do |mixins, i|
|
330
|
+
i.respond_to?(:mixins) ? [*mixins, *i.mixins] : mixins
|
331
|
+
end.uniq
|
332
|
+
end
|
333
|
+
|
334
|
+
def method_missing(name, *args, &block)
|
335
|
+
if Nandi.config.custom_methods.key?(name)
|
336
|
+
invoke_custom_method(name, *args, &block)
|
337
|
+
else
|
338
|
+
super
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
private
|
343
|
+
|
344
|
+
attr_reader :validator
|
345
|
+
|
346
|
+
def current_instructions
|
347
|
+
@instructions[@direction]
|
348
|
+
end
|
349
|
+
|
350
|
+
def default_statement_timeout
|
351
|
+
Nandi.config.access_exclusive_statement_timeout
|
352
|
+
end
|
353
|
+
|
354
|
+
def default_lock_timeout
|
355
|
+
Nandi.config.access_exclusive_lock_timeout
|
356
|
+
end
|
357
|
+
|
358
|
+
def invoke_custom_method(name, *args, &block)
|
359
|
+
klass = Nandi.config.custom_methods[name]
|
360
|
+
current_instructions << klass.new(*args, &block)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|