pg_online_schema_change 0.1.0 → 0.4.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/.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
|