pg_online_schema_change 0.1.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +57 -24
- data/.rubocop_todo.yml +44 -0
- data/CHANGELOG.md +10 -1
- data/Dockerfile +5 -0
- data/Gemfile +0 -13
- data/Gemfile.lock +13 -13
- data/README.md +53 -12
- data/diagrams/how-it-works.excalidraw +500 -508
- data/diagrams/how-it-works.png +0 -0
- data/lib/pg_online_schema_change/cli.rb +3 -1
- data/lib/pg_online_schema_change/client.rb +12 -6
- data/lib/pg_online_schema_change/functions.rb +4 -2
- data/lib/pg_online_schema_change/helper.rb +10 -1
- data/lib/pg_online_schema_change/orchestrate.rb +55 -27
- data/lib/pg_online_schema_change/query.rb +22 -20
- data/lib/pg_online_schema_change/replay.rb +19 -10
- data/lib/pg_online_schema_change/store.rb +7 -3
- data/lib/pg_online_schema_change/version.rb +1 -1
- data/lib/pg_online_schema_change.rb +7 -11
- data/scripts/release.sh +28 -0
- metadata +178 -9
data/diagrams/how-it-works.png
CHANGED
Binary file
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "thor"
|
2
4
|
|
3
5
|
module PgOnlineSchemaChange
|
@@ -25,7 +27,7 @@ module PgOnlineSchemaChange
|
|
25
27
|
def perform
|
26
28
|
client_options = Struct.new(*options.keys.map(&:to_sym)).new(*options.values)
|
27
29
|
|
28
|
-
PgOnlineSchemaChange.logger
|
30
|
+
PgOnlineSchemaChange.logger(verbose: client_options.verbose)
|
29
31
|
PgOnlineSchemaChange::Orchestrate.run!(client_options)
|
30
32
|
end
|
31
33
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "pg"
|
2
4
|
|
3
5
|
module PgOnlineSchemaChange
|
@@ -16,7 +18,9 @@ module PgOnlineSchemaChange
|
|
16
18
|
@drop = options.drop
|
17
19
|
@kill_backends = options.kill_backends
|
18
20
|
@wait_time_for_lock = options.wait_time_for_lock
|
21
|
+
|
19
22
|
handle_copy_statement(options.copy_statement)
|
23
|
+
handle_validations
|
20
24
|
|
21
25
|
@connection = PG.connect(
|
22
26
|
dbname: @dbname,
|
@@ -26,17 +30,19 @@ module PgOnlineSchemaChange
|
|
26
30
|
port: @port,
|
27
31
|
)
|
28
32
|
|
29
|
-
raise Error, "Not a valid ALTER statement: #{@alter_statement}" unless Query.alter_statement?(@alter_statement)
|
30
|
-
|
31
|
-
unless Query.same_table?(@alter_statement)
|
32
|
-
raise Error "All statements should belong to the same table: #{@alter_statement}"
|
33
|
-
end
|
34
|
-
|
35
33
|
@table = Query.table(@alter_statement)
|
36
34
|
|
37
35
|
PgOnlineSchemaChange.logger.debug("Connection established")
|
38
36
|
end
|
39
37
|
|
38
|
+
def handle_validations
|
39
|
+
raise Error, "Not a valid ALTER statement: #{@alter_statement}" unless Query.alter_statement?(@alter_statement)
|
40
|
+
|
41
|
+
return if Query.same_table?(@alter_statement)
|
42
|
+
|
43
|
+
raise Error "All statements should belong to the same table: #{@alter_statement}"
|
44
|
+
end
|
45
|
+
|
40
46
|
def handle_copy_statement(statement)
|
41
47
|
return if statement.nil? || statement == ""
|
42
48
|
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
FUNC_FIX_SERIAL_SEQUENCE = <<~SQL
|
2
4
|
CREATE OR REPLACE FUNCTION fix_serial_sequence(_table regclass, _newtable text)
|
3
5
|
RETURNS void AS
|
4
6
|
$func$
|
@@ -35,7 +37,7 @@ FUNC_FIX_SERIAL_SEQUENCE = <<~SQL.freeze
|
|
35
37
|
$func$ LANGUAGE plpgsql VOLATILE;
|
36
38
|
SQL
|
37
39
|
|
38
|
-
FUNC_CREATE_TABLE_ALL = <<~SQL
|
40
|
+
FUNC_CREATE_TABLE_ALL = <<~SQL
|
39
41
|
CREATE OR REPLACE FUNCTION create_table_all(source_table text, newsource_table text)
|
40
42
|
RETURNS void language plpgsql
|
41
43
|
as $$
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module PgOnlineSchemaChange
|
2
4
|
module Helper
|
3
5
|
def primary_key
|
@@ -15,7 +17,14 @@ module PgOnlineSchemaChange
|
|
15
17
|
result = Store.send(:get, method)
|
16
18
|
return result if result
|
17
19
|
|
18
|
-
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond_to_missing?(method_name, *args)
|
24
|
+
result = Store.send(:get, method)
|
25
|
+
return true if result
|
26
|
+
|
27
|
+
super
|
19
28
|
end
|
20
29
|
end
|
21
30
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "securerandom"
|
2
4
|
|
3
5
|
module PgOnlineSchemaChange
|
4
6
|
class Orchestrate
|
5
|
-
SWAP_STATEMENT_TIMEOUT = "5s"
|
7
|
+
SWAP_STATEMENT_TIMEOUT = "5s"
|
6
8
|
|
7
9
|
extend Helper
|
8
10
|
|
@@ -21,12 +23,20 @@ module PgOnlineSchemaChange
|
|
21
23
|
Query.run(client.connection, FUNC_FIX_SERIAL_SEQUENCE)
|
22
24
|
Query.run(client.connection, FUNC_CREATE_TABLE_ALL)
|
23
25
|
|
26
|
+
setup_store
|
27
|
+
end
|
28
|
+
|
29
|
+
def setup_store
|
24
30
|
# Set this early on to ensure their creation and cleanup (unexpected)
|
25
31
|
# happens at all times. IOW, the calls from Store.get always return
|
26
32
|
# the same value.
|
27
33
|
Store.set(:old_primary_table, "pgosc_op_table_#{client.table}")
|
28
|
-
Store.set(:audit_table, "pgosc_at_#{client.table}_#{
|
29
|
-
Store.set(:
|
34
|
+
Store.set(:audit_table, "pgosc_at_#{client.table}_#{pgosc_identifier}")
|
35
|
+
Store.set(:operation_type_column, "operation_type_#{pgosc_identifier}")
|
36
|
+
Store.set(:trigger_time_column, "trigger_time_#{pgosc_identifier}")
|
37
|
+
Store.set(:audit_table_pk, "at_#{pgosc_identifier}_id")
|
38
|
+
Store.set(:audit_table_pk_sequence, "#{audit_table}_#{audit_table_pk}_seq")
|
39
|
+
Store.set(:shadow_table, "pgosc_st_#{client.table}_#{pgosc_identifier}")
|
30
40
|
end
|
31
41
|
|
32
42
|
def run!(options)
|
@@ -37,10 +47,10 @@ module PgOnlineSchemaChange
|
|
37
47
|
|
38
48
|
setup_audit_table!
|
39
49
|
setup_trigger!
|
40
|
-
setup_shadow_table!
|
41
|
-
disable_vacuum!
|
42
|
-
run_alter_statement!
|
43
|
-
copy_data!
|
50
|
+
setup_shadow_table! # re-uses transaction with serializable
|
51
|
+
disable_vacuum! # re-uses transaction with serializable
|
52
|
+
run_alter_statement! # re-uses transaction with serializable
|
53
|
+
copy_data! # re-uses transaction with serializable
|
44
54
|
run_analyze!
|
45
55
|
replay_and_swap!
|
46
56
|
run_analyze!
|
@@ -70,7 +80,7 @@ module PgOnlineSchemaChange
|
|
70
80
|
reader = setup_signals!
|
71
81
|
signal = reader.gets.chomp
|
72
82
|
|
73
|
-
while !reader.closed? && IO.select([reader])
|
83
|
+
while !reader.closed? && IO.select([reader]) # rubocop:disable Lint/UnreachableLoop
|
74
84
|
logger.info "Signal #{signal} received, cleaning up"
|
75
85
|
|
76
86
|
client.connection.cancel
|
@@ -85,7 +95,7 @@ module PgOnlineSchemaChange
|
|
85
95
|
logger.info("Setting up audit table", { audit_table: audit_table })
|
86
96
|
|
87
97
|
sql = <<~SQL
|
88
|
-
CREATE TABLE #{audit_table} (
|
98
|
+
CREATE TABLE #{audit_table} (#{audit_table_pk} SERIAL PRIMARY KEY, #{operation_type_column} text, #{trigger_time_column} timestamp, LIKE #{client.table});
|
89
99
|
SQL
|
90
100
|
|
91
101
|
Query.run(client.connection, sql)
|
@@ -109,13 +119,13 @@ module PgOnlineSchemaChange
|
|
109
119
|
$$
|
110
120
|
BEGIN
|
111
121
|
IF ( TG_OP = 'INSERT') THEN
|
112
|
-
INSERT INTO \"#{audit_table}\" select 'INSERT',
|
122
|
+
INSERT INTO \"#{audit_table}\" select nextval(\'#{audit_table_pk_sequence}\'), 'INSERT', clock_timestamp(), NEW.* ;
|
113
123
|
RETURN NEW;
|
114
124
|
ELSIF ( TG_OP = 'UPDATE') THEN
|
115
|
-
INSERT INTO \"#{audit_table}\" select 'UPDATE',
|
125
|
+
INSERT INTO \"#{audit_table}\" select nextval(\'#{audit_table_pk_sequence}\'), 'UPDATE', clock_timestamp(), NEW.* ;
|
116
126
|
RETURN NEW;
|
117
127
|
ELSIF ( TG_OP = 'DELETE') THEN
|
118
|
-
INSERT INTO \"#{audit_table}\" select 'DELETE',
|
128
|
+
INSERT INTO \"#{audit_table}\" select nextval(\'#{audit_table_pk_sequence}\'), 'DELETE', clock_timestamp(), OLD.* ;
|
119
129
|
RETURN NEW;
|
120
130
|
END IF;
|
121
131
|
END;
|
@@ -132,18 +142,28 @@ module PgOnlineSchemaChange
|
|
132
142
|
end
|
133
143
|
|
134
144
|
def setup_shadow_table!
|
145
|
+
# re-uses transaction with serializable
|
146
|
+
# This ensures that all queries from here till copy_data run with serializable.
|
147
|
+
# This is to to ensure that once the trigger is added to the primay table
|
148
|
+
# and contents being copied into the shadow, after a delete all on audit table,
|
149
|
+
# any replaying of rows that happen next from audit table do not contain
|
150
|
+
# any duplicates. We are ensuring there are no race conditions between
|
151
|
+
# adding the trigger, till the copy ends, since they all happen in the
|
152
|
+
# same serializable transaction.
|
153
|
+
Query.run(client.connection, "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", true)
|
135
154
|
logger.info("Setting up shadow table", { shadow_table: shadow_table })
|
136
155
|
|
137
|
-
Query.run(client.connection, "SELECT create_table_all('#{client.table}', '#{shadow_table}');")
|
156
|
+
Query.run(client.connection, "SELECT create_table_all('#{client.table}', '#{shadow_table}');", true)
|
138
157
|
|
139
158
|
# update serials
|
140
|
-
Query.run(client.connection, "SELECT fix_serial_sequence('#{client.table}', '#{shadow_table}');")
|
159
|
+
Query.run(client.connection, "SELECT fix_serial_sequence('#{client.table}', '#{shadow_table}');", true)
|
141
160
|
end
|
142
161
|
|
143
|
-
# Disabling vacuum to avoid any issues during the process
|
144
162
|
def disable_vacuum!
|
145
|
-
|
146
|
-
|
163
|
+
# re-uses transaction with serializable
|
164
|
+
# Disabling vacuum to avoid any issues during the process
|
165
|
+
result = Query.storage_parameters_for(client, client.table, true) || ""
|
166
|
+
Store.set(:primary_table_storage_parameters, result)
|
147
167
|
|
148
168
|
logger.debug("Disabling vacuum on shadow and audit table",
|
149
169
|
{ shadow_table: shadow_table, audit_table: audit_table })
|
@@ -156,32 +176,38 @@ module PgOnlineSchemaChange
|
|
156
176
|
autovacuum_enabled = false, toast.autovacuum_enabled = false
|
157
177
|
);
|
158
178
|
SQL
|
159
|
-
Query.run(client.connection, sql)
|
179
|
+
Query.run(client.connection, sql, true)
|
160
180
|
end
|
161
181
|
|
162
182
|
def run_alter_statement!
|
183
|
+
# re-uses transaction with serializable
|
163
184
|
statement = Query.alter_statement_for(client, shadow_table)
|
164
185
|
logger.info("Running alter statement on shadow table",
|
165
186
|
{ shadow_table: shadow_table, parent_table: client.table })
|
166
|
-
Query.run(client.connection, statement)
|
187
|
+
Query.run(client.connection, statement, true)
|
167
188
|
|
168
189
|
Store.set(:dropped_columns_list, Query.dropped_columns(client))
|
169
190
|
Store.set(:renamed_columns_list, Query.renamed_columns(client))
|
170
191
|
end
|
171
192
|
|
172
|
-
# Begin the process to copy data into copy table
|
173
|
-
# depending on the size of the table, this can be a time
|
174
|
-
# taking operation.
|
175
193
|
def copy_data!
|
176
|
-
|
194
|
+
# re-uses transaction with serializable
|
195
|
+
# Begin the process to copy data into copy table
|
196
|
+
# depending on the size of the table, this can be a time
|
197
|
+
# taking operation.
|
198
|
+
logger.info("Clearing contents of audit table before copy..", { shadow_table: shadow_table, parent_table: client.table })
|
199
|
+
Query.run(client.connection, "DELETE FROM #{audit_table}", true)
|
177
200
|
|
201
|
+
logger.info("Copying contents..", { shadow_table: shadow_table, parent_table: client.table })
|
178
202
|
if client.copy_statement
|
179
203
|
query = format(client.copy_statement, shadow_table: shadow_table)
|
180
|
-
return Query.run(client.connection, query)
|
204
|
+
return Query.run(client.connection, query, true)
|
181
205
|
end
|
182
206
|
|
183
207
|
sql = Query.copy_data_statement(client, shadow_table)
|
184
|
-
Query.run(client.connection, sql)
|
208
|
+
Query.run(client.connection, sql, true)
|
209
|
+
ensure
|
210
|
+
Query.run(client.connection, "COMMIT;") # commit the serializable transaction
|
185
211
|
end
|
186
212
|
|
187
213
|
def replay_and_swap!
|
@@ -255,8 +281,10 @@ module PgOnlineSchemaChange
|
|
255
281
|
Query.run(client.connection, sql)
|
256
282
|
end
|
257
283
|
|
258
|
-
private
|
259
|
-
|
284
|
+
private
|
285
|
+
|
286
|
+
def pgosc_identifier
|
287
|
+
@pgosc_identifier ||= SecureRandom.hex(3)
|
260
288
|
end
|
261
289
|
end
|
262
290
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "pg_query"
|
2
4
|
require "pg"
|
3
5
|
|
@@ -5,7 +7,7 @@ module PgOnlineSchemaChange
|
|
5
7
|
class Query
|
6
8
|
extend Helper
|
7
9
|
|
8
|
-
INDEX_SUFFIX = "_pgosc"
|
10
|
+
INDEX_SUFFIX = "_pgosc"
|
9
11
|
DROPPED_COLUMN_TYPE = :AT_DropColumn
|
10
12
|
RENAMED_COLUMN_TYPE = :AT_RenameColumn
|
11
13
|
LOCK_ATTEMPT = 4
|
@@ -15,28 +17,28 @@ module PgOnlineSchemaChange
|
|
15
17
|
PgQuery.parse(query).tree.stmts.all? do |statement|
|
16
18
|
statement.stmt.alter_table_stmt.instance_of?(PgQuery::AlterTableStmt) || statement.stmt.rename_stmt.instance_of?(PgQuery::RenameStmt)
|
17
19
|
end
|
18
|
-
rescue PgQuery::ParseError
|
20
|
+
rescue PgQuery::ParseError
|
19
21
|
false
|
20
22
|
end
|
21
23
|
|
22
24
|
def same_table?(query)
|
23
|
-
tables = PgQuery.parse(query).tree.stmts.
|
25
|
+
tables = PgQuery.parse(query).tree.stmts.filter_map do |statement|
|
24
26
|
if statement.stmt.alter_table_stmt.instance_of?(PgQuery::AlterTableStmt)
|
25
27
|
statement.stmt.alter_table_stmt.relation.relname
|
26
28
|
elsif statement.stmt.rename_stmt.instance_of?(PgQuery::RenameStmt)
|
27
29
|
statement.stmt.rename_stmt.relation.relname
|
28
30
|
end
|
29
|
-
end
|
31
|
+
end
|
30
32
|
|
31
33
|
tables.uniq.count == 1
|
32
|
-
rescue PgQuery::ParseError
|
34
|
+
rescue PgQuery::ParseError
|
33
35
|
false
|
34
36
|
end
|
35
37
|
|
36
38
|
def table(query)
|
37
|
-
from_rename_statement = PgQuery.parse(query).tree.stmts.
|
39
|
+
from_rename_statement = PgQuery.parse(query).tree.stmts.filter_map do |statement|
|
38
40
|
statement.stmt.rename_stmt&.relation&.relname
|
39
|
-
end
|
41
|
+
end[0]
|
40
42
|
PgQuery.parse(query).tables[0] || from_rename_statement
|
41
43
|
end
|
42
44
|
|
@@ -48,7 +50,7 @@ module PgOnlineSchemaChange
|
|
48
50
|
connection.async_exec("BEGIN;")
|
49
51
|
|
50
52
|
result = connection.async_exec(query, &block)
|
51
|
-
rescue Exception
|
53
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
52
54
|
connection.cancel if connection.transaction_status != PG::PQTRANS_IDLE
|
53
55
|
connection.block
|
54
56
|
logger.info("Exception raised, rolling back query", { rollback: true, query: query })
|
@@ -60,7 +62,7 @@ module PgOnlineSchemaChange
|
|
60
62
|
result
|
61
63
|
end
|
62
64
|
|
63
|
-
def table_columns(client, table = nil)
|
65
|
+
def table_columns(client, table = nil, reuse_trasaction = false)
|
64
66
|
sql = <<~SQL
|
65
67
|
SELECT attname as column_name, format_type(atttypid, atttypmod) as type, attnum as column_position FROM pg_attribute
|
66
68
|
WHERE attrelid = \'#{table || client.table}\'::regclass AND attnum > 0 AND NOT attisdropped
|
@@ -68,7 +70,7 @@ module PgOnlineSchemaChange
|
|
68
70
|
SQL
|
69
71
|
mapped_columns = []
|
70
72
|
|
71
|
-
run(client.connection, sql) do |result|
|
73
|
+
run(client.connection, sql, reuse_trasaction) do |result|
|
72
74
|
mapped_columns = result.map do |row|
|
73
75
|
row["column_name_regular"] = row["column_name"]
|
74
76
|
row["column_name"] = client.connection.quote_ident(row["column_name"])
|
@@ -144,11 +146,11 @@ module PgOnlineSchemaChange
|
|
144
146
|
end
|
145
147
|
|
146
148
|
references.map do |row|
|
147
|
-
if row["definition"].end_with?("NOT VALID")
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
149
|
+
add_statement = if row["definition"].end_with?("NOT VALID")
|
150
|
+
"ALTER TABLE #{row["table_on"]} ADD CONSTRAINT #{row["constraint_name"]} #{row["definition"]};"
|
151
|
+
else
|
152
|
+
"ALTER TABLE #{row["table_on"]} ADD CONSTRAINT #{row["constraint_name"]} #{row["definition"]} NOT VALID;"
|
153
|
+
end
|
152
154
|
|
153
155
|
drop_statement = "ALTER TABLE #{row["table_on"]} DROP CONSTRAINT #{row["constraint_name"]};"
|
154
156
|
|
@@ -210,13 +212,13 @@ module PgOnlineSchemaChange
|
|
210
212
|
columns.first
|
211
213
|
end
|
212
214
|
|
213
|
-
def storage_parameters_for(client, table)
|
215
|
+
def storage_parameters_for(client, table, reuse_trasaction = false)
|
214
216
|
query = <<~SQL
|
215
217
|
SELECT array_to_string(reloptions, ',') as params FROM pg_class WHERE relname=\'#{table}\';
|
216
218
|
SQL
|
217
219
|
|
218
220
|
columns = []
|
219
|
-
run(client.connection, query) do |result|
|
221
|
+
run(client.connection, query, reuse_trasaction) do |result|
|
220
222
|
columns = result.map { |row| row["params"] }
|
221
223
|
end
|
222
224
|
|
@@ -266,8 +268,8 @@ module PgOnlineSchemaChange
|
|
266
268
|
run(client.connection, query, true)
|
267
269
|
end
|
268
270
|
|
269
|
-
def copy_data_statement(client, shadow_table)
|
270
|
-
select_columns = table_columns(client).map do |entry|
|
271
|
+
def copy_data_statement(client, shadow_table, reuse_trasaction = false)
|
272
|
+
select_columns = table_columns(client, client.table, reuse_trasaction).map do |entry|
|
271
273
|
entry["column_name_regular"]
|
272
274
|
end
|
273
275
|
|
@@ -291,7 +293,7 @@ module PgOnlineSchemaChange
|
|
291
293
|
client.connection.quote_ident(select_column)
|
292
294
|
end
|
293
295
|
|
294
|
-
|
296
|
+
<<~SQL
|
295
297
|
INSERT INTO #{shadow_table}(#{insert_into_columns.join(", ")})
|
296
298
|
SELECT #{select_columns.join(", ")}
|
297
299
|
FROM ONLY #{client.table}
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
4
|
+
|
1
5
|
module PgOnlineSchemaChange
|
2
6
|
class Replay
|
3
7
|
extend Helper
|
@@ -5,7 +9,6 @@ module PgOnlineSchemaChange
|
|
5
9
|
class << self
|
6
10
|
PULL_BATCH_COUNT = 1000
|
7
11
|
DELTA_COUNT = 20
|
8
|
-
RESERVED_COLUMNS = %w[operation_type trigger_time].freeze
|
9
12
|
|
10
13
|
# This, picks PULL_BATCH_COUNT rows by primary key from audit_table,
|
11
14
|
# replays it on the shadow_table. Once the batch is done,
|
@@ -25,7 +28,7 @@ module PgOnlineSchemaChange
|
|
25
28
|
|
26
29
|
def rows_to_play(reuse_trasaction = false)
|
27
30
|
select_query = <<~SQL
|
28
|
-
SELECT * FROM #{audit_table} ORDER BY #{
|
31
|
+
SELECT * FROM #{audit_table} ORDER BY #{audit_table_pk} LIMIT #{PULL_BATCH_COUNT};
|
29
32
|
SQL
|
30
33
|
|
31
34
|
rows = []
|
@@ -34,6 +37,10 @@ module PgOnlineSchemaChange
|
|
34
37
|
rows
|
35
38
|
end
|
36
39
|
|
40
|
+
def reserved_columns
|
41
|
+
@reserved_columns ||= [trigger_time_column, operation_type_column, audit_table_pk]
|
42
|
+
end
|
43
|
+
|
37
44
|
def play!(rows, reuse_trasaction = false)
|
38
45
|
logger.info("Replaying rows, count: #{rows.size}")
|
39
46
|
|
@@ -44,7 +51,7 @@ module PgOnlineSchemaChange
|
|
44
51
|
|
45
52
|
# Remove audit table cols, since we will be
|
46
53
|
# re-mapping them for inserts and updates
|
47
|
-
|
54
|
+
reserved_columns.each do |col|
|
48
55
|
new_row.delete(col)
|
49
56
|
end
|
50
57
|
|
@@ -73,7 +80,7 @@ module PgOnlineSchemaChange
|
|
73
80
|
client.connection.escape_string(value)
|
74
81
|
end
|
75
82
|
|
76
|
-
case row[
|
83
|
+
case row[operation_type_column]
|
77
84
|
when "INSERT"
|
78
85
|
values = new_row.map { |_, val| "'#{val}'" }.join(",")
|
79
86
|
|
@@ -110,13 +117,15 @@ module PgOnlineSchemaChange
|
|
110
117
|
Query.run(client.connection, to_be_replayed.join, reuse_trasaction)
|
111
118
|
|
112
119
|
# Delete items from the audit now that are replayed
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
120
|
+
return unless to_be_deleted_rows.count >= 1
|
121
|
+
|
122
|
+
delete_query = <<~SQL
|
123
|
+
DELETE FROM #{audit_table} WHERE #{primary_key} IN (#{to_be_deleted_rows.join(",")})
|
124
|
+
SQL
|
125
|
+
Query.run(client.connection, delete_query, reuse_trasaction)
|
119
126
|
end
|
120
127
|
end
|
121
128
|
end
|
122
129
|
end
|
130
|
+
|
131
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
@@ -1,17 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "pg_query"
|
2
4
|
require "pg"
|
3
5
|
|
4
6
|
module PgOnlineSchemaChange
|
5
7
|
class Store
|
6
8
|
class << self
|
7
|
-
|
9
|
+
@object = {}
|
8
10
|
|
9
11
|
def get(key)
|
10
|
-
|
12
|
+
@object ||= {}
|
13
|
+
@object[key.to_s] || @object[key.to_sym]
|
11
14
|
end
|
12
15
|
|
13
16
|
def set(key, value)
|
14
|
-
|
17
|
+
@object ||= {}
|
18
|
+
@object[key.to_sym] = value
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
@@ -18,16 +18,12 @@ module PgOnlineSchemaChange
|
|
18
18
|
class CountBelowDelta < StandardError; end
|
19
19
|
class AccessExclusiveLockNotAcquired < StandardError; end
|
20
20
|
|
21
|
-
def self.logger
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.logger
|
31
|
-
@@logger
|
21
|
+
def self.logger(verbose: false)
|
22
|
+
@logger ||= begin
|
23
|
+
logger = Ougai::Logger.new($stdout)
|
24
|
+
logger.level = verbose ? Ougai::Logger::TRACE : Ougai::Logger::INFO
|
25
|
+
logger.with_fields = { version: PgOnlineSchemaChange::VERSION }
|
26
|
+
logger
|
27
|
+
end
|
32
28
|
end
|
33
29
|
end
|
data/scripts/release.sh
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
export VERSION=$1
|
2
|
+
echo "VERSION: ${VERSION}"
|
3
|
+
|
4
|
+
echo "=== Pushing tags to github ===="
|
5
|
+
git tag v$VERSION
|
6
|
+
git push origin --tags
|
7
|
+
|
8
|
+
echo "=== Building Gem ===="
|
9
|
+
gem build pg_online_schema_change.gemspec
|
10
|
+
|
11
|
+
echo "=== Pushing gem ===="
|
12
|
+
gem push pg_online_schema_change-$VERSION.gem
|
13
|
+
|
14
|
+
echo "=== Sleeping for 5s ===="
|
15
|
+
sleep 5
|
16
|
+
|
17
|
+
echo "=== Building Image ===="
|
18
|
+
docker build . --build-arg VERSION=$VERSION -t shayonj/pg-osc:$VERSION
|
19
|
+
|
20
|
+
echo "=== Tagging Image ===="
|
21
|
+
docker image tag shayonj/pg-osc:$VERSION shayonj/pg-osc:latest
|
22
|
+
|
23
|
+
echo "=== Pushing Image ===="
|
24
|
+
docker push shayonj/pg-osc:$VERSION
|
25
|
+
docker push shayonj/pg-osc:latest
|
26
|
+
|
27
|
+
echo "=== Cleaning up ===="
|
28
|
+
rm pg_online_schema_change-$VERSION.gem
|