safe-pg-migrations 3.1.4 → 4.0.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/README.md +55 -15
- data/lib/safe-pg-migrations/base.rb +1 -0
- data/lib/safe-pg-migrations/helpers/batch_over.rb +25 -0
- data/lib/safe-pg-migrations/helpers/volatile_default.rb +33 -0
- data/lib/safe-pg-migrations/plugins/statement_insurer/add_column.rb +50 -1
- data/lib/safe-pg-migrations/plugins/statement_insurer.rb +2 -6
- data/lib/safe-pg-migrations/plugins/strong_migrations_integration.rb +48 -19
- data/lib/safe-pg-migrations/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3edbea6934dae51bfef3563e0d24e04d3b589bf417ffd9635687265f1b973613
|
|
4
|
+
data.tar.gz: b4714af68927b76b3580b9d74b83d96f73bdaa8127395ec030379d29886d70b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 774fac16319205c8bafb5e1f346aa23bfc6de00df261bb307ec0a095be56da56242c2103fb3e9dfc7e9f4d7ce0023817eb5f8680476d3e0dc61fd328ebb23a72
|
|
7
|
+
data.tar.gz: 7f6b0661848d67fb0193155b21211486413fa39d1769caa7524f8429bc8303f1b71faba7fd502f0a137352c09f61a4487644cb62bf710fcd903b4d281cde326d
|
data/README.md
CHANGED
|
@@ -116,11 +116,25 @@ PG will still needs to update every row of the table, and will most likely state
|
|
|
116
116
|
<details>
|
|
117
117
|
<summary>Safe add_column - adding a volatile default value</summary>
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
> **⚠️ ERROR**
|
|
120
|
+
>
|
|
121
|
+
> Using `default_value_backfill: :update_in_batches` with **volatile defaults** (like `NOW()`, `clock_timestamp()`, `random()`, `gen_random_uuid()`) is **not allowed** and will raise an error.
|
|
122
|
+
>
|
|
123
|
+
> Volatile defaults are non-deterministic functions that are evaluated per row, causing migrations to hang for a very long time on large tables. You must backfill them manually with proper monitoring and control.
|
|
124
|
+
>
|
|
125
|
+
> **Non-volatile defaults** (like fixed strings, numbers, booleans) are still safe to use with automatic backfill.
|
|
126
|
+
|
|
127
|
+
**Safe PG Migrations** provides the extra option parameter `default_value_backfill:`. When your migration is adding a **non-volatile** default value, the option `:update_in_batches` can be set. It will automatically backfill the value in a safe manner.
|
|
120
128
|
|
|
121
129
|
```ruby
|
|
130
|
+
# ✅ SAFE - non-volatile default with automatic backfill
|
|
122
131
|
safety_assured do
|
|
123
|
-
add_column :users, :
|
|
132
|
+
add_column :users, :status, :string, default: 'active', default_value_backfill: :update_in_batches
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ❌ NOT ALLOWED - volatile default with automatic backfill (will raise an error)
|
|
136
|
+
safety_assured do
|
|
137
|
+
add_column :users, :created_at, :datetime, default: -> { 'clock_timestamp()' }, default_value_backfill: :update_in_batches
|
|
124
138
|
end
|
|
125
139
|
```
|
|
126
140
|
|
|
@@ -132,18 +146,44 @@ More specifically, it will:
|
|
|
132
146
|
4. change the column to `null: false`, if defined in the parameters, following the algorithm we have defined below.
|
|
133
147
|
|
|
134
148
|
---
|
|
135
|
-
**NOTE**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
1.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
**NOTE: Manual Backfill for Volatile Defaults**
|
|
150
|
+
|
|
151
|
+
For **volatile defaults** (non-deterministic functions), you MUST backfill manually. Split the operation into multiple steps in this EXACT order:
|
|
152
|
+
|
|
153
|
+
1. **ALTER COLUMN SET DEFAULT** (for new and updated rows)
|
|
154
|
+
```ruby
|
|
155
|
+
change_column_default :users, :created_at, -> { 'NOW()' }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
2. **ADD CONSTRAINT CHECK NOT NULL NOT VALID** (for new and updated rows, only if you need NOT NULL)
|
|
159
|
+
```ruby
|
|
160
|
+
add_check_constraint :users, "created_at IS NOT NULL",
|
|
161
|
+
name: "check_users_created_at_not_null",
|
|
162
|
+
validate: false
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
3. **BACKFILL** the column using a job (chunk over the primary key)
|
|
166
|
+
```ruby
|
|
167
|
+
# Your own script to backfill in batches
|
|
168
|
+
User.in_batches.update_all("created_at = NOW()")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
4. **VALIDATE CONSTRAINT** (check whole table, only if you added the constraint)
|
|
172
|
+
```ruby
|
|
173
|
+
validate_check_constraint :users, name: "check_users_created_at_not_null"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
5. **ALTER COLUMN SET NOT NULL** (only if you need NOT NULL)
|
|
177
|
+
```ruby
|
|
178
|
+
change_column_null :users, :created_at, false
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
6. **DROP CONSTRAINT** (only if you added the constraint)
|
|
182
|
+
```ruby
|
|
183
|
+
remove_check_constraint :users, name: "check_users_created_at_not_null"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
For **non-volatile defaults**, data backfill with `:update_in_batches` is safe but takes time. If your table is very large, you might want to backfill manually for better control.
|
|
147
187
|
---
|
|
148
188
|
|
|
149
189
|
`default_value_backfill:` also accept the value `:auto` which is set by default. In this case, **Safe PG Migrations** will not backfill data and will let PostgreSQL handle it itself.
|
|
@@ -161,7 +201,7 @@ It is also possible to set a threshold for the table size, above which the migra
|
|
|
161
201
|
|
|
162
202
|
Creating an index requires a `SHARE` lock on the target table which blocks all write on the table while the index is created (which can take some time on a large table). This is usually not practical in a live environment. Thus, **Safe PG Migrations** ensures indexes are created concurrently.
|
|
163
203
|
|
|
164
|
-
As `CREATE INDEX CONCURRENTLY` and `DROP INDEX CONCURRENTLY` are non-blocking operations (ie: read/write operations on the table are still possible), **Safe PG Migrations** sets a lock timeout to
|
|
204
|
+
As `CREATE INDEX CONCURRENTLY` and `DROP INDEX CONCURRENTLY` are non-blocking operations (ie: read/write operations on the table are still possible), **Safe PG Migrations** sets a statement and lock timeout to 0 seconds for those 2 specific statements.
|
|
165
205
|
|
|
166
206
|
If you still get lock timeout while adding / removing indexes, it might be for one of those reasons:
|
|
167
207
|
|
|
@@ -7,6 +7,7 @@ require 'safe-pg-migrations/helpers/index_helper'
|
|
|
7
7
|
require 'safe-pg-migrations/helpers/batch_over'
|
|
8
8
|
require 'safe-pg-migrations/helpers/session_setting_management'
|
|
9
9
|
require 'safe-pg-migrations/helpers/statements_helper'
|
|
10
|
+
require 'safe-pg-migrations/helpers/volatile_default'
|
|
10
11
|
require 'safe-pg-migrations/plugins/verbose_sql_logger'
|
|
11
12
|
require 'safe-pg-migrations/plugins/blocking_activity_logger'
|
|
12
13
|
require 'safe-pg-migrations/plugins/statement_insurer/add_column'
|
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module SafePgMigrations
|
|
4
4
|
module Helpers
|
|
5
|
+
# This helper class allows to iterate over records in batches, in a similar
|
|
6
|
+
# way to ActiveRecord's `in_batches` method with the :use_ranges option,
|
|
7
|
+
# which was introduced in ActiveRecord 7.1, see:
|
|
8
|
+
#
|
|
9
|
+
# - https://api.rubyonrails.org/classes/ActiveRecord/Batches.html#method-i-in_batches
|
|
10
|
+
# - https://github.com/rails/rails/blob/v7.1.0/activerecord/CHANGELOG.md
|
|
11
|
+
# - https://github.com/rails/rails/pull/45414
|
|
12
|
+
# - https://github.com/rails/rails/commit/620f24782977b8e53e06cf0e2c905a591936e990
|
|
13
|
+
#
|
|
14
|
+
# In ActiveRecord 8.1, `in_baches(use_ranges: true)` was optimized further
|
|
15
|
+
# to use less cpu, memory, and bandwidth, see:
|
|
16
|
+
#
|
|
17
|
+
# - https://github.com/rails/rails/releases/tag/v8.1.0
|
|
18
|
+
# - https://github.com/rails/rails/pull/51243
|
|
19
|
+
# - https://github.com/rails/rails/commit/c097bf6c24443323da8fe64030dd963951121dea
|
|
20
|
+
#
|
|
21
|
+
# If using ActiveRecord 8.1 or later, it's recommended to use the built-in
|
|
22
|
+
# method, e.g.
|
|
23
|
+
#
|
|
24
|
+
# User.in_batches(of: 100, use_ranges: true).each { |batch| ... }
|
|
25
|
+
#
|
|
26
|
+
# Otherwise, this helper can be used as a fallback:
|
|
27
|
+
#
|
|
28
|
+
# SafePgMigrations::Helpers::BatchOver.new(User, of: 100).each_batch { |batch| ... }
|
|
29
|
+
#
|
|
5
30
|
class BatchOver
|
|
6
31
|
def initialize(model, of: SafePgMigrations.config.backfill_batch_size)
|
|
7
32
|
@model = model
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafePgMigrations
|
|
4
|
+
module Helpers
|
|
5
|
+
module VolatileDefault
|
|
6
|
+
VOLATILE_DEFAULT_PATTERNS = [
|
|
7
|
+
/\bclock_timestamp\s*\(/i,
|
|
8
|
+
/\bnow\s*\(/i,
|
|
9
|
+
/\bcurrent_timestamp\b/i,
|
|
10
|
+
/\bcurrent_time\b/i,
|
|
11
|
+
/\bcurrent_date\b/i,
|
|
12
|
+
/\brandom\s*\(/i,
|
|
13
|
+
/\buuid_generate/i,
|
|
14
|
+
/\bgen_random_uuid\s*\(/i,
|
|
15
|
+
/\btimeofday\s*\(/i,
|
|
16
|
+
/\btransaction_timestamp\s*\(/i,
|
|
17
|
+
/\bstatement_timestamp\s*\(/i,
|
|
18
|
+
/\bnextval\s*\(/i,
|
|
19
|
+
/\bgen_random_bytes\s*\(/i,
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def volatile_default?(default)
|
|
25
|
+
return false if default.nil?
|
|
26
|
+
return true if default.is_a?(Proc)
|
|
27
|
+
return false unless default.is_a?(String)
|
|
28
|
+
|
|
29
|
+
VOLATILE_DEFAULT_PATTERNS.any? { |pattern| default.match?(pattern) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -8,6 +8,11 @@ module SafePgMigrations
|
|
|
8
8
|
|
|
9
9
|
options.delete(:default_value_backfill)
|
|
10
10
|
|
|
11
|
+
default = options[:default]
|
|
12
|
+
|
|
13
|
+
# Raise if using automatic backfill with a volatile default
|
|
14
|
+
raise_on_volatile_default(table_name, column_name, default) if volatile_default?(default)
|
|
15
|
+
|
|
11
16
|
raise <<~ERROR unless backfill_column_default_safe?(table_name)
|
|
12
17
|
Table #{table_name} has more than #{SafePgMigrations.config.default_value_backfill_threshold} rows.
|
|
13
18
|
Backfilling the default value for column #{column_name} on table #{table_name} would take too long.
|
|
@@ -53,11 +58,55 @@ module SafePgMigrations
|
|
|
53
58
|
|
|
54
59
|
Helpers::Logger.say_method_call(:backfill_column_default, table_name, column_name)
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
batch_handler = lambda do |batch|
|
|
57
62
|
batch.update_all("#{quoted_column_name} = DEFAULT")
|
|
58
63
|
|
|
59
64
|
sleep SafePgMigrations.config.backfill_pause
|
|
60
65
|
end
|
|
66
|
+
|
|
67
|
+
backfill_batch_size = SafePgMigrations.config.backfill_batch_size
|
|
68
|
+
|
|
69
|
+
if ActiveRecord.version >= Gem::Version.new('8.1')
|
|
70
|
+
model.in_batches(of: backfill_batch_size, use_ranges: true).each(&batch_handler)
|
|
71
|
+
else
|
|
72
|
+
Helpers::BatchOver.new(model, of: backfill_batch_size).each_batch(&batch_handler)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def volatile_default?(default)
|
|
77
|
+
Helpers::VolatileDefault.volatile_default?(default)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def raise_on_volatile_default(table_name, column_name, default)
|
|
81
|
+
default_display = default.is_a?(Proc) ? '<Proc>' : default
|
|
82
|
+
|
|
83
|
+
raise <<~ERROR
|
|
84
|
+
Using default_value_backfill: :update_in_batches with volatile default '#{default_display}'
|
|
85
|
+
on #{table_name}.#{column_name} is not allowed.
|
|
86
|
+
|
|
87
|
+
Volatile defaults are non-deterministic functions like gen_random_uuid(), now(), or clock_timestamp().
|
|
88
|
+
They are evaluated per row and can cause migrations to hang for a very long time on large tables.
|
|
89
|
+
You should backfill them "manually" with proper monitoring and control.
|
|
90
|
+
|
|
91
|
+
Split the operation into multiple steps in this EXACT order:
|
|
92
|
+
|
|
93
|
+
1. ALTER COLUMN SET DEFAULT (for new and updated rows)
|
|
94
|
+
change_column_default :#{table_name}, :#{column_name}, '#{default_display}'
|
|
95
|
+
2. ADD CONSTRAINT CHECK NOT NULL NOT VALID (for new and updated rows)
|
|
96
|
+
# Only if you need NOT NULL:
|
|
97
|
+
add_check_constraint :#{table_name}, "#{column_name} IS NOT NULL", name: "check_#{table_name}_#{column_name}_not_null", validate: false
|
|
98
|
+
3. BACKFILL the column (using a job or something else, chucking by PK)
|
|
99
|
+
# Your own script to backfill in batches
|
|
100
|
+
4. VALIDATE CONSTRAINT (check whole table)
|
|
101
|
+
# Only if you added the constraint in step 3:
|
|
102
|
+
validate_check_constraint :#{table_name}, name: "check_#{table_name}_#{column_name}_not_null"
|
|
103
|
+
5. ALTER COLUMN SET NOT NULL
|
|
104
|
+
# Only if you need NOT NULL:
|
|
105
|
+
change_column_null :#{table_name}, :#{column_name}, false
|
|
106
|
+
6. DROP CONSTRAINT
|
|
107
|
+
# Only if you added the constraint in step 3:
|
|
108
|
+
remove_check_constraint :#{table_name}, name: "check_#{table_name}_#{column_name}_not_null"
|
|
109
|
+
ERROR
|
|
61
110
|
end
|
|
62
111
|
end
|
|
63
112
|
end
|
|
@@ -47,18 +47,14 @@ module SafePgMigrations
|
|
|
47
47
|
super do |td|
|
|
48
48
|
yield td if block_given?
|
|
49
49
|
td.indexes.map! do |key, index_options|
|
|
50
|
-
index_options[:algorithm]
|
|
50
|
+
index_options[:algorithm] = nil unless index_options.key?(:algorithm)
|
|
51
51
|
[key, index_options]
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def add_index(table_name, column_name, **options)
|
|
57
|
-
|
|
58
|
-
options.delete :algorithm
|
|
59
|
-
else
|
|
60
|
-
options[:algorithm] = :concurrently
|
|
61
|
-
end
|
|
57
|
+
options[:algorithm] = :concurrently unless options.key?(:algorithm)
|
|
62
58
|
|
|
63
59
|
Helpers::Logger.say_method_call(:add_index, table_name, column_name, **options)
|
|
64
60
|
without_timeout { super(table_name, column_name, **options) }
|
|
@@ -12,27 +12,12 @@ module SafePgMigrations
|
|
|
12
12
|
next unless method == :add_column
|
|
13
13
|
|
|
14
14
|
options = args.last.is_a?(Hash) ? args.last : {}
|
|
15
|
-
|
|
15
|
+
default = options[:default]
|
|
16
16
|
default_value_backfill = options.fetch(:default_value_backfill, :auto)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
check_message = <<~CHECK
|
|
20
|
-
default_value_backfill: :update_in_batches will take time if the table is too big.
|
|
21
|
-
|
|
22
|
-
Your configuration sets a pause of #{SafePgMigrations.config.backfill_pause} seconds between batches of
|
|
23
|
-
#{SafePgMigrations.config.backfill_batch_size} rows. Each batch execution will take time as well. Please
|
|
24
|
-
check that the estimated duration of the migration is acceptable
|
|
25
|
-
before adding `safety_assured`.
|
|
26
|
-
CHECK
|
|
27
|
-
|
|
28
|
-
check_message += <<~CHECK if SafePgMigrations.config.default_value_backfill_threshold
|
|
18
|
+
next unless default_value_backfill == :update_in_batches
|
|
29
19
|
|
|
30
|
-
|
|
31
|
-
#{SafePgMigrations.config.default_value_backfill_threshold} rows.
|
|
32
|
-
CHECK
|
|
33
|
-
|
|
34
|
-
stop! check_message
|
|
35
|
-
end
|
|
20
|
+
stop! StrongMigrationsIntegration.send(:backfill_check_message, default)
|
|
36
21
|
end
|
|
37
22
|
end
|
|
38
23
|
|
|
@@ -41,6 +26,39 @@ module SafePgMigrations
|
|
|
41
26
|
def strong_migration_available?
|
|
42
27
|
Object.const_defined? :StrongMigrations
|
|
43
28
|
end
|
|
29
|
+
|
|
30
|
+
def backfill_check_message(default)
|
|
31
|
+
if Helpers::VolatileDefault.volatile_default?(default)
|
|
32
|
+
default_display = default.is_a?(Proc) ? '<Proc>' : default
|
|
33
|
+
|
|
34
|
+
<<~CHECK
|
|
35
|
+
Using default_value_backfill: :update_in_batches with volatile default '#{default_display}' is not allowed.
|
|
36
|
+
|
|
37
|
+
Volatile defaults (like NOW(), clock_timestamp(), random()) are evaluated per row and can cause
|
|
38
|
+
migrations to hang for a very long time on large tables.
|
|
39
|
+
|
|
40
|
+
Please backfill volatile defaults manually instead. See the safe-pg-migrations README for the
|
|
41
|
+
recommended approach.
|
|
42
|
+
CHECK
|
|
43
|
+
else
|
|
44
|
+
check_message = <<~CHECK
|
|
45
|
+
default_value_backfill: :update_in_batches will take time if the table is too big.
|
|
46
|
+
|
|
47
|
+
Your configuration sets a pause of #{SafePgMigrations.config.backfill_pause} seconds between batches of
|
|
48
|
+
#{SafePgMigrations.config.backfill_batch_size} rows. Each batch execution will take time as well. Please
|
|
49
|
+
check that the estimated duration of the migration is acceptable
|
|
50
|
+
before adding `safety_assured`.
|
|
51
|
+
CHECK
|
|
52
|
+
|
|
53
|
+
check_message += <<~CHECK if SafePgMigrations.config.default_value_backfill_threshold
|
|
54
|
+
|
|
55
|
+
Also, please note that SafePgMigrations is configured to raise if the table has more than
|
|
56
|
+
#{SafePgMigrations.config.default_value_backfill_threshold} rows.
|
|
57
|
+
CHECK
|
|
58
|
+
|
|
59
|
+
check_message
|
|
60
|
+
end
|
|
61
|
+
end
|
|
44
62
|
end
|
|
45
63
|
|
|
46
64
|
SAFE_METHODS = %i[
|
|
@@ -64,9 +82,20 @@ module SafePgMigrations
|
|
|
64
82
|
def add_column(table_name, *args, **options)
|
|
65
83
|
return super unless respond_to?(:safety_assured)
|
|
66
84
|
|
|
67
|
-
|
|
85
|
+
default_value_backfill = options.fetch(:default_value_backfill, :auto)
|
|
68
86
|
|
|
87
|
+
# Auto backfill is safe - use safety_assured
|
|
88
|
+
return safety_assured { super } if default_value_backfill == :auto
|
|
89
|
+
|
|
90
|
+
# :update_in_batches always requires explicit safety_assured (volatile defaults will be
|
|
91
|
+
# blocked by the check above before reaching this point)
|
|
69
92
|
super
|
|
70
93
|
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def volatile_default?(default)
|
|
98
|
+
Helpers::VolatileDefault.volatile_default?(default)
|
|
99
|
+
end
|
|
71
100
|
end
|
|
72
101
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: safe-pg-migrations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthieu Prat
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date:
|
|
14
|
+
date: 2026-03-13 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: activerecord
|
|
@@ -61,6 +61,7 @@ files:
|
|
|
61
61
|
- lib/safe-pg-migrations/helpers/satisfied_helper.rb
|
|
62
62
|
- lib/safe-pg-migrations/helpers/session_setting_management.rb
|
|
63
63
|
- lib/safe-pg-migrations/helpers/statements_helper.rb
|
|
64
|
+
- lib/safe-pg-migrations/helpers/volatile_default.rb
|
|
64
65
|
- lib/safe-pg-migrations/plugins/blocking_activity_logger.rb
|
|
65
66
|
- lib/safe-pg-migrations/plugins/idempotent_statements.rb
|
|
66
67
|
- lib/safe-pg-migrations/plugins/statement_insurer.rb
|