pg_easy_replicate 0.1.7 → 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -3
- data/Dockerfile +2 -0
- data/Gemfile.lock +9 -7
- data/README.md +14 -7
- data/docker-compose.yml +4 -4
- data/lib/pg_easy_replicate/cli.rb +9 -0
- data/lib/pg_easy_replicate/helper.rb +5 -1
- data/lib/pg_easy_replicate/orchestrate.rb +14 -6
- data/lib/pg_easy_replicate/query.rb +2 -2
- data/lib/pg_easy_replicate/version.rb +1 -1
- data/lib/pg_easy_replicate.rb +74 -20
- data/scripts/e2e-bootstrap.sh +6 -10
- data/scripts/e2e-start.sh +5 -5
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02ca1ab51b854644e7a44fcb9a166a363b608784f4c38a6872cc6be5d35b21f3
|
4
|
+
data.tar.gz: dfb7b875e61b5c032940af84417ce7cf35b75897039ba4efd400513d7da5de20
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 698b38142b8f8a8c9a64623d9a3bb06791a3a5bc174c4abffc49a480787d6cf33a002b75e9e6a61c23f443c962818544ef3c98fab5970bc1bccf1224ebaa8cb6
|
7
|
+
data.tar.gz: f6942a91cade0e60c9c5e245f4cabe9a7d0e698316855c7eacbd53bc1b2769a3d465e12dc7e1cda34f10149b24b3bfde35a585ab6b8583f9645b9faae5439021
|
data/CHANGELOG.md
CHANGED
@@ -1,13 +1,23 @@
|
|
1
|
-
## [0.1.
|
1
|
+
## [0.1.8] - 2023-07-23
|
2
|
+
|
3
|
+
- Introduce --copy_schema via pg_dump - #35
|
4
|
+
|
5
|
+
## [0.1.7] - 2023-06-26
|
6
|
+
|
7
|
+
- Perform smoke test with retries in CI - #26
|
8
|
+
- Default schema to `public` #29
|
9
|
+
- Perform vacuum and analyze before and after switchover - #30
|
10
|
+
|
11
|
+
## [0.1.6] - 2023-06-24
|
2
12
|
|
3
13
|
- Bug fix: Support custom schema name
|
4
14
|
- New smoke spec in CI
|
5
15
|
|
6
|
-
## [0.1.5] - 2023-06-
|
16
|
+
## [0.1.5] - 2023-06-24
|
7
17
|
|
8
18
|
- Fix bug in `stop_sync`
|
9
19
|
|
10
|
-
## [0.1.4] - 2023-06-
|
20
|
+
## [0.1.4] - 2023-06-24
|
11
21
|
|
12
22
|
- Drop lockbox dependency
|
13
23
|
- Support password with special chars and test for url encoded URI
|
data/Dockerfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pg_easy_replicate (0.1.
|
4
|
+
pg_easy_replicate (0.1.9)
|
5
5
|
ougai (~> 2.0.0)
|
6
6
|
pg (~> 1.5.3)
|
7
|
-
sequel (
|
7
|
+
sequel (>= 5.69, < 5.71)
|
8
8
|
thor (~> 1.2.2)
|
9
9
|
|
10
10
|
GEM
|
@@ -18,6 +18,7 @@ GEM
|
|
18
18
|
thor
|
19
19
|
tilt
|
20
20
|
json (2.6.3)
|
21
|
+
language_server-protocol (3.17.0.3)
|
21
22
|
method_source (1.0.0)
|
22
23
|
oj (3.14.3)
|
23
24
|
ougai (2.0.0)
|
@@ -36,7 +37,7 @@ GEM
|
|
36
37
|
rake (13.0.6)
|
37
38
|
rbs (3.1.0)
|
38
39
|
regexp_parser (2.8.1)
|
39
|
-
rexml (3.2.
|
40
|
+
rexml (3.2.6)
|
40
41
|
rspec (3.12.0)
|
41
42
|
rspec-core (~> 3.12.0)
|
42
43
|
rspec-expectations (~> 3.12.0)
|
@@ -50,14 +51,15 @@ GEM
|
|
50
51
|
diff-lcs (>= 1.2.0, < 2.0)
|
51
52
|
rspec-support (~> 3.12.0)
|
52
53
|
rspec-support (3.12.0)
|
53
|
-
rubocop (1.
|
54
|
+
rubocop (1.55.0)
|
54
55
|
json (~> 2.3)
|
56
|
+
language_server-protocol (>= 3.17.0)
|
55
57
|
parallel (~> 1.10)
|
56
58
|
parser (>= 3.2.2.3)
|
57
59
|
rainbow (>= 2.2.2, < 4.0)
|
58
60
|
regexp_parser (>= 1.8, < 3.0)
|
59
61
|
rexml (>= 3.2.5, < 4.0)
|
60
|
-
rubocop-ast (>= 1.28.
|
62
|
+
rubocop-ast (>= 1.28.1, < 2.0)
|
61
63
|
ruby-progressbar (~> 1.7)
|
62
64
|
unicode-display_width (>= 2.4.0, < 3.0)
|
63
65
|
rubocop-ast (1.29.0)
|
@@ -73,12 +75,12 @@ GEM
|
|
73
75
|
rubocop-ast (>= 0.4.0)
|
74
76
|
rubocop-rake (0.6.0)
|
75
77
|
rubocop (~> 1.0)
|
76
|
-
rubocop-rspec (2.
|
78
|
+
rubocop-rspec (2.23.0)
|
77
79
|
rubocop (~> 1.33)
|
78
80
|
rubocop-capybara (~> 2.17)
|
79
81
|
rubocop-factory_bot (~> 2.22)
|
80
82
|
ruby-progressbar (1.13.0)
|
81
|
-
sequel (5.
|
83
|
+
sequel (5.70.0)
|
82
84
|
syntax_tree (6.1.1)
|
83
85
|
prettier_print (>= 1.2.0)
|
84
86
|
syntax_tree-haml (4.0.3)
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[![CI](https://github.com/shayonj/pg_easy_replicate/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/shayonj/pg_easy_replicate/actions/workflows/ci.yaml)
|
4
4
|
[![Smoke spec](https://github.com/shayonj/pg_easy_replicate/actions/workflows/smoke.yaml/badge.svg?branch=main)](https://github.com/shayonj/pg_easy_replicate/actions/workflows/ci.yaml)
|
5
|
-
[![Gem Version](https://badge.fury.io/rb/pg_easy_replicate.svg?
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/pg_easy_replicate.svg?2)](https://badge.fury.io/rb/pg_easy_replicate)
|
6
6
|
|
7
7
|
`pg_easy_replicate` is a CLI orchestrator tool that simplifies the process of setting up [logical replication](https://www.postgresql.org/docs/current/logical-replication.html) between two PostgreSQL databases. `pg_easy_replicate` also supports switchover. After the source (primary database) is fully replicating, `pg_easy_replicate` puts it into read-only mode and via logical replication flushes all data to the new target database. This ensures zero data loss and minimal downtime for the application. This method can be useful for performing minimal downtime (up to <1min, depending) major version upgrades between two PostgreSQL databases, load testing with blue/green database setup and other similar use cases.
|
8
8
|
|
@@ -26,6 +26,8 @@ Battle tested in production at [Tines](https://www.tines.com/) 🚀
|
|
26
26
|
- [Switchover strategies with minimal downtime](#switchover-strategies-with-minimal-downtime)
|
27
27
|
- [Rolling restart strategy](#rolling-restart-strategy)
|
28
28
|
- [DNS Failover strategy](#dns-failover-strategy)
|
29
|
+
- [FAQ](#faq)
|
30
|
+
- [Adding internal user to pgBouncer `userlist`](#adding-internal-user-to-pgbouncer-userlist)
|
29
31
|
- [Contributing](#contributing)
|
30
32
|
|
31
33
|
## Installation
|
@@ -57,7 +59,6 @@ https://hub.docker.com/r/shayonj/pg_easy_replicate
|
|
57
59
|
- PostgreSQL 10 and later
|
58
60
|
- Ruby 2.7 and later
|
59
61
|
- Database user should have permissions for `SUPERUSER` or pass in the special user role that has the privileges to create role, schema, publication and subscription on both databases. More on `--special-user-role` section below.
|
60
|
-
- Both databases should have the same schema
|
61
62
|
|
62
63
|
## Limits
|
63
64
|
|
@@ -115,7 +116,7 @@ $ pg_easy_replicate config_check
|
|
115
116
|
Every sync will need to be bootstrapped before you can set up the sync between two databases. Bootstrap creates a new super user to perform the orchestration required during the rest of the process. It also creates some internal metadata tables for record keeping.
|
116
117
|
|
117
118
|
```bash
|
118
|
-
$ pg_easy_replicate bootstrap --group-name database-cluster-1
|
119
|
+
$ pg_easy_replicate bootstrap --group-name database-cluster-1 --copy-schema
|
119
120
|
|
120
121
|
{"name":"pg_easy_replicate","hostname":"PKHXQVK6DW","pid":21485,"level":30,"time":"2023-06-19T15:51:11.015-04:00","v":0,"msg":"Setting up schema","version":"0.1.0"}
|
121
122
|
...
|
@@ -132,7 +133,7 @@ For AWS the special user role is `rds_superuser`, and for GCP it is `cloudsqlsup
|
|
132
133
|
#### Config Check
|
133
134
|
|
134
135
|
```bash
|
135
|
-
$ pg_easy_replicate config_check --special-user-role="rds_superuser"
|
136
|
+
$ pg_easy_replicate config_check --special-user-role="rds_superuser" --copy-schema
|
136
137
|
|
137
138
|
✅ Config is looking good.
|
138
139
|
```
|
@@ -140,7 +141,7 @@ $ pg_easy_replicate config_check --special-user-role="rds_superuser"
|
|
140
141
|
#### Bootstrap
|
141
142
|
|
142
143
|
```bash
|
143
|
-
$ pg_easy_replicate bootstrap --group-name database-cluster-1 --special-user-role="rds_superuser"
|
144
|
+
$ pg_easy_replicate bootstrap --group-name database-cluster-1 --special-user-role="rds_superuser" --copy-schema
|
144
145
|
|
145
146
|
{"name":"pg_easy_replicate","hostname":"PKHXQVK6DW","pid":21485,"level":30,"time":"2023-06-19T15:51:11.015-04:00","v":0,"msg":"Setting up schema","version":"0.1.0"}
|
146
147
|
...
|
@@ -217,12 +218,12 @@ By default all tables are added for replication but you can create multiple grou
|
|
217
218
|
|
218
219
|
```bash
|
219
220
|
|
220
|
-
$ pg_easy_replicate bootstrap --group-name database-cluster-1
|
221
|
+
$ pg_easy_replicate bootstrap --group-name database-cluster-1 --copy-schema
|
221
222
|
$ pg_easy_replicate start_sync --group-name database-cluster-1 --schema-name public --tables "users, posts, events"
|
222
223
|
|
223
224
|
...
|
224
225
|
|
225
|
-
$ pg_easy_replicate bootstrap --group-name database-cluster-2
|
226
|
+
$ pg_easy_replicate bootstrap --group-name database-cluster-2 --copy-schema
|
226
227
|
$ pg_easy_replicate start_sync --group-name database-cluster-2 --schema-name public --tables "comments, views"
|
227
228
|
|
228
229
|
...
|
@@ -247,6 +248,12 @@ In this strategy, you have a weighted based DNS system (example [AWS Route53 wei
|
|
247
248
|
|
248
249
|
Next, you can set up a program that watches the `stats` and waits until `switchover_completed_at` is reporting as `true`. Once that happens it updates the weight in the DNS weighted group where 100% of the requests now go to the new/target database. Note: Keeping a low `ttl` is recommended.
|
249
250
|
|
251
|
+
## FAQ
|
252
|
+
|
253
|
+
### Adding internal user to pgBouncer `userlist`
|
254
|
+
|
255
|
+
`pg_easy_replicate` creates a special user to orchestrate the replication. If you us pgBouncer, you may need to allow `pger_su_h1a4fb` as a user that can perform login by adding it to the `userlist`.
|
256
|
+
|
250
257
|
## Contributing
|
251
258
|
|
252
259
|
PRs most welcome. You can get started locally by
|
data/docker-compose.yml
CHANGED
@@ -5,8 +5,8 @@ services:
|
|
5
5
|
ports:
|
6
6
|
- "5432:5432"
|
7
7
|
environment:
|
8
|
-
POSTGRES_USER:
|
9
|
-
POSTGRES_PASSWORD:
|
8
|
+
POSTGRES_USER: james-bond
|
9
|
+
POSTGRES_PASSWORD: james-bond123@7!'3aaR
|
10
10
|
POSTGRES_DB: postgres
|
11
11
|
command: >
|
12
12
|
-c wal_level=logical
|
@@ -21,8 +21,8 @@ services:
|
|
21
21
|
ports:
|
22
22
|
- "5433:5432"
|
23
23
|
environment:
|
24
|
-
POSTGRES_USER:
|
25
|
-
POSTGRES_PASSWORD:
|
24
|
+
POSTGRES_USER: james-bond
|
25
|
+
POSTGRES_PASSWORD: james-bond123@7!'3aaR
|
26
26
|
POSTGRES_DB: postgres
|
27
27
|
command: >
|
28
28
|
-c wal_level=logical
|
@@ -12,9 +12,14 @@ module PgEasyReplicate
|
|
12
12
|
aliases: "-s",
|
13
13
|
desc:
|
14
14
|
"Name of the role that has superuser permissions. Usually useful for AWS (rds_superuser) or GCP (cloudsqlsuperuser)."
|
15
|
+
method_option :copy_schema,
|
16
|
+
aliases: "-c",
|
17
|
+
boolean: true,
|
18
|
+
desc: "Copy schema to the new database"
|
15
19
|
def config_check
|
16
20
|
PgEasyReplicate.assert_config(
|
17
21
|
special_user_role: options[:special_user_role],
|
22
|
+
copy_schema: options[:copy_schema],
|
18
23
|
)
|
19
24
|
|
20
25
|
puts "✅ Config is looking good."
|
@@ -28,6 +33,10 @@ module PgEasyReplicate
|
|
28
33
|
aliases: "-s",
|
29
34
|
desc:
|
30
35
|
"Name of the role that has superuser permissions. Usually useful with AWS (rds_superuser) or GCP (cloudsqlsuperuser)."
|
36
|
+
method_option :copy_schema,
|
37
|
+
aliases: "-c",
|
38
|
+
boolean: true,
|
39
|
+
desc: "Copy schema to the new database"
|
31
40
|
desc "bootstrap",
|
32
41
|
"Sets up temporary tables for information required during runtime"
|
33
42
|
def bootstrap
|
@@ -36,13 +36,17 @@ module PgEasyReplicate
|
|
36
36
|
|
37
37
|
def underscore(str)
|
38
38
|
str
|
39
|
-
.gsub(
|
39
|
+
.gsub("::", "/")
|
40
40
|
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
41
41
|
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
42
42
|
.tr("-", "_")
|
43
43
|
.downcase
|
44
44
|
end
|
45
45
|
|
46
|
+
def quote_ident(sql_ident)
|
47
|
+
PG::Connection.quote_ident(sql_ident)
|
48
|
+
end
|
49
|
+
|
46
50
|
def test_env?
|
47
51
|
ENV.fetch("RACK_ENV", nil) == "test"
|
48
52
|
end
|
@@ -94,7 +94,8 @@ module PgEasyReplicate
|
|
94
94
|
.map do |table_name|
|
95
95
|
Query.run(
|
96
96
|
query:
|
97
|
-
"ALTER PUBLICATION #{publication_name(group_name)}
|
97
|
+
"ALTER PUBLICATION #{quote_ident(publication_name(group_name))}
|
98
|
+
ADD TABLE #{quote_ident(table_name)}",
|
98
99
|
connection_url: conn_string,
|
99
100
|
schema: schema,
|
100
101
|
)
|
@@ -107,7 +108,11 @@ module PgEasyReplicate
|
|
107
108
|
Query
|
108
109
|
.run(
|
109
110
|
query:
|
110
|
-
"SELECT table_name
|
111
|
+
"SELECT table_name
|
112
|
+
FROM information_schema.tables
|
113
|
+
WHERE table_schema = '#{schema}' AND
|
114
|
+
table_type = 'BASE TABLE'
|
115
|
+
ORDER BY table_name",
|
111
116
|
connection_url: conn_string,
|
112
117
|
)
|
113
118
|
.map(&:values)
|
@@ -121,7 +126,8 @@ module PgEasyReplicate
|
|
121
126
|
{ publication_name: publication_name(group_name) },
|
122
127
|
)
|
123
128
|
Query.run(
|
124
|
-
query:
|
129
|
+
query:
|
130
|
+
"DROP PUBLICATION IF EXISTS #{quote_ident(publication_name(group_name))}",
|
125
131
|
connection_url: conn_string,
|
126
132
|
user: db_user(conn_string),
|
127
133
|
)
|
@@ -144,7 +150,9 @@ module PgEasyReplicate
|
|
144
150
|
|
145
151
|
Query.run(
|
146
152
|
query:
|
147
|
-
"CREATE SUBSCRIPTION #{subscription_name(group_name)
|
153
|
+
"CREATE SUBSCRIPTION #{quote_ident(subscription_name(group_name))}
|
154
|
+
CONNECTION '#{source_conn_string}'
|
155
|
+
PUBLICATION #{quote_ident(publication_name(group_name))}",
|
148
156
|
connection_url: target_conn_string,
|
149
157
|
user: db_user(target_conn_string),
|
150
158
|
transaction: false,
|
@@ -282,7 +290,7 @@ module PgEasyReplicate
|
|
282
290
|
)
|
283
291
|
|
284
292
|
alter_sql =
|
285
|
-
"ALTER USER #{db_user(source_db_url)} set default_transaction_read_only = true"
|
293
|
+
"ALTER USER #{quote_ident(db_user(source_db_url))} set default_transaction_read_only = true"
|
286
294
|
Query.run(query: alter_sql, connection_url: source_db_url)
|
287
295
|
|
288
296
|
kill_sql =
|
@@ -297,7 +305,7 @@ module PgEasyReplicate
|
|
297
305
|
logger.info("Restoring connections")
|
298
306
|
|
299
307
|
alter_sql =
|
300
|
-
"ALTER USER #{db_user(source_db_url)} set default_transaction_read_only = false"
|
308
|
+
"ALTER USER #{quote_ident(db_user(source_db_url))} set default_transaction_read_only = false"
|
301
309
|
Query.run(query: alter_sql, connection_url: source_db_url)
|
302
310
|
end
|
303
311
|
|
@@ -17,12 +17,12 @@ module PgEasyReplicate
|
|
17
17
|
if transaction
|
18
18
|
r =
|
19
19
|
conn.transaction do
|
20
|
-
conn.run("SET search_path to #{schema}") if schema
|
20
|
+
conn.run("SET search_path to #{quote_ident(schema)}") if schema
|
21
21
|
conn.run("SET statement_timeout to '5s'")
|
22
22
|
conn.fetch(query).to_a
|
23
23
|
end
|
24
24
|
else
|
25
|
-
conn.run("SET search_path to #{schema}") if schema
|
25
|
+
conn.run("SET search_path to #{quote_ident(schema)}") if schema
|
26
26
|
conn.run("SET statement_timeout to '5s'")
|
27
27
|
r = conn.fetch(query).to_a
|
28
28
|
end
|
data/lib/pg_easy_replicate.rb
CHANGED
@@ -4,6 +4,8 @@ require "json"
|
|
4
4
|
require "ougai"
|
5
5
|
require "pg"
|
6
6
|
require "sequel"
|
7
|
+
require "open3"
|
8
|
+
require "English"
|
7
9
|
|
8
10
|
require "pg_easy_replicate/helper"
|
9
11
|
require "pg_easy_replicate/version"
|
@@ -15,16 +17,21 @@ require "pg_easy_replicate/cli"
|
|
15
17
|
|
16
18
|
Sequel.default_timezone = :utc
|
17
19
|
module PgEasyReplicate
|
20
|
+
SCHEMA_FILE_LOCATION = "/tmp/pger_schema.sql"
|
21
|
+
|
18
22
|
class Error < StandardError
|
19
23
|
end
|
20
24
|
|
21
25
|
extend Helper
|
22
26
|
|
23
27
|
class << self
|
24
|
-
def config(special_user_role: nil)
|
28
|
+
def config(special_user_role: nil, copy_schema: false)
|
25
29
|
abort_with("SOURCE_DB_URL is missing") if source_db_url.nil?
|
26
30
|
abort_with("TARGET_DB_URL is missing") if target_db_url.nil?
|
27
31
|
|
32
|
+
system("which pg_dump")
|
33
|
+
pg_dump_exists = $CHILD_STATUS.success?
|
34
|
+
|
28
35
|
@config ||=
|
29
36
|
begin
|
30
37
|
q =
|
@@ -47,14 +54,20 @@ module PgEasyReplicate
|
|
47
54
|
connection_url: target_db_url,
|
48
55
|
user: db_user(target_db_url),
|
49
56
|
),
|
57
|
+
pg_dump_exists: pg_dump_exists,
|
50
58
|
}
|
51
59
|
rescue => e
|
52
60
|
abort_with("Unable to check config: #{e.message}")
|
53
61
|
end
|
54
62
|
end
|
55
63
|
|
56
|
-
def assert_config(special_user_role: nil)
|
57
|
-
config_hash =
|
64
|
+
def assert_config(special_user_role: nil, copy_schema: false)
|
65
|
+
config_hash =
|
66
|
+
config(special_user_role: special_user_role, copy_schema: copy_schema)
|
67
|
+
|
68
|
+
if copy_schema && !config_hash.dig(:pg_dump_exists)
|
69
|
+
abort_with("pg_dump must exist if copy_schema (-c) is passed")
|
70
|
+
end
|
58
71
|
|
59
72
|
unless assert_wal_level_logical(config_hash.dig(:source_db))
|
60
73
|
abort_with("WAL_LEVEL should be LOGICAL on source DB")
|
@@ -74,7 +87,15 @@ module PgEasyReplicate
|
|
74
87
|
|
75
88
|
def bootstrap(options)
|
76
89
|
logger.info("Setting up schema")
|
77
|
-
|
90
|
+
setup_internal_schema
|
91
|
+
|
92
|
+
if options[:copy_schema]
|
93
|
+
logger.info("Setting up schema on targer database")
|
94
|
+
copy_schema(
|
95
|
+
source_conn_string: source_db_url,
|
96
|
+
target_conn_string: target_db_url,
|
97
|
+
)
|
98
|
+
end
|
78
99
|
|
79
100
|
logger.info("Setting up replication user on source database")
|
80
101
|
create_user(
|
@@ -103,7 +124,7 @@ module PgEasyReplicate
|
|
103
124
|
|
104
125
|
if options[:everything]
|
105
126
|
logger.info("Dropping schema")
|
106
|
-
|
127
|
+
drop_internal_schema
|
107
128
|
end
|
108
129
|
|
109
130
|
if options[:everything] || options[:sync]
|
@@ -130,9 +151,10 @@ module PgEasyReplicate
|
|
130
151
|
abort_with("Unable to cleanup: #{e.message}")
|
131
152
|
end
|
132
153
|
|
133
|
-
def
|
154
|
+
def drop_internal_schema
|
134
155
|
Query.run(
|
135
|
-
query:
|
156
|
+
query:
|
157
|
+
"DROP SCHEMA IF EXISTS #{quote_ident(internal_schema_name)} CASCADE",
|
136
158
|
connection_url: source_db_url,
|
137
159
|
schema: internal_schema_name,
|
138
160
|
user: db_user(target_db_url),
|
@@ -141,11 +163,11 @@ module PgEasyReplicate
|
|
141
163
|
raise "Unable to drop schema: #{e.message}"
|
142
164
|
end
|
143
165
|
|
144
|
-
def
|
166
|
+
def setup_internal_schema
|
145
167
|
sql = <<~SQL
|
146
|
-
create schema if not exists #{internal_schema_name};
|
147
|
-
grant usage on schema #{internal_schema_name} to #{db_user(source_db_url)};
|
148
|
-
grant create on schema #{internal_schema_name} to #{db_user(source_db_url)};
|
168
|
+
create schema if not exists #{quote_ident(internal_schema_name)};
|
169
|
+
grant usage on schema #{quote_ident(internal_schema_name)} to #{quote_ident(db_user(source_db_url))};
|
170
|
+
grant create on schema #{quote_ident(internal_schema_name)} to #{quote_ident(db_user(source_db_url))};
|
149
171
|
SQL
|
150
172
|
|
151
173
|
Query.run(
|
@@ -169,7 +191,39 @@ module PgEasyReplicate
|
|
169
191
|
end
|
170
192
|
end
|
171
193
|
|
172
|
-
|
194
|
+
def copy_schema(source_conn_string:, target_conn_string:)
|
195
|
+
export_schema(conn_string: source_conn_string)
|
196
|
+
import_schema(conn_string: target_conn_string)
|
197
|
+
end
|
198
|
+
|
199
|
+
def export_schema(conn_string:)
|
200
|
+
logger.info("Exporting schema to #{SCHEMA_FILE_LOCATION}")
|
201
|
+
_, stderr, status =
|
202
|
+
Open3.capture3(
|
203
|
+
"pg_dump",
|
204
|
+
conn_string,
|
205
|
+
"-f",
|
206
|
+
SCHEMA_FILE_LOCATION,
|
207
|
+
"--schema-only",
|
208
|
+
)
|
209
|
+
|
210
|
+
success = status.success?
|
211
|
+
raise stderr unless success
|
212
|
+
rescue => e
|
213
|
+
raise "Unable to export schema: #{e.message}"
|
214
|
+
end
|
215
|
+
|
216
|
+
def import_schema(conn_string:)
|
217
|
+
logger.info("Importing schema from #{SCHEMA_FILE_LOCATION}")
|
218
|
+
|
219
|
+
_, stderr, status =
|
220
|
+
Open3.capture3("psql", "-f", SCHEMA_FILE_LOCATION, conn_string)
|
221
|
+
|
222
|
+
success = status.success?
|
223
|
+
raise stderr unless success
|
224
|
+
rescue => e
|
225
|
+
raise "Unable to import schema: #{e.message}"
|
226
|
+
end
|
173
227
|
|
174
228
|
def assert_wal_level_logical(db_config)
|
175
229
|
db_config&.find do |r|
|
@@ -221,9 +275,9 @@ module PgEasyReplicate
|
|
221
275
|
password = connection_info(conn_string)[:password].gsub("'") { "''" }
|
222
276
|
|
223
277
|
sql = <<~SQL
|
224
|
-
drop role if exists #{internal_user_name};
|
225
|
-
create role #{internal_user_name} with password '#{password}' login createdb createrole;
|
226
|
-
grant all privileges on database #{db_name(conn_string)} TO #{internal_user_name};
|
278
|
+
drop role if exists #{quote_ident(internal_user_name)};
|
279
|
+
create role #{quote_ident(internal_user_name)} with password '#{password}' login createdb createrole;
|
280
|
+
grant all privileges on database #{quote_ident(db_name(conn_string))} TO #{quote_ident(internal_user_name)};
|
227
281
|
SQL
|
228
282
|
|
229
283
|
Query.run(
|
@@ -235,9 +289,9 @@ module PgEasyReplicate
|
|
235
289
|
|
236
290
|
sql =
|
237
291
|
if special_user_role
|
238
|
-
"grant #{special_user_role} to #{internal_user_name};"
|
292
|
+
"grant #{quote_ident(special_user_role)} to #{quote_ident(internal_user_name)};"
|
239
293
|
else
|
240
|
-
"alter user #{internal_user_name} with superuser;"
|
294
|
+
"alter user #{quote_ident(internal_user_name)} with superuser;"
|
241
295
|
end
|
242
296
|
|
243
297
|
Query.run(
|
@@ -250,7 +304,7 @@ module PgEasyReplicate
|
|
250
304
|
return unless grant_permissions_on_schema
|
251
305
|
Query.run(
|
252
306
|
query:
|
253
|
-
"grant all on schema #{internal_schema_name} to #{internal_user_name}",
|
307
|
+
"grant all on schema #{quote_ident(internal_schema_name)} to #{quote_ident(internal_user_name)}",
|
254
308
|
connection_url: conn_string,
|
255
309
|
user: db_user(conn_string),
|
256
310
|
transaction: false,
|
@@ -261,7 +315,7 @@ module PgEasyReplicate
|
|
261
315
|
|
262
316
|
def drop_user(conn_string:, group_name:)
|
263
317
|
sql = <<~SQL
|
264
|
-
revoke all privileges on database #{db_name(conn_string)} from #{internal_user_name};
|
318
|
+
revoke all privileges on database #{db_name(conn_string)} from #{quote_ident(internal_user_name)};
|
265
319
|
SQL
|
266
320
|
Query.run(
|
267
321
|
query: sql,
|
@@ -270,7 +324,7 @@ module PgEasyReplicate
|
|
270
324
|
)
|
271
325
|
|
272
326
|
sql = <<~SQL
|
273
|
-
drop role if exists #{internal_user_name};
|
327
|
+
drop role if exists #{quote_ident(internal_user_name)};
|
274
328
|
SQL
|
275
329
|
|
276
330
|
Query.run(
|
data/scripts/e2e-bootstrap.sh
CHANGED
@@ -3,17 +3,13 @@
|
|
3
3
|
set -eo pipefail
|
4
4
|
|
5
5
|
if [[ -z ${GITHUB_WORKFLOW} ]]; then
|
6
|
-
export SECONDARY_SOURCE_DB_URL="postgres://
|
6
|
+
export SECONDARY_SOURCE_DB_URL="postgres://james-bond:james-bond123%407%21%273aaR@source_db/postgres"
|
7
7
|
fi
|
8
8
|
|
9
|
-
export SOURCE_DB_URL="postgres://
|
10
|
-
export TARGET_DB_URL="postgres://
|
11
|
-
export PGPASSWORD='
|
9
|
+
export SOURCE_DB_URL="postgres://james-bond:james-bond123%407%21%273aaR@localhost:5432/postgres"
|
10
|
+
export TARGET_DB_URL="postgres://james-bond:james-bond123%407%21%273aaR@localhost:5433/postgres"
|
11
|
+
export PGPASSWORD='james-bond123@7!'"'"'3aaR'
|
12
12
|
|
13
|
-
pgbench --initialize -s 5 --foreign-keys --host localhost -U
|
13
|
+
pgbench --initialize -s 5 --foreign-keys --host localhost -U james-bond -d postgres
|
14
14
|
|
15
|
-
|
16
|
-
cat schema.sql | psql --host localhost -U jamesbond -d postgres -p 5433
|
17
|
-
rm schema.sql
|
18
|
-
|
19
|
-
bundle exec bin/pg_easy_replicate config_check
|
15
|
+
bundle exec bin/pg_easy_replicate config_check --copy-schema
|
data/scripts/e2e-start.sh
CHANGED
@@ -3,16 +3,16 @@
|
|
3
3
|
set -eo pipefail
|
4
4
|
|
5
5
|
if [[ -z ${GITHUB_WORKFLOW} ]]; then
|
6
|
-
export SECONDARY_SOURCE_DB_URL="postgres://
|
6
|
+
export SECONDARY_SOURCE_DB_URL="postgres://james-bond:james-bond123%407%21%273aaR@source_db/postgres"
|
7
7
|
fi
|
8
8
|
|
9
|
-
export SOURCE_DB_URL="postgres://
|
10
|
-
export TARGET_DB_URL="postgres://
|
11
|
-
export PGPASSWORD='
|
9
|
+
export SOURCE_DB_URL="postgres://james-bond:james-bond123%407%21%273aaR@localhost:5432/postgres"
|
10
|
+
export TARGET_DB_URL="postgres://james-bond:james-bond123%407%21%273aaR@localhost:5433/postgres"
|
11
|
+
export PGPASSWORD='james-bond123@7!'"'"''"'"'3aaR'
|
12
12
|
|
13
13
|
# Bootstrap and cleanup
|
14
14
|
echo "===== Performing Bootstrap and cleanup"
|
15
|
-
bundle exec bin/pg_easy_replicate bootstrap -g cluster-1
|
15
|
+
bundle exec bin/pg_easy_replicate bootstrap -g cluster-1 --copy-schema
|
16
16
|
bundle exec bin/pg_easy_replicate start_sync -g cluster-1 -s public
|
17
17
|
bundle exec bin/pg_easy_replicate stats -g cluster-1
|
18
18
|
bundle exec bin/pg_easy_replicate switchover -g cluster-1
|
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.1.
|
4
|
+
version: 0.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shayon Mukherjee
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ougai
|
@@ -42,16 +42,22 @@ dependencies:
|
|
42
42
|
name: sequel
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.69'
|
48
|
+
- - "<"
|
46
49
|
- !ruby/object:Gem::Version
|
47
|
-
version: 5.
|
50
|
+
version: '5.71'
|
48
51
|
type: :runtime
|
49
52
|
prerelease: false
|
50
53
|
version_requirements: !ruby/object:Gem::Requirement
|
51
54
|
requirements:
|
52
|
-
- - "
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '5.69'
|
58
|
+
- - "<"
|
53
59
|
- !ruby/object:Gem::Version
|
54
|
-
version: 5.
|
60
|
+
version: '5.71'
|
55
61
|
- !ruby/object:Gem::Dependency
|
56
62
|
name: thor
|
57
63
|
requirement: !ruby/object:Gem::Requirement
|