pg_easy_replicate 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/pg_easy_replicate/cli.rb +10 -0
- data/lib/pg_easy_replicate/helper.rb +27 -0
- data/lib/pg_easy_replicate/index_manager.rb +3 -3
- data/lib/pg_easy_replicate/orchestrate.rb +28 -57
- data/lib/pg_easy_replicate/version.rb +1 -1
- data/lib/pg_easy_replicate.rb +79 -3
- 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: efcbf369a4410abd783ea1dba49ead97fa471705842df4a35d28864349b1c70c
|
4
|
+
data.tar.gz: 676d97aa0c551dd0f9d58c3bccb229951bcb2dfc34653995375c359718bc1d7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 774ef530f4be4b409fdf2d4956a44deb09aebf59c8ef480074d426ca4c72006673c7a6915d860e61846fb00df8e14fa9534e920f5802873ed727e37564197b25
|
7
|
+
data.tar.gz: 50055941c3a83351b22ceaeb539b807a7cec5e3130732944c63e8a5440a7707a273a546681b1d581d4f79d9fba616998fc48cffc009e401bfc570e76e27064f0
|
data/Gemfile.lock
CHANGED
@@ -16,10 +16,20 @@ module PgEasyReplicate
|
|
16
16
|
aliases: "-c",
|
17
17
|
boolean: true,
|
18
18
|
desc: "Copy schema to the new database"
|
19
|
+
method_option :tables,
|
20
|
+
aliases: "-t",
|
21
|
+
desc:
|
22
|
+
"Comma separated list of table names. Default: All tables"
|
23
|
+
method_option :schema_name,
|
24
|
+
aliases: "-s",
|
25
|
+
desc:
|
26
|
+
"Name of the schema tables are in, only required if passing list of tables"
|
19
27
|
def config_check
|
20
28
|
PgEasyReplicate.assert_config(
|
21
29
|
special_user_role: options[:special_user_role],
|
22
30
|
copy_schema: options[:copy_schema],
|
31
|
+
tables: options[:tables],
|
32
|
+
schema_name: options[:schema_name],
|
23
33
|
)
|
24
34
|
|
25
35
|
puts "✅ Config is looking good."
|
@@ -72,5 +72,32 @@ module PgEasyReplicate
|
|
72
72
|
raise(msg) if test_env?
|
73
73
|
abort(msg)
|
74
74
|
end
|
75
|
+
|
76
|
+
def determine_tables(conn_string:, list: "", schema: nil)
|
77
|
+
schema ||= "public"
|
78
|
+
|
79
|
+
tables = list&.split(",") || []
|
80
|
+
if tables.size > 0
|
81
|
+
tables
|
82
|
+
else
|
83
|
+
list_all_tables(schema: schema, conn_string: conn_string)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def list_all_tables(schema:, conn_string:)
|
88
|
+
Query
|
89
|
+
.run(
|
90
|
+
query:
|
91
|
+
"SELECT table_name
|
92
|
+
FROM information_schema.tables
|
93
|
+
WHERE table_schema = '#{schema}' AND
|
94
|
+
table_type = 'BASE TABLE'
|
95
|
+
ORDER BY table_name",
|
96
|
+
connection_url: conn_string,
|
97
|
+
user: db_user(conn_string),
|
98
|
+
)
|
99
|
+
.map(&:values)
|
100
|
+
.flatten
|
101
|
+
end
|
75
102
|
end
|
76
103
|
end
|
@@ -17,7 +17,7 @@ module PgEasyReplicate
|
|
17
17
|
tables: tables,
|
18
18
|
schema: schema,
|
19
19
|
).each do |index|
|
20
|
-
drop_sql = "DROP INDEX CONCURRENTLY #{index[:index_name]};"
|
20
|
+
drop_sql = "DROP INDEX CONCURRENTLY #{schema}.#{index[:index_name]};"
|
21
21
|
|
22
22
|
Query.run(
|
23
23
|
query: drop_sql,
|
@@ -56,8 +56,8 @@ module PgEasyReplicate
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def self.fetch_indices(conn_string:, tables:, schema:)
|
59
|
-
return [] if tables.
|
60
|
-
table_list = tables.
|
59
|
+
return [] if tables.empty?
|
60
|
+
table_list = tables.map { |table| "'#{table}'" }.join(",")
|
61
61
|
|
62
62
|
sql = <<-SQL
|
63
63
|
SELECT
|
@@ -46,7 +46,7 @@ module PgEasyReplicate
|
|
46
46
|
|
47
47
|
Group.create(
|
48
48
|
name: options[:group_name],
|
49
|
-
table_names: tables,
|
49
|
+
table_names: tables.join(","),
|
50
50
|
schema_name: schema_name,
|
51
51
|
started_at: Time.now.utc,
|
52
52
|
recreate_indices_post_copy: options[:recreate_indices_post_copy],
|
@@ -63,7 +63,7 @@ module PgEasyReplicate
|
|
63
63
|
else
|
64
64
|
Group.create(
|
65
65
|
name: options[:group_name],
|
66
|
-
table_names: tables,
|
66
|
+
table_names: tables.join(","),
|
67
67
|
schema_name: schema_name,
|
68
68
|
started_at: Time.now.utc,
|
69
69
|
failed_at: Time.now.utc,
|
@@ -92,42 +92,24 @@ module PgEasyReplicate
|
|
92
92
|
schema:,
|
93
93
|
group_name:,
|
94
94
|
conn_string:,
|
95
|
-
tables:
|
95
|
+
tables: []
|
96
96
|
)
|
97
97
|
logger.info(
|
98
98
|
"Adding tables up publication",
|
99
99
|
{ publication_name: publication_name(group_name) },
|
100
100
|
)
|
101
101
|
|
102
|
-
tables
|
103
|
-
.
|
104
|
-
.map do |table_name|
|
105
|
-
Query.run(
|
106
|
-
query:
|
107
|
-
"ALTER PUBLICATION #{quote_ident(publication_name(group_name))}
|
108
|
-
ADD TABLE #{quote_ident(table_name)}",
|
109
|
-
connection_url: conn_string,
|
110
|
-
schema: schema,
|
111
|
-
)
|
112
|
-
end
|
113
|
-
rescue => e
|
114
|
-
raise "Unable to add tables to publication: #{e.message}"
|
115
|
-
end
|
116
|
-
|
117
|
-
def list_all_tables(schema:, conn_string:)
|
118
|
-
Query
|
119
|
-
.run(
|
102
|
+
tables.map do |table_name|
|
103
|
+
Query.run(
|
120
104
|
query:
|
121
|
-
"
|
122
|
-
|
123
|
-
WHERE table_schema = '#{schema}' AND
|
124
|
-
table_type = 'BASE TABLE'
|
125
|
-
ORDER BY table_name",
|
105
|
+
"ALTER PUBLICATION #{quote_ident(publication_name(group_name))}
|
106
|
+
ADD TABLE #{quote_ident(table_name)}",
|
126
107
|
connection_url: conn_string,
|
108
|
+
schema: schema,
|
127
109
|
)
|
128
|
-
|
129
|
-
|
130
|
-
|
110
|
+
end
|
111
|
+
rescue => e
|
112
|
+
raise "Unable to add tables to publication: #{e.message}"
|
131
113
|
end
|
132
114
|
|
133
115
|
def drop_publication(group_name:, conn_string:)
|
@@ -225,10 +207,11 @@ module PgEasyReplicate
|
|
225
207
|
lag_delta_size: nil
|
226
208
|
)
|
227
209
|
group = Group.find(group_name)
|
210
|
+
tables_list = group[:table_names].split(",")
|
228
211
|
|
229
212
|
run_vacuum_analyze(
|
230
213
|
conn_string: target_conn_string,
|
231
|
-
tables:
|
214
|
+
tables: tables_list,
|
232
215
|
schema: group[:schema_name],
|
233
216
|
)
|
234
217
|
|
@@ -239,7 +222,7 @@ module PgEasyReplicate
|
|
239
222
|
IndexManager.recreate_indices(
|
240
223
|
source_conn_string: source_db_url,
|
241
224
|
target_conn_string: target_db_url,
|
242
|
-
tables:
|
225
|
+
tables: tables_list,
|
243
226
|
schema: group[:schema_name],
|
244
227
|
)
|
245
228
|
end
|
@@ -257,7 +240,7 @@ module PgEasyReplicate
|
|
257
240
|
# Run vacuum analyze to refresh the planner post switchover
|
258
241
|
run_vacuum_analyze(
|
259
242
|
conn_string: target_conn_string,
|
260
|
-
tables:
|
243
|
+
tables: tables_list,
|
261
244
|
schema: group[:schema_name],
|
262
245
|
)
|
263
246
|
drop_subscription(
|
@@ -369,21 +352,19 @@ module PgEasyReplicate
|
|
369
352
|
end
|
370
353
|
|
371
354
|
def run_vacuum_analyze(conn_string:, tables:, schema:)
|
372
|
-
tables
|
373
|
-
.
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
)
|
386
|
-
end
|
355
|
+
tables.each do |t|
|
356
|
+
logger.info(
|
357
|
+
"Running vacuum analyze on #{t}",
|
358
|
+
schema: schema,
|
359
|
+
table: t,
|
360
|
+
)
|
361
|
+
Query.run(
|
362
|
+
query: "VACUUM VERBOSE ANALYZE #{t}",
|
363
|
+
connection_url: conn_string,
|
364
|
+
schema: schema,
|
365
|
+
transaction: false,
|
366
|
+
)
|
367
|
+
end
|
387
368
|
rescue => e
|
388
369
|
raise "Unable to run vacuum and analyze: #{e.message}"
|
389
370
|
end
|
@@ -391,16 +372,6 @@ module PgEasyReplicate
|
|
391
372
|
def mark_switchover_complete(group_name)
|
392
373
|
Group.update(group_name: group_name, switchover_completed_at: Time.now)
|
393
374
|
end
|
394
|
-
|
395
|
-
private
|
396
|
-
|
397
|
-
def determine_tables(schema:, conn_string:, list: "")
|
398
|
-
tables = list&.split(",") || []
|
399
|
-
unless tables.size > 0
|
400
|
-
return list_all_tables(schema: schema, conn_string: conn_string)
|
401
|
-
end
|
402
|
-
""
|
403
|
-
end
|
404
375
|
end
|
405
376
|
end
|
406
377
|
end
|
data/lib/pg_easy_replicate.rb
CHANGED
@@ -26,7 +26,12 @@ module PgEasyReplicate
|
|
26
26
|
extend Helper
|
27
27
|
|
28
28
|
class << self
|
29
|
-
def config(
|
29
|
+
def config(
|
30
|
+
special_user_role: nil,
|
31
|
+
copy_schema: false,
|
32
|
+
tables: "",
|
33
|
+
schema_name: nil
|
34
|
+
)
|
30
35
|
abort_with("SOURCE_DB_URL is missing") if source_db_url.nil?
|
31
36
|
abort_with("TARGET_DB_URL is missing") if target_db_url.nil?
|
32
37
|
|
@@ -56,15 +61,31 @@ module PgEasyReplicate
|
|
56
61
|
user: db_user(target_db_url),
|
57
62
|
),
|
58
63
|
pg_dump_exists: pg_dump_exists,
|
64
|
+
tables_have_replica_identity:
|
65
|
+
tables_have_replica_identity?(
|
66
|
+
conn_string: source_db_url,
|
67
|
+
tables: tables,
|
68
|
+
schema_name: schema_name,
|
69
|
+
),
|
59
70
|
}
|
60
71
|
rescue => e
|
61
72
|
abort_with("Unable to check config: #{e.message}")
|
62
73
|
end
|
63
74
|
end
|
64
75
|
|
65
|
-
def assert_config(
|
76
|
+
def assert_config(
|
77
|
+
special_user_role: nil,
|
78
|
+
copy_schema: false,
|
79
|
+
tables: "",
|
80
|
+
schema_name: nil
|
81
|
+
)
|
66
82
|
config_hash =
|
67
|
-
config(
|
83
|
+
config(
|
84
|
+
special_user_role: special_user_role,
|
85
|
+
copy_schema: copy_schema,
|
86
|
+
tables: tables,
|
87
|
+
schema_name: schema_name,
|
88
|
+
)
|
68
89
|
|
69
90
|
if copy_schema && !config_hash.dig(:pg_dump_exists)
|
70
91
|
abort_with("pg_dump must exist if copy_schema (-c) is passed")
|
@@ -82,6 +103,19 @@ module PgEasyReplicate
|
|
82
103
|
abort_with("User on source database does not have super user privilege")
|
83
104
|
end
|
84
105
|
|
106
|
+
if tables.split(",").size > 0 && (schema_name.nil? || schema_name == "")
|
107
|
+
abort_with("Schema name is required if tables are passed")
|
108
|
+
end
|
109
|
+
|
110
|
+
unless config_hash.dig(:tables_have_replica_identity)
|
111
|
+
abort_with(
|
112
|
+
"Ensure all tables involved in logical replication have an appropriate replica identity set. This can be done using:
|
113
|
+
1. Default (Primary Key): `ALTER TABLE table_name REPLICA IDENTITY DEFAULT;`
|
114
|
+
2. Unique Index: `ALTER TABLE table_name REPLICA IDENTITY USING INDEX index_name;`
|
115
|
+
3. Full (All Columns): `ALTER TABLE table_name REPLICA IDENTITY FULL;`",
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
85
119
|
return if config_hash.dig(:target_db_is_super_user)
|
86
120
|
abort_with("User on target database does not have super user privilege")
|
87
121
|
end
|
@@ -352,5 +386,47 @@ module PgEasyReplicate
|
|
352
386
|
)
|
353
387
|
.any? { |q| q[:username] == user }
|
354
388
|
end
|
389
|
+
|
390
|
+
def tables_have_replica_identity?(
|
391
|
+
conn_string:,
|
392
|
+
tables: "",
|
393
|
+
schema_name: nil
|
394
|
+
)
|
395
|
+
schema_name ||= "public"
|
396
|
+
|
397
|
+
table_list =
|
398
|
+
determine_tables(
|
399
|
+
schema: schema_name,
|
400
|
+
conn_string: source_db_url,
|
401
|
+
list: tables,
|
402
|
+
)
|
403
|
+
return false if table_list.empty?
|
404
|
+
|
405
|
+
formatted_table_list = table_list.map { |table| "'#{table}'" }.join(", ")
|
406
|
+
|
407
|
+
sql = <<~SQL
|
408
|
+
SELECT t.relname AS table_name,
|
409
|
+
CASE
|
410
|
+
WHEN t.relreplident = 'd' THEN 'default'
|
411
|
+
WHEN t.relreplident = 'n' THEN 'nothing'
|
412
|
+
WHEN t.relreplident = 'i' THEN 'index'
|
413
|
+
WHEN t.relreplident = 'f' THEN 'full'
|
414
|
+
END AS replica_identity
|
415
|
+
FROM pg_class t
|
416
|
+
JOIN pg_namespace ns ON t.relnamespace = ns.oid
|
417
|
+
WHERE ns.nspname = '#{schema_name}'
|
418
|
+
AND t.relkind = 'r'
|
419
|
+
AND t.relname IN (#{formatted_table_list})
|
420
|
+
SQL
|
421
|
+
|
422
|
+
results =
|
423
|
+
Query.run(
|
424
|
+
query: sql,
|
425
|
+
connection_url: conn_string,
|
426
|
+
user: db_user(conn_string),
|
427
|
+
)
|
428
|
+
|
429
|
+
results.all? { |r| r[:replica_identity] != "nothing" }
|
430
|
+
end
|
355
431
|
end
|
356
432
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_easy_replicate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shayon Mukherjee
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-01-
|
11
|
+
date: 2024-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ougai
|