pg_easy_replicate 0.2.1 → 0.2.2
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/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
|