pg_online_schema_change 0.9.5 → 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +14 -14
- data/README.md +8 -17
- data/lib/pg_online_schema_change/client.rb +4 -0
- data/lib/pg_online_schema_change/orchestrate.rb +47 -12
- data/lib/pg_online_schema_change/query.rb +24 -10
- data/lib/pg_online_schema_change/version.rb +1 -1
- metadata +11 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1838409c592e1497790a6e02f883b857851450181854baf660b9b7ee3664c6e
|
4
|
+
data.tar.gz: 4f7e58eeafb2413ec774988b3190391033e819a444a25da3b1ff0fcbf6bf64e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c769f18ab1815fa543246fd18bb3157add074894f15371abb88e1af61e9381eade6850e4fba3a7d6d3701711594ed10a9018c7fe1c45a5ffdaf501891bf7e548
|
7
|
+
data.tar.gz: 84667ac77dab1d97478d51d9635dfcacd38e23ffe59ef6171863ca52d69294af19672bd89dba49a6d325b1c64c4e76a5dc8c57bf3dfee1120e2bd4bec1ca9e21
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## [0.9.6] - 2023-11-04
|
2
|
+
|
3
|
+
- Fix and add links to caveats section in #130
|
4
|
+
- Refresh views across all schemas post swap in #134
|
5
|
+
|
6
|
+
## [0.9.5] - 2023-10-15
|
7
|
+
|
8
|
+
- Validate one constraint at a time in #124
|
9
|
+
- Introduce --skip-foreign-key-validation in #125
|
10
|
+
|
1
11
|
## [0.9.4] - 2023-09-17
|
2
12
|
|
3
13
|
- Resolving gem push and sync glitch in 0.9.3
|
@@ -13,6 +23,7 @@
|
|
13
23
|
|
14
24
|
- Dependency updates
|
15
25
|
- Create shadow and audit with auatovacuum default turned off. Should avoid lock queues when disabling vacuum on audit table. #97
|
26
|
+
|
16
27
|
## [0.9.1] - 2023-06-24
|
17
28
|
|
18
29
|
- Dependency updates and refresh docker release process with multi-platform build
|
data/Gemfile.lock
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pg_online_schema_change (0.9.
|
4
|
+
pg_online_schema_change (0.9.7)
|
5
5
|
ougai (~> 2.0.0)
|
6
6
|
pg (>= 1.3.2, < 1.6.0)
|
7
7
|
pg_query (>= 2.1.3, < 4.3.0)
|
8
|
-
thor (
|
8
|
+
thor (>= 1.2.1, < 1.4.0)
|
9
9
|
|
10
10
|
GEM
|
11
11
|
remote: https://rubygems.org/
|
12
12
|
specs:
|
13
13
|
ast (2.4.2)
|
14
|
-
|
14
|
+
bigdecimal (3.1.5)
|
15
15
|
coderay (1.1.3)
|
16
16
|
diff-lcs (1.5.0)
|
17
|
-
google-protobuf (3.
|
18
|
-
google-protobuf (3.
|
17
|
+
google-protobuf (3.25.2-arm64-darwin)
|
18
|
+
google-protobuf (3.25.2-x86_64-linux)
|
19
19
|
haml (6.1.1)
|
20
20
|
temple (>= 0.8.2)
|
21
21
|
thor
|
@@ -23,11 +23,12 @@ GEM
|
|
23
23
|
json (2.6.3)
|
24
24
|
language_server-protocol (3.17.0.3)
|
25
25
|
method_source (1.0.0)
|
26
|
-
oj (3.16.
|
26
|
+
oj (3.16.3)
|
27
|
+
bigdecimal (>= 3.0)
|
27
28
|
ougai (2.0.0)
|
28
29
|
oj (~> 3.10)
|
29
30
|
parallel (1.23.0)
|
30
|
-
parser (3.2.2.
|
31
|
+
parser (3.2.2.4)
|
31
32
|
ast (~> 2.4.1)
|
32
33
|
racc
|
33
34
|
pg (1.5.4)
|
@@ -39,9 +40,9 @@ GEM
|
|
39
40
|
method_source (~> 1.0)
|
40
41
|
racc (1.7.1)
|
41
42
|
rainbow (3.1.1)
|
42
|
-
rake (13.0
|
43
|
+
rake (13.1.0)
|
43
44
|
rbs (3.1.0)
|
44
|
-
regexp_parser (2.8.
|
45
|
+
regexp_parser (2.8.2)
|
45
46
|
rexml (3.2.6)
|
46
47
|
rspec (3.12.0)
|
47
48
|
rspec-core (~> 3.12.0)
|
@@ -56,19 +57,18 @@ GEM
|
|
56
57
|
diff-lcs (>= 1.2.0, < 2.0)
|
57
58
|
rspec-support (~> 3.12.0)
|
58
59
|
rspec-support (3.12.0)
|
59
|
-
rubocop (1.
|
60
|
-
base64 (~> 0.1.1)
|
60
|
+
rubocop (1.57.2)
|
61
61
|
json (~> 2.3)
|
62
62
|
language_server-protocol (>= 3.17.0)
|
63
63
|
parallel (~> 1.10)
|
64
|
-
parser (>= 3.2.2.
|
64
|
+
parser (>= 3.2.2.4)
|
65
65
|
rainbow (>= 2.2.2, < 4.0)
|
66
66
|
regexp_parser (>= 1.8, < 3.0)
|
67
67
|
rexml (>= 3.2.5, < 4.0)
|
68
68
|
rubocop-ast (>= 1.28.1, < 2.0)
|
69
69
|
ruby-progressbar (~> 1.7)
|
70
70
|
unicode-display_width (>= 2.4.0, < 3.0)
|
71
|
-
rubocop-ast (1.
|
71
|
+
rubocop-ast (1.30.0)
|
72
72
|
parser (>= 3.2.1.0)
|
73
73
|
rubocop-capybara (2.18.0)
|
74
74
|
rubocop (~> 1.41)
|
@@ -97,7 +97,7 @@ GEM
|
|
97
97
|
rbs
|
98
98
|
syntax_tree (>= 2.0.1)
|
99
99
|
temple (0.10.0)
|
100
|
-
thor (1.
|
100
|
+
thor (1.3.0)
|
101
101
|
tilt (2.1.0)
|
102
102
|
unicode-display_width (2.5.0)
|
103
103
|
|
data/README.md
CHANGED
@@ -9,10 +9,11 @@ pg-online-schema-change (`pg-osc`) is a tool for making schema changes (any `ALT
|
|
9
9
|
|
10
10
|
`pg-osc` uses the concept of shadow table to perform schema changes. At a high level, it creates a shadow table that looks structurally the same as the primary table, performs the schema change on the shadow table, copies contents from the primary table to the shadow table and swaps the table names in the end while preserving all changes to the primary table using triggers (via audit table).
|
11
11
|
|
12
|
-
`pg-osc` is inspired by the design and workings of tools like `pg_repack` and `pt-online-schema-change` (MySQL). Read more below on [how does it work](#how-does-it-work), [prominent features](#prominent-features), the [caveats](#
|
12
|
+
`pg-osc` is inspired by the design and workings of tools like `pg_repack` and `pt-online-schema-change` (MySQL). Read more below on [how does it work](#how-does-it-work), [prominent features](#prominent-features), the [caveats](#few-things-to-keep-in-mind) and [examples](#examples)
|
13
13
|
|
14
14
|
## Table of Contents
|
15
15
|
|
16
|
+
- [Table of Contents](#table-of-contents)
|
16
17
|
- [Installation](#installation)
|
17
18
|
- [Requirements](#requirements)
|
18
19
|
- [Usage](#usage)
|
@@ -22,15 +23,15 @@ pg-online-schema-change (`pg-osc`) is a tool for making schema changes (any `ALT
|
|
22
23
|
- [Renaming a column](#renaming-a-column)
|
23
24
|
- [Multiple ALTER statements](#multiple-alter-statements)
|
24
25
|
- [Kill other backends after 5s](#kill-other-backends-after-5s)
|
26
|
+
- [Replaying larger workloads](#replaying-larger-workloads)
|
25
27
|
- [Backfill data](#backfill-data)
|
26
28
|
- [Running using Docker](#running-using-docker)
|
27
|
-
- [
|
29
|
+
- [Few things to keep in mind](#few-things-to-keep-in-mind)
|
28
30
|
- [How does it work](#how-does-it-work)
|
29
31
|
- [Development](#development)
|
32
|
+
- [Local testing](#local-testing)
|
30
33
|
- [Releasing](#releasing)
|
31
34
|
- [Contributing](#contributing)
|
32
|
-
- [License](#license)
|
33
|
-
- [Code of Conduct](#code-of-conduct)
|
34
35
|
|
35
36
|
## Installation
|
36
37
|
|
@@ -103,12 +104,12 @@ print the version
|
|
103
104
|
## Prominent features
|
104
105
|
|
105
106
|
- `pg-osc` supports when a column is being added, dropped or renamed with no data loss.
|
106
|
-
- `pg-osc` acquires minimal locks throughout the process (read more below on the caveats).
|
107
|
+
- `pg-osc` acquires minimal locks throughout the process (read more below on the [caveats](#few-things-to-keep-in-mind)).
|
107
108
|
- Copies over indexes and Foreign keys.
|
108
109
|
- Optionally drop or retain old tables in the end.
|
110
|
+
- Reduce bloat (since pg-osc creates a new table and drops the old one post swap).
|
109
111
|
- Tune how slow or fast should replays be from the audit/log table ([Replaying larger workloads](#replaying-larger-workloads)).
|
110
112
|
- Backfill old/new columns as data is copied from primary table to shadow table, and then perform the swap. [Example](#backfill-data)
|
111
|
-
- **TBD**: Ability to reverse the change with no data loss. [tracking issue](https://github.com/shayonj/pg-osc/issues/14)
|
112
113
|
|
113
114
|
## Load test
|
114
115
|
|
@@ -216,7 +217,7 @@ docker run --network host -it --rm shayonj/pg-osc:latest \
|
|
216
217
|
--drop
|
217
218
|
```
|
218
219
|
|
219
|
-
##
|
220
|
+
## Few things to keep in mind
|
220
221
|
|
221
222
|
- Partitioned tables are not supported as of yet. Pull requests and ideas welcome.
|
222
223
|
- A primary key should exist on the table; without it, `pg-osc` will raise an exception
|
@@ -229,8 +230,6 @@ docker run --network host -it --rm shayonj/pg-osc:latest \
|
|
229
230
|
- Due to the nature of duplicating a table, there needs to be enough space on the disk to support the operation.
|
230
231
|
- Index, constraints and sequence names will be altered and lose their original naming.
|
231
232
|
- Can be fixed in future releases. Feel free to open a feature req.
|
232
|
-
- Triggers are not carried over.
|
233
|
-
- Can be fixed in future releases. Feel free to open a feature req.
|
234
233
|
- Foreign keys are dropped & re-added to referencing tables with a `NOT VALID`. A follow on `VALIDATE CONSTRAINT` is run.
|
235
234
|
- Ensures that integrity is maintained and re-introducing FKs doesn't acquire additional locks, hence the `NOT VALID`.
|
236
235
|
|
@@ -297,11 +296,3 @@ bundle exec bin/pg-online-schema-change perform -a 'ALTER TABLE pgbench_accounts
|
|
297
296
|
## Contributing
|
298
297
|
|
299
298
|
Bug reports and pull requests are welcome on GitHub at https://github.com/shayonj/pg-osc.
|
300
|
-
|
301
|
-
## License
|
302
|
-
|
303
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
304
|
-
|
305
|
-
## Code of Conduct
|
306
|
-
|
307
|
-
Everyone interacting in the PgOnlineSchemaChange project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/shayonj/pg-osc/blob/main/CODE_OF_CONDUCT.md).
|
@@ -5,6 +5,7 @@ require "securerandom"
|
|
5
5
|
module PgOnlineSchemaChange
|
6
6
|
class Orchestrate
|
7
7
|
SWAP_STATEMENT_TIMEOUT = "5s"
|
8
|
+
TRACK_PROGRESS_INTERVAL = 60 # seconds
|
8
9
|
|
9
10
|
extend Helper
|
10
11
|
|
@@ -58,6 +59,10 @@ module PgOnlineSchemaChange
|
|
58
59
|
|
59
60
|
raise Error, "Parent table has no primary key, exiting..." if primary_key.nil?
|
60
61
|
|
62
|
+
logger.info("Performing some house keeping....")
|
63
|
+
run_analyze!
|
64
|
+
run_vacuum!
|
65
|
+
|
61
66
|
setup_audit_table!
|
62
67
|
|
63
68
|
setup_trigger!
|
@@ -159,6 +164,12 @@ module PgOnlineSchemaChange
|
|
159
164
|
end
|
160
165
|
|
161
166
|
def setup_shadow_table!
|
167
|
+
logger.info("Setting up shadow table", { shadow_table: shadow_table })
|
168
|
+
Query.run(
|
169
|
+
client.connection,
|
170
|
+
"SELECT create_table_all('#{client.table_name}', '#{shadow_table}');",
|
171
|
+
)
|
172
|
+
|
162
173
|
# re-uses transaction with serializable
|
163
174
|
# This ensures that all queries from here till copy_data run with serializable.
|
164
175
|
# This is to to ensure that once the trigger is added to the primay table
|
@@ -168,13 +179,6 @@ module PgOnlineSchemaChange
|
|
168
179
|
# adding the trigger, till the copy ends, since they all happen in the
|
169
180
|
# same serializable transaction.
|
170
181
|
Query.run(client.connection, "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", true)
|
171
|
-
logger.info("Setting up shadow table", { shadow_table: shadow_table })
|
172
|
-
|
173
|
-
Query.run(
|
174
|
-
client.connection,
|
175
|
-
"SELECT create_table_all('#{client.table_name}', '#{shadow_table}');",
|
176
|
-
true,
|
177
|
-
)
|
178
182
|
|
179
183
|
# update serials
|
180
184
|
Query.run(
|
@@ -208,19 +212,44 @@ module PgOnlineSchemaChange
|
|
208
212
|
)
|
209
213
|
Query.run(client.connection, "DELETE FROM #{audit_table}", true)
|
210
214
|
|
211
|
-
logger.info(
|
212
|
-
"Copying contents..",
|
213
|
-
{ shadow_table: shadow_table, parent_table: client.table_name },
|
214
|
-
)
|
215
215
|
if client.copy_statement
|
216
216
|
query = format(client.copy_statement, shadow_table: shadow_table)
|
217
217
|
return Query.run(client.connection, query, true)
|
218
218
|
end
|
219
219
|
|
220
|
+
logger.info(
|
221
|
+
"Copying contents..",
|
222
|
+
{ shadow_table: shadow_table, parent_table: client.table_name },
|
223
|
+
)
|
224
|
+
|
225
|
+
@copy_finished = false
|
226
|
+
log_progress
|
227
|
+
|
220
228
|
sql = Query.copy_data_statement(client, shadow_table, true)
|
221
229
|
Query.run(client.connection, sql, true)
|
222
230
|
ensure
|
223
231
|
Query.run(client.connection, "COMMIT;") # commit the serializable transaction
|
232
|
+
@copy_finished = true
|
233
|
+
end
|
234
|
+
|
235
|
+
def log_progress
|
236
|
+
new_connection = client.checkout_connection
|
237
|
+
source_table_size = Query.get_table_size(new_connection, client.schema, client.table_name)
|
238
|
+
|
239
|
+
Thread.new do
|
240
|
+
loop do
|
241
|
+
break if @copy_finished
|
242
|
+
|
243
|
+
shadow_table_size = Query.get_table_size(new_connection, client.schema, shadow_table)
|
244
|
+
progress = (shadow_table_size.to_f / source_table_size) * 100
|
245
|
+
logger.info("Estimated copy progress: #{progress.round(2)}% complete")
|
246
|
+
|
247
|
+
break if @copy_finished || progress >= 100
|
248
|
+
sleep(TRACK_PROGRESS_INTERVAL) unless ENV["CI"]
|
249
|
+
rescue StandardError => e
|
250
|
+
logger.info("Reporting progress failed: #{e.message}")
|
251
|
+
end
|
252
|
+
end
|
224
253
|
end
|
225
254
|
|
226
255
|
def replay_and_swap!
|
@@ -272,7 +301,13 @@ module PgOnlineSchemaChange
|
|
272
301
|
def run_analyze!
|
273
302
|
logger.info("Performing ANALYZE!")
|
274
303
|
|
275
|
-
|
304
|
+
client.connection.async_exec("ANALYZE VERBOSE #{client.schema}.#{client.table_name};")
|
305
|
+
end
|
306
|
+
|
307
|
+
def run_vacuum!
|
308
|
+
logger.info("Performing VACUUM!")
|
309
|
+
|
310
|
+
client.connection.async_exec("VACUUM VERBOSE #{client.schema}.#{client.table_name};")
|
276
311
|
end
|
277
312
|
|
278
313
|
def validate_constraints!
|
@@ -311,20 +311,25 @@ module PgOnlineSchemaChange
|
|
311
311
|
|
312
312
|
def view_definitions_for(client, table)
|
313
313
|
query = <<~SQL
|
314
|
-
SELECT DISTINCT
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
JOIN
|
320
|
-
|
321
|
-
|
322
|
-
|
314
|
+
SELECT DISTINCT
|
315
|
+
dependent_view.relname AS view_name,
|
316
|
+
pg_get_viewdef(dependent_view.oid) AS view_definition,
|
317
|
+
view_ns.nspname AS schema_name
|
318
|
+
FROM pg_class AS source_table
|
319
|
+
JOIN pg_depend ON pg_depend.refobjid = source_table.oid
|
320
|
+
JOIN pg_rewrite ON pg_rewrite.oid = pg_depend.objid
|
321
|
+
JOIN pg_class AS dependent_view ON dependent_view.oid = pg_rewrite.ev_class
|
322
|
+
JOIN pg_namespace AS view_ns ON dependent_view.relnamespace = view_ns.oid
|
323
|
+
AND dependent_view.relkind = 'v'
|
324
|
+
AND source_table.relname = '#{table}';
|
323
325
|
SQL
|
324
326
|
|
325
327
|
definitions = []
|
326
328
|
run(client.connection, query) do |result|
|
327
|
-
definitions =
|
329
|
+
definitions =
|
330
|
+
result.map do |row|
|
331
|
+
{ "#{row["schema_name"]}.#{row["view_name"]}" => row["view_definition"].strip }
|
332
|
+
end
|
328
333
|
end
|
329
334
|
|
330
335
|
definitions
|
@@ -423,6 +428,15 @@ module PgOnlineSchemaChange
|
|
423
428
|
SELECT setval((select pg_get_serial_sequence('#{shadow_table}', '#{primary_key}')), (SELECT max(#{primary_key}) FROM #{table}));
|
424
429
|
SQL
|
425
430
|
end
|
431
|
+
|
432
|
+
def get_table_size(connection, schema, table_name)
|
433
|
+
size_query = "SELECT pg_table_size('#{schema}.#{table_name}');"
|
434
|
+
result = run(connection, size_query).first
|
435
|
+
result["pg_table_size"].to_i
|
436
|
+
rescue StandardError => e
|
437
|
+
logger.error("Error getting table size: #{e.message}")
|
438
|
+
0
|
439
|
+
end
|
426
440
|
end
|
427
441
|
end
|
428
442
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_online_schema_change
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shayon Mukherjee
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ougai
|
@@ -68,16 +68,22 @@ dependencies:
|
|
68
68
|
name: thor
|
69
69
|
requirement: !ruby/object:Gem::Requirement
|
70
70
|
requirements:
|
71
|
-
- - "
|
71
|
+
- - ">="
|
72
72
|
- !ruby/object:Gem::Version
|
73
73
|
version: 1.2.1
|
74
|
+
- - "<"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 1.4.0
|
74
77
|
type: :runtime
|
75
78
|
prerelease: false
|
76
79
|
version_requirements: !ruby/object:Gem::Requirement
|
77
80
|
requirements:
|
78
|
-
- - "
|
81
|
+
- - ">="
|
79
82
|
- !ruby/object:Gem::Version
|
80
83
|
version: 1.2.1
|
84
|
+
- - "<"
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 1.4.0
|
81
87
|
- !ruby/object:Gem::Dependency
|
82
88
|
name: prettier_print
|
83
89
|
requirement: !ruby/object:Gem::Requirement
|
@@ -305,7 +311,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
305
311
|
requirements:
|
306
312
|
- - ">="
|
307
313
|
- !ruby/object:Gem::Version
|
308
|
-
version:
|
314
|
+
version: 3.0.0
|
309
315
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
310
316
|
requirements:
|
311
317
|
- - ">="
|