online_migrations 0.36.0 → 0.37.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 +4 -4
- data/CHANGELOG.md +10 -0
- data/docs/background_data_migrations.md +12 -0
- data/docs/background_schema_migrations.md +16 -4
- data/lib/online_migrations/background_data_migrations/migration.rb +20 -2
- data/lib/online_migrations/background_schema_migrations/migration.rb +14 -0
- data/lib/online_migrations/command_checker.rb +4 -3
- data/lib/online_migrations/command_recorder.rb +153 -0
- data/lib/online_migrations/foreign_keys_collector.rb +5 -2
- data/lib/online_migrations/schema_cache.rb +8 -0
- data/lib/online_migrations/schema_dumper.rb +7 -1
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +6 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdad1569af01bacbaa092898b51ba64ad8f6c9746c091441af38b11921bbe2cc
|
|
4
|
+
data.tar.gz: 7f3d23f9e0d5b78fb958322e852bcc274e73ef9c035e05e2e072bec1ebcca086
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2fce7817c096a2ca24fed7f8f4984a28bb12a5b4992dd395da4b6ae843ed39684f0917ad0f112c81d7b46ee178a2f3424d7ac9426d32b8ed4a1a83251398985f
|
|
7
|
+
data.tar.gz: e2cd67a8215dbb287ebdd306c574cbbbfaaafcd5c55b0fb2ec2f393f363870ad3e228dd83fedca3b44287baf148504edf2ca191103e179b63e92675636328c4b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## master (unreleased)
|
|
2
2
|
|
|
3
|
+
## 0.37.0 (2026-09-23)
|
|
4
|
+
|
|
5
|
+
- Add ability to restart background migrations
|
|
6
|
+
- Fix collecting referenced tables when using `foreign_key: { to_table: ... }`
|
|
7
|
+
|
|
8
|
+
## 0.36.1 (2026-09-23)
|
|
9
|
+
|
|
10
|
+
- Fix compatibility with future Rails 8.2
|
|
11
|
+
- Fix pausing already paused background data migrations
|
|
12
|
+
|
|
3
13
|
## 0.36.0 (2026-07-20)
|
|
4
14
|
|
|
5
15
|
- Serialize to JSON `cursor` value for background data migrations
|
|
@@ -354,6 +354,18 @@ migration.retry # => `true` if scheduled to be retried, `false` - if not
|
|
|
354
354
|
|
|
355
355
|
The migration will be retried on the next Scheduler run.
|
|
356
356
|
|
|
357
|
+
## Restarting a migration
|
|
358
|
+
|
|
359
|
+
To restart a migration, run:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
migration = OnlineMigrations::DataMigrations::Migration.find(id)
|
|
363
|
+
migration.force_restart
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
It ignores any previous status and marks the migration for the restart. Can be useful if the migration is stuck or
|
|
367
|
+
you need to rerun an already finished migration etc.
|
|
368
|
+
|
|
357
369
|
## Configuring
|
|
358
370
|
|
|
359
371
|
There are a few configurable options for the data migrations. Custom configurations should be placed in a `online_migrations.rb` initializer.
|
|
@@ -77,7 +77,7 @@ You shouldn't depend on the schema until the background schema migration is fini
|
|
|
77
77
|
To retry a failed migration, run:
|
|
78
78
|
|
|
79
79
|
```ruby
|
|
80
|
-
migration = OnlineMigrations::
|
|
80
|
+
migration = OnlineMigrations::SchemaMigrations::Migration.find(id)
|
|
81
81
|
migration.retry # => `true` if scheduled to be retried, `false` - if not
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -88,10 +88,22 @@ The migration will be retried on the next Scheduler run.
|
|
|
88
88
|
To cancel an existing migration from future performing, run:
|
|
89
89
|
|
|
90
90
|
```ruby
|
|
91
|
-
migration = OnlineMigrations::
|
|
91
|
+
migration = OnlineMigrations::SchemaMigrations::Migration.find(id)
|
|
92
92
|
migration.cancel
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
## Restarting a migration
|
|
96
|
+
|
|
97
|
+
To restart a migration, run:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
migration = OnlineMigrations::SchemaMigrations::Migration.find(id)
|
|
101
|
+
migration.force_restart
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It ignores any previous status and marks the migration for the restart. Can be useful if the migration is stuck or
|
|
105
|
+
you need to rerun a cancelled migration.
|
|
106
|
+
|
|
95
107
|
## Instrumentation
|
|
96
108
|
|
|
97
109
|
Background schema migrations use the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API.
|
|
@@ -156,7 +168,7 @@ Check the [source code](https://github.com/fatkodima/online_migrations/blob/mast
|
|
|
156
168
|
**Note**: You can dynamically change certain migration parameters while the migration is run.
|
|
157
169
|
For example,
|
|
158
170
|
```ruby
|
|
159
|
-
migration = OnlineMigrations::
|
|
171
|
+
migration = OnlineMigrations::SchemaMigrations::Migration.find(id)
|
|
160
172
|
migration.update!(
|
|
161
173
|
statement_timeout: 2.hours, # The statement timeout value used when running the migration
|
|
162
174
|
max_attempts: 10 # The # of attempts the failing migration will be retried
|
|
@@ -182,7 +194,7 @@ end
|
|
|
182
194
|
The error handler should be a lambda that accepts 2 arguments:
|
|
183
195
|
|
|
184
196
|
* `error`: The exception that was raised.
|
|
185
|
-
* `errored_migration`: An `OnlineMigrations::
|
|
197
|
+
* `errored_migration`: An `OnlineMigrations::SchemaMigrations::Migration` object that represents a failed migration.
|
|
186
198
|
|
|
187
199
|
### Multiple databases and sharding
|
|
188
200
|
|
|
@@ -177,12 +177,12 @@ module OnlineMigrations
|
|
|
177
177
|
true
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
-
# Pause this data migration. No-op if migration is completed.
|
|
180
|
+
# Pause this data migration. No-op if migration is completed or already paused.
|
|
181
181
|
#
|
|
182
182
|
# @return [Boolean] whether this data migration was paused.
|
|
183
183
|
#
|
|
184
184
|
def pause
|
|
185
|
-
return false if completed?
|
|
185
|
+
return false if paused? || completed?
|
|
186
186
|
|
|
187
187
|
if pending? || enqueued? || delayed? || stuck? || errored?
|
|
188
188
|
paused!
|
|
@@ -240,6 +240,24 @@ module OnlineMigrations
|
|
|
240
240
|
true
|
|
241
241
|
end
|
|
242
242
|
|
|
243
|
+
# Force restart this data migration.
|
|
244
|
+
#
|
|
245
|
+
def force_restart
|
|
246
|
+
update_columns(
|
|
247
|
+
status: :pending,
|
|
248
|
+
cursor: nil,
|
|
249
|
+
jid: nil,
|
|
250
|
+
started_at: nil,
|
|
251
|
+
finished_at: nil,
|
|
252
|
+
tick_total: nil,
|
|
253
|
+
tick_count: 0,
|
|
254
|
+
time_running: 0.0,
|
|
255
|
+
error_class: nil,
|
|
256
|
+
error_message: nil,
|
|
257
|
+
backtrace: nil
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
243
261
|
# @private
|
|
244
262
|
def persist_progress(cursor, number_of_ticks, duration)
|
|
245
263
|
update!(
|
|
@@ -135,6 +135,20 @@ module OnlineMigrations
|
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
# Force restart this schema migration.
|
|
139
|
+
#
|
|
140
|
+
def force_restart
|
|
141
|
+
update_columns(
|
|
142
|
+
status: :pending,
|
|
143
|
+
started_at: nil,
|
|
144
|
+
finished_at: nil,
|
|
145
|
+
attempts: 0,
|
|
146
|
+
error_class: nil,
|
|
147
|
+
error_message: nil,
|
|
148
|
+
backtrace: nil
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
138
152
|
def index_addition?
|
|
139
153
|
definition.match?(/create (unique )?index/i)
|
|
140
154
|
end
|
|
@@ -490,9 +490,10 @@ module OnlineMigrations
|
|
|
490
490
|
bad_index = index && !concurrently_set
|
|
491
491
|
|
|
492
492
|
foreign_key = options.fetch(:foreign_key, false)
|
|
493
|
+
foreign_key_options = foreign_key.is_a?(Hash) ? foreign_key : {}
|
|
493
494
|
|
|
494
495
|
if foreign_key
|
|
495
|
-
foreign_table_name = Utils.foreign_table_name(ref_name,
|
|
496
|
+
foreign_table_name = Utils.foreign_table_name(ref_name, foreign_key_options)
|
|
496
497
|
@foreign_key_tables << foreign_table_name.to_s
|
|
497
498
|
end
|
|
498
499
|
|
|
@@ -519,9 +520,10 @@ module OnlineMigrations
|
|
|
519
520
|
|
|
520
521
|
def add_reference_concurrently(table_name, ref_name, **options)
|
|
521
522
|
foreign_key = options.fetch(:foreign_key, false)
|
|
523
|
+
foreign_key_options = foreign_key.is_a?(Hash) ? foreign_key : {}
|
|
522
524
|
|
|
523
525
|
if foreign_key
|
|
524
|
-
foreign_table_name = Utils.foreign_table_name(ref_name,
|
|
526
|
+
foreign_table_name = Utils.foreign_table_name(ref_name, foreign_key_options)
|
|
525
527
|
@foreign_key_tables << foreign_table_name.to_s
|
|
526
528
|
end
|
|
527
529
|
|
|
@@ -529,7 +531,6 @@ module OnlineMigrations
|
|
|
529
531
|
type = (options[:type] || :bigint).to_sym
|
|
530
532
|
column_name = "#{ref_name}_id"
|
|
531
533
|
|
|
532
|
-
foreign_key_options = foreign_key.is_a?(Hash) ? foreign_key : {}
|
|
533
534
|
check_mismatched_foreign_key_type(table_name, column_name, type, **foreign_key_options)
|
|
534
535
|
end
|
|
535
536
|
end
|
|
@@ -153,4 +153,157 @@ module OnlineMigrations
|
|
|
153
153
|
[:add_text_limit_constraint, args]
|
|
154
154
|
end
|
|
155
155
|
end
|
|
156
|
+
|
|
157
|
+
# @private
|
|
158
|
+
# New format was introduced in https://github.com/rails/rails/pull/58239.
|
|
159
|
+
module CommandRecorder82
|
|
160
|
+
REVERSIBLE_AND_IRREVERSIBLE_METHODS = [
|
|
161
|
+
:update_column_in_batches,
|
|
162
|
+
:initialize_column_rename,
|
|
163
|
+
:initialize_columns_rename,
|
|
164
|
+
:revert_initialize_column_rename,
|
|
165
|
+
:revert_initialize_columns_rename,
|
|
166
|
+
:finalize_column_rename,
|
|
167
|
+
:finalize_columns_rename,
|
|
168
|
+
:revert_finalize_column_rename,
|
|
169
|
+
:revert_finalize_columns_rename,
|
|
170
|
+
:initialize_table_rename,
|
|
171
|
+
:revert_initialize_table_rename,
|
|
172
|
+
:finalize_table_rename,
|
|
173
|
+
:revert_finalize_table_rename,
|
|
174
|
+
:swap_column_names,
|
|
175
|
+
:add_column_with_default,
|
|
176
|
+
:add_not_null_constraint,
|
|
177
|
+
:remove_not_null_constraint,
|
|
178
|
+
:add_text_limit_constraint,
|
|
179
|
+
:remove_text_limit_constraint,
|
|
180
|
+
:add_reference_concurrently,
|
|
181
|
+
:change_column_type_in_background,
|
|
182
|
+
:enqueue_background_data_migration,
|
|
183
|
+
:remove_background_data_migration,
|
|
184
|
+
|
|
185
|
+
# column type change helpers
|
|
186
|
+
:initialize_column_type_change,
|
|
187
|
+
:initialize_columns_type_change,
|
|
188
|
+
:revert_initialize_column_type_change,
|
|
189
|
+
:revert_initialize_columns_type_change,
|
|
190
|
+
:backfill_column_for_type_change,
|
|
191
|
+
:backfill_columns_for_type_change,
|
|
192
|
+
:finalize_column_type_change,
|
|
193
|
+
:finalize_columns_type_change,
|
|
194
|
+
:revert_finalize_column_type_change,
|
|
195
|
+
:cleanup_column_type_change,
|
|
196
|
+
:cleanup_columns_type_change,
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
REVERSIBLE_AND_IRREVERSIBLE_METHODS.each do |method|
|
|
200
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
201
|
+
def #{method}(*args, **kwargs, &block) # def create_table(*args, **kwargs, &block)
|
|
202
|
+
record(:"#{method}", args, kwargs, &block) # record(:create_table, args, kwargs, &block)
|
|
203
|
+
end # end
|
|
204
|
+
RUBY
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
module StraightReversions
|
|
209
|
+
{
|
|
210
|
+
initialize_column_rename: :revert_initialize_column_rename,
|
|
211
|
+
initialize_columns_rename: :revert_initialize_columns_rename,
|
|
212
|
+
finalize_column_rename: :revert_finalize_column_rename,
|
|
213
|
+
finalize_columns_rename: :revert_finalize_columns_rename,
|
|
214
|
+
initialize_table_rename: :revert_initialize_table_rename,
|
|
215
|
+
finalize_table_rename: :revert_finalize_table_rename,
|
|
216
|
+
add_not_null_constraint: :remove_not_null_constraint,
|
|
217
|
+
initialize_column_type_change: :revert_initialize_column_type_change,
|
|
218
|
+
initialize_columns_type_change: :revert_initialize_columns_type_change,
|
|
219
|
+
finalize_column_type_change: :revert_finalize_column_type_change,
|
|
220
|
+
finalize_columns_type_change: :revert_finalize_columns_type_change,
|
|
221
|
+
}.each do |cmd, inv|
|
|
222
|
+
[[inv, cmd], [cmd, inv]].each do |method, inverse|
|
|
223
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
224
|
+
def invert_#{method}(args, kwargs, &block) # def invert_create_table(args, kwargs, &block)
|
|
225
|
+
[:#{inverse}, args, kwargs, block] # [:drop_table, args, kwargs, block]
|
|
226
|
+
end # end
|
|
227
|
+
RUBY
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
include StraightReversions
|
|
233
|
+
|
|
234
|
+
def invert_add_reference_concurrently(args, kwargs)
|
|
235
|
+
[:remove_reference, args, kwargs]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def invert_swap_column_names(args, kwargs)
|
|
239
|
+
table_name, column1, column2 = args
|
|
240
|
+
[:swap_column_names, [table_name, column2, column1], kwargs]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def invert_add_column_with_default(args, kwargs)
|
|
244
|
+
table_name, column_name, = args
|
|
245
|
+
[:remove_column, [table_name, column_name], kwargs]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def invert_revert_initialize_column_rename(args, kwargs)
|
|
249
|
+
_table, column, new_column = args
|
|
250
|
+
if !column || !new_column
|
|
251
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
252
|
+
"revert_initialize_column_rename is only reversible if given a column and new_column."
|
|
253
|
+
end
|
|
254
|
+
[:initialize_column_rename, args, kwargs]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def invert_revert_initialize_columns_rename(args, kwargs)
|
|
258
|
+
_table, old_new_column_hash = args
|
|
259
|
+
if !old_new_column_hash
|
|
260
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
261
|
+
"revert_initialize_columns_rename is only reversible if given a hash of old and new columns."
|
|
262
|
+
end
|
|
263
|
+
[:initialize_columns_rename, args, kwargs]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def invert_finalize_table_rename(args, kwargs)
|
|
267
|
+
_table_name, new_name = args
|
|
268
|
+
if !new_name
|
|
269
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
270
|
+
"finalize_table_rename is only reversible if given a new_name."
|
|
271
|
+
end
|
|
272
|
+
[:revert_finalize_table_rename, args, kwargs]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def invert_revert_initialize_column_type_change(args, kwargs)
|
|
276
|
+
if !args[2]
|
|
277
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
278
|
+
"revert_initialize_column_type_change is only reversible if given a new_type."
|
|
279
|
+
end
|
|
280
|
+
super
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def invert_revert_initialize_columns_type_change(args, kwargs)
|
|
284
|
+
if args[1].empty?
|
|
285
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
286
|
+
"revert_initialize_columns_type_change is only reversible if given a columns_and_types."
|
|
287
|
+
end
|
|
288
|
+
super
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def invert_add_not_null_constraint(args, kwargs)
|
|
292
|
+
kwargs.delete(:validate)
|
|
293
|
+
[:remove_not_null_constraint, args, kwargs]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def invert_add_text_limit_constraint(args, kwargs)
|
|
297
|
+
kwargs.delete(:validate)
|
|
298
|
+
[:remove_text_limit_constraint, args, kwargs]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def invert_remove_text_limit_constraint(args, kwargs)
|
|
302
|
+
if !args[2]
|
|
303
|
+
raise ActiveRecord::IrreversibleMigration, "remove_text_limit_constraint is only reversible if given a limit."
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
[:add_text_limit_constraint, args, kwargs]
|
|
307
|
+
end
|
|
308
|
+
end
|
|
156
309
|
end
|
|
@@ -18,9 +18,12 @@ module OnlineMigrations
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def references(*ref_names, **options)
|
|
21
|
-
|
|
21
|
+
foreign_key = options[:foreign_key]
|
|
22
|
+
|
|
23
|
+
if foreign_key
|
|
24
|
+
foreign_key_options = foreign_key.is_a?(Hash) ? foreign_key : {}
|
|
22
25
|
ref_names.each do |ref_name|
|
|
23
|
-
@referenced_tables << Utils.foreign_table_name(ref_name,
|
|
26
|
+
@referenced_tables << Utils.foreign_table_name(ref_name, foreign_key_options)
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
end
|
|
@@ -79,6 +79,14 @@ module OnlineMigrations
|
|
|
79
79
|
new_column = old_column.dup
|
|
80
80
|
# Active Record defines only reader for :name
|
|
81
81
|
new_column.instance_variable_set(:@name, new_column_name)
|
|
82
|
+
|
|
83
|
+
# https://github.com/rails/rails/commit/0c5b92705960ceff44324e69a921e760f1646503
|
|
84
|
+
# New in Active Record 8.2+.
|
|
85
|
+
if new_column.respond_to?(:slot)
|
|
86
|
+
max_slot = columns.map(&:slot).max
|
|
87
|
+
new_column.instance_variable_set(:@slot, max_slot + 1)
|
|
88
|
+
end
|
|
89
|
+
|
|
82
90
|
# Correspond to the Active Record freezing of each column
|
|
83
91
|
columns << new_column.freeze
|
|
84
92
|
end
|
|
@@ -15,7 +15,13 @@ module OnlineMigrations
|
|
|
15
15
|
|
|
16
16
|
class WrappedConnection < SimpleDelegator
|
|
17
17
|
def columns(table_name)
|
|
18
|
-
super
|
|
18
|
+
columns = super
|
|
19
|
+
|
|
20
|
+
if Utils.ar_version >= 8.2 && columns.is_a?(Hash)
|
|
21
|
+
columns.transform_values { |v| v.sort_by(&:name) }
|
|
22
|
+
else
|
|
23
|
+
columns.sort_by(&:name)
|
|
24
|
+
end
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
end
|
data/lib/online_migrations.rb
CHANGED
|
@@ -119,7 +119,12 @@ module OnlineMigrations
|
|
|
119
119
|
ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache)
|
|
120
120
|
|
|
121
121
|
ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(OnlineMigrations::DatabaseTasks)
|
|
122
|
-
|
|
122
|
+
|
|
123
|
+
if Utils.ar_version >= 8.2
|
|
124
|
+
ActiveRecord::Migration::CommandRecorder.include(OnlineMigrations::CommandRecorder82)
|
|
125
|
+
else
|
|
126
|
+
ActiveRecord::Migration::CommandRecorder.include(OnlineMigrations::CommandRecorder)
|
|
127
|
+
end
|
|
123
128
|
|
|
124
129
|
if !ActiveRecord::Batches::BatchEnumerator.method_defined?(:use_ranges)
|
|
125
130
|
ActiveRecord::Batches::BatchEnumerator.include(OnlineMigrations::ActiveRecordBatchEnumerator)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: online_migrations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.37.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fatkodima
|
|
@@ -116,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
117
|
version: '0'
|
|
118
118
|
requirements: []
|
|
119
|
-
rubygems_version: 4.0.
|
|
119
|
+
rubygems_version: 4.0.20
|
|
120
120
|
specification_version: 4
|
|
121
121
|
summary: Catch unsafe PostgreSQL migrations in development and run them easier in
|
|
122
122
|
production
|