pgsync 0.5.5 → 0.6.4
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.
Potentially problematic release.
This version of pgsync might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -3
- data/LICENSE.txt +1 -1
- data/README.md +87 -32
- data/config.yml +4 -0
- data/lib/pgsync.rb +6 -1
- data/lib/pgsync/client.rb +55 -57
- data/lib/pgsync/data_source.rb +55 -104
- data/lib/pgsync/init.rb +50 -6
- data/lib/pgsync/schema_sync.rb +83 -0
- data/lib/pgsync/sequence.rb +29 -0
- data/lib/pgsync/sync.rb +82 -199
- data/lib/pgsync/table.rb +28 -0
- data/lib/pgsync/table_sync.rb +254 -207
- data/lib/pgsync/task.rb +325 -0
- data/lib/pgsync/task_resolver.rb +237 -0
- data/lib/pgsync/utils.rb +55 -14
- data/lib/pgsync/version.rb +1 -1
- metadata +7 -3
- data/lib/pgsync/table_list.rb +0 -141
data/lib/pgsync/data_source.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
module PgSync
|
2
2
|
class DataSource
|
3
|
+
include Utils
|
4
|
+
|
3
5
|
attr_reader :url
|
4
6
|
|
5
|
-
def initialize(
|
6
|
-
@url =
|
7
|
+
def initialize(url, name:, debug:)
|
8
|
+
@url = url
|
9
|
+
@name = name
|
10
|
+
@debug = debug
|
7
11
|
end
|
8
12
|
|
9
13
|
def exists?
|
@@ -29,8 +33,18 @@ module PgSync
|
|
29
33
|
# gets visible tables
|
30
34
|
def tables
|
31
35
|
@tables ||= begin
|
32
|
-
query =
|
33
|
-
|
36
|
+
query = <<~SQL
|
37
|
+
SELECT
|
38
|
+
table_schema AS schema,
|
39
|
+
table_name AS table
|
40
|
+
FROM
|
41
|
+
information_schema.tables
|
42
|
+
WHERE
|
43
|
+
table_type = 'BASE TABLE' AND
|
44
|
+
table_schema NOT IN ('information_schema', 'pg_catalog')
|
45
|
+
ORDER BY 1, 2
|
46
|
+
SQL
|
47
|
+
execute(query).map { |row| Table.new(row["schema"], row["table"]) }
|
34
48
|
end
|
35
49
|
end
|
36
50
|
|
@@ -38,54 +52,24 @@ module PgSync
|
|
38
52
|
table_set.include?(table)
|
39
53
|
end
|
40
54
|
|
41
|
-
def columns(table)
|
42
|
-
query = "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2"
|
43
|
-
execute(query, table.split(".", 2)).map { |row| row["column_name"] }
|
44
|
-
end
|
45
|
-
|
46
|
-
def sequences(table, columns)
|
47
|
-
execute("SELECT #{columns.map { |f| "pg_get_serial_sequence(#{escape("#{quote_ident_full(table)}")}, #{escape(f)}) AS #{quote_ident(f)}" }.join(", ")}")[0].values.compact
|
48
|
-
end
|
49
|
-
|
50
55
|
def max_id(table, primary_key, sql_clause = nil)
|
51
|
-
execute("SELECT MAX(#{quote_ident(primary_key)}) FROM #{quote_ident_full(table)}#{sql_clause}")[
|
56
|
+
execute("SELECT MAX(#{quote_ident(primary_key)}) FROM #{quote_ident_full(table)}#{sql_clause}").first["max"].to_i
|
52
57
|
end
|
53
58
|
|
54
59
|
def min_id(table, primary_key, sql_clause = nil)
|
55
|
-
execute("SELECT MIN(#{quote_ident(primary_key)}) FROM #{quote_ident_full(table)}#{sql_clause}")[
|
60
|
+
execute("SELECT MIN(#{quote_ident(primary_key)}) FROM #{quote_ident_full(table)}#{sql_clause}").first["min"].to_i
|
56
61
|
end
|
57
62
|
|
58
63
|
def last_value(seq)
|
59
|
-
execute("
|
64
|
+
execute("SELECT last_value FROM #{quote_ident_full(seq)}").first["last_value"]
|
60
65
|
end
|
61
66
|
|
62
67
|
def truncate(table)
|
63
68
|
execute("TRUNCATE #{quote_ident_full(table)} CASCADE")
|
64
69
|
end
|
65
70
|
|
66
|
-
# https://stackoverflow.com/a/20537829
|
67
|
-
def primary_key(table)
|
68
|
-
query = <<-SQL
|
69
|
-
SELECT
|
70
|
-
pg_attribute.attname,
|
71
|
-
format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
|
72
|
-
FROM
|
73
|
-
pg_index, pg_class, pg_attribute, pg_namespace
|
74
|
-
WHERE
|
75
|
-
pg_class.oid = $2::regclass AND
|
76
|
-
indrelid = pg_class.oid AND
|
77
|
-
nspname = $1 AND
|
78
|
-
pg_class.relnamespace = pg_namespace.oid AND
|
79
|
-
pg_attribute.attrelid = pg_class.oid AND
|
80
|
-
pg_attribute.attnum = any(pg_index.indkey) AND
|
81
|
-
indisprimary
|
82
|
-
SQL
|
83
|
-
row = execute(query, [table.split(".", 2)[0], quote_ident_full(table)])[0]
|
84
|
-
row && row["attname"]
|
85
|
-
end
|
86
|
-
|
87
71
|
def triggers(table)
|
88
|
-
query =
|
72
|
+
query = <<~SQL
|
89
73
|
SELECT
|
90
74
|
tgname AS name,
|
91
75
|
tgisinternal AS internal,
|
@@ -108,9 +92,10 @@ module PgSync
|
|
108
92
|
else
|
109
93
|
config = {dbname: @url}
|
110
94
|
end
|
95
|
+
@concurrent_id = concurrent_id
|
111
96
|
PG::Connection.new(config)
|
112
97
|
rescue URI::InvalidURIError
|
113
|
-
raise Error, "Invalid connection string"
|
98
|
+
raise Error, "Invalid connection string. Make sure it works with `psql`"
|
114
99
|
end
|
115
100
|
end
|
116
101
|
end
|
@@ -122,55 +107,57 @@ module PgSync
|
|
122
107
|
end
|
123
108
|
end
|
124
109
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
def dump_command(tables)
|
130
|
-
tables = tables ? tables.keys.map { |t| "-t #{Shellwords.escape(quote_ident_full(t))}" }.join(" ") : ""
|
131
|
-
"pg_dump -Fc --verbose --schema-only --no-owner --no-acl #{tables} -d #{@url}"
|
110
|
+
# reconnect for new thread or process
|
111
|
+
def reconnect_if_needed
|
112
|
+
reconnect if @concurrent_id != concurrent_id
|
132
113
|
end
|
133
114
|
|
134
|
-
def
|
135
|
-
|
136
|
-
"pg_restore --verbose --no-owner --no-acl --clean #{if_exists ? "--if-exists" : nil} -d #{@url}"
|
137
|
-
end
|
138
|
-
|
139
|
-
def fully_resolve_tables(tables)
|
140
|
-
no_schema_tables = {}
|
141
|
-
search_path_index = Hash[search_path.map.with_index.to_a]
|
142
|
-
self.tables.group_by { |t| t.split(".", 2)[-1] }.each do |group, t2|
|
143
|
-
no_schema_tables[group] = t2.sort_by { |t| [search_path_index[t.split(".", 2)[0]] || 1000000, t] }[0]
|
144
|
-
end
|
145
|
-
|
146
|
-
Hash[tables.map { |k, v| [no_schema_tables[k] || k, v] }]
|
115
|
+
def search_path
|
116
|
+
@search_path ||= execute("SELECT unnest(current_schemas(true)) AS schema").map { |r| r["schema"] }
|
147
117
|
end
|
148
118
|
|
149
|
-
def
|
150
|
-
@
|
119
|
+
def server_version_num
|
120
|
+
@server_version_num ||= execute("SHOW server_version_num").first["server_version_num"].to_i
|
151
121
|
end
|
152
122
|
|
153
123
|
def execute(query, params = [])
|
124
|
+
log_sql query, params
|
154
125
|
conn.exec_params(query, params).to_a
|
155
126
|
end
|
156
127
|
|
157
128
|
def transaction
|
158
129
|
if conn.transaction_status == 0
|
159
130
|
# not currently in transaction
|
160
|
-
|
161
|
-
|
162
|
-
|
131
|
+
log_sql "BEGIN"
|
132
|
+
result =
|
133
|
+
conn.transaction do
|
134
|
+
yield
|
135
|
+
end
|
136
|
+
log_sql "COMMIT"
|
137
|
+
result
|
163
138
|
else
|
164
139
|
yield
|
165
140
|
end
|
166
141
|
end
|
167
142
|
|
143
|
+
# TODO log time for each statement
|
144
|
+
def log_sql(query, params = {})
|
145
|
+
if @debug
|
146
|
+
message = "#{colorize("[#{@name}]", :cyan)} #{query.gsub(/\s+/, " ").strip}"
|
147
|
+
message = "#{message} #{params.inspect}" if params.any?
|
148
|
+
log message
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
168
152
|
private
|
169
153
|
|
170
|
-
def
|
171
|
-
|
172
|
-
|
173
|
-
|
154
|
+
def concurrent_id
|
155
|
+
[Process.pid, Thread.current.object_id]
|
156
|
+
end
|
157
|
+
|
158
|
+
def reconnect
|
159
|
+
@conn.reset
|
160
|
+
@concurrent_id = concurrent_id
|
174
161
|
end
|
175
162
|
|
176
163
|
def table_set
|
@@ -185,41 +172,5 @@ module PgSync
|
|
185
172
|
conn.conninfo_hash
|
186
173
|
end
|
187
174
|
end
|
188
|
-
|
189
|
-
def quote_ident_full(ident)
|
190
|
-
ident.split(".", 2).map { |v| quote_ident(v) }.join(".")
|
191
|
-
end
|
192
|
-
|
193
|
-
def quote_ident(value)
|
194
|
-
PG::Connection.quote_ident(value)
|
195
|
-
end
|
196
|
-
|
197
|
-
def escape(value)
|
198
|
-
if value.is_a?(String)
|
199
|
-
"'#{quote_string(value)}'"
|
200
|
-
else
|
201
|
-
value
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
# activerecord
|
206
|
-
def quote_string(s)
|
207
|
-
s.gsub(/\\/, '\&\&').gsub(/'/, "''")
|
208
|
-
end
|
209
|
-
|
210
|
-
def resolve_url(source)
|
211
|
-
if source
|
212
|
-
source = source.dup
|
213
|
-
source.gsub!(/\$\([^)]+\)/) do |m|
|
214
|
-
command = m[2..-2]
|
215
|
-
result = `#{command}`.chomp
|
216
|
-
unless $?.success?
|
217
|
-
raise Error, "Command exited with non-zero status:\n#{command}"
|
218
|
-
end
|
219
|
-
result
|
220
|
-
end
|
221
|
-
end
|
222
|
-
source
|
223
|
-
end
|
224
175
|
end
|
225
176
|
end
|
data/lib/pgsync/init.rb
CHANGED
@@ -2,11 +2,26 @@ module PgSync
|
|
2
2
|
class Init
|
3
3
|
include Utils
|
4
4
|
|
5
|
-
def
|
6
|
-
|
7
|
-
@options =
|
5
|
+
def initialize(arguments, options)
|
6
|
+
@arguments = arguments
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
if @arguments.size > 1
|
12
|
+
raise Error, "Usage:\n pgsync --init [db]"
|
13
|
+
end
|
8
14
|
|
9
|
-
file =
|
15
|
+
file =
|
16
|
+
if @options[:config]
|
17
|
+
@options[:config]
|
18
|
+
elsif @arguments.any?
|
19
|
+
db_config_file(@arguments.first)
|
20
|
+
elsif @options[:db]
|
21
|
+
db_config_file(@options[:db])
|
22
|
+
else
|
23
|
+
".pgsync.yml"
|
24
|
+
end
|
10
25
|
|
11
26
|
if File.exist?(file)
|
12
27
|
raise Error, "#{file} exists."
|
@@ -15,8 +30,19 @@ module PgSync
|
|
15
30
|
if rails?
|
16
31
|
<<~EOS
|
17
32
|
exclude:
|
18
|
-
- schema_migrations
|
19
33
|
- ar_internal_metadata
|
34
|
+
- schema_migrations
|
35
|
+
EOS
|
36
|
+
elsif django?
|
37
|
+
# TODO exclude other tables?
|
38
|
+
<<~EOS
|
39
|
+
exclude:
|
40
|
+
- django_migrations
|
41
|
+
EOS
|
42
|
+
elsif laravel?
|
43
|
+
<<~EOS
|
44
|
+
exclude:
|
45
|
+
- migrations
|
20
46
|
EOS
|
21
47
|
else
|
22
48
|
<<~EOS
|
@@ -35,12 +61,30 @@ module PgSync
|
|
35
61
|
end
|
36
62
|
end
|
37
63
|
|
64
|
+
def django?
|
65
|
+
file_exists?("manage.py", /django/i)
|
66
|
+
end
|
67
|
+
|
38
68
|
def heroku?
|
39
69
|
`git remote -v 2>&1`.include?("git.heroku.com") rescue false
|
40
70
|
end
|
41
71
|
|
72
|
+
def laravel?
|
73
|
+
file_exists?("artisan")
|
74
|
+
end
|
75
|
+
|
42
76
|
def rails?
|
43
|
-
|
77
|
+
file_exists?("bin/rails")
|
78
|
+
end
|
79
|
+
|
80
|
+
def file_exists?(path, contents = nil)
|
81
|
+
if contents
|
82
|
+
File.read(path).match(contents)
|
83
|
+
else
|
84
|
+
File.exist?(path)
|
85
|
+
end
|
86
|
+
rescue
|
87
|
+
false
|
44
88
|
end
|
45
89
|
end
|
46
90
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module PgSync
|
2
|
+
class SchemaSync
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
attr_reader :args, :opts
|
6
|
+
|
7
|
+
def initialize(source:, destination:, tasks:, args:, opts:)
|
8
|
+
@source = source
|
9
|
+
@destination = destination
|
10
|
+
@tasks = tasks
|
11
|
+
@args = args
|
12
|
+
@opts = opts
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform
|
16
|
+
if opts[:preserve]
|
17
|
+
raise Error, "Cannot use --preserve with --schema-first or --schema-only"
|
18
|
+
end
|
19
|
+
|
20
|
+
show_spinner = output.tty? && !opts[:debug]
|
21
|
+
|
22
|
+
if show_spinner
|
23
|
+
spinner = TTY::Spinner.new(":spinner Syncing schema", format: :dots)
|
24
|
+
spinner.auto_spin
|
25
|
+
end
|
26
|
+
|
27
|
+
# if spinner, capture lines to show on error
|
28
|
+
lines = []
|
29
|
+
success =
|
30
|
+
run_command("#{dump_command} | #{restore_command}") do |line|
|
31
|
+
if show_spinner
|
32
|
+
lines << line
|
33
|
+
else
|
34
|
+
log line
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if show_spinner
|
39
|
+
if success
|
40
|
+
spinner.success
|
41
|
+
else
|
42
|
+
spinner.error
|
43
|
+
log lines.join
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
raise Error, "Schema sync returned non-zero exit code" unless success
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def run_command(command)
|
53
|
+
Open3.popen2e(command) do |stdin, stdout, wait_thr|
|
54
|
+
stdout.each do |line|
|
55
|
+
yield line
|
56
|
+
end
|
57
|
+
wait_thr.value.success?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def pg_restore_version
|
62
|
+
`pg_restore --version`.lines[0].chomp.split(" ")[-1].split(/[^\d.]/)[0]
|
63
|
+
rescue Errno::ENOENT
|
64
|
+
raise Error, "pg_restore not found"
|
65
|
+
end
|
66
|
+
|
67
|
+
def dump_command
|
68
|
+
tables =
|
69
|
+
if !opts[:all_schemas] || opts[:tables] || opts[:groups] || args[0] || opts[:exclude] || opts[:schemas]
|
70
|
+
@tasks.map { |task| "-t #{Shellwords.escape(task.quoted_table)}" }
|
71
|
+
else
|
72
|
+
[]
|
73
|
+
end
|
74
|
+
|
75
|
+
"pg_dump -Fc --verbose --schema-only --no-owner --no-acl #{tables.join(" ")} -d #{@source.url}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def restore_command
|
79
|
+
if_exists = Gem::Version.new(pg_restore_version) >= Gem::Version.new("9.4.0")
|
80
|
+
"pg_restore --verbose --no-owner --no-acl --clean #{if_exists ? "--if-exists" : nil} -d #{@destination.url}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# minimal class to keep schema and sequence name separate
|
2
|
+
module PgSync
|
3
|
+
class Sequence
|
4
|
+
attr_reader :schema, :name, :column
|
5
|
+
|
6
|
+
def initialize(schema, name, column:)
|
7
|
+
@schema = schema
|
8
|
+
@name = name
|
9
|
+
@column = column
|
10
|
+
end
|
11
|
+
|
12
|
+
def full_name
|
13
|
+
"#{schema}.#{name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def eql?(other)
|
17
|
+
other.schema == schema && other.name == name
|
18
|
+
end
|
19
|
+
|
20
|
+
# override hash when overriding eql?
|
21
|
+
def hash
|
22
|
+
[schema, name].hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
full_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/pgsync/sync.rb
CHANGED
@@ -2,21 +2,26 @@ module PgSync
|
|
2
2
|
class Sync
|
3
3
|
include Utils
|
4
4
|
|
5
|
-
def
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
def initialize(arguments, options)
|
6
|
+
@arguments = arguments
|
7
|
+
@options = options
|
8
|
+
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
opts[opt] ||= config[opt.to_s]
|
13
|
-
end
|
10
|
+
def perform
|
11
|
+
started_at = Time.now
|
14
12
|
|
15
|
-
|
16
|
-
|
13
|
+
args = @arguments
|
14
|
+
opts = @options
|
17
15
|
|
18
|
-
#
|
19
|
-
|
16
|
+
# only resolve commands from config, not CLI arguments
|
17
|
+
[:to, :from].each do |opt|
|
18
|
+
opts[opt] ||= resolve_source(config[opt.to_s])
|
19
|
+
end
|
20
|
+
|
21
|
+
# merge other config
|
22
|
+
[:to_safe, :exclude, :schemas].each do |opt|
|
23
|
+
opts[opt] ||= config[opt.to_s]
|
24
|
+
end
|
20
25
|
|
21
26
|
if args.size > 2
|
22
27
|
raise Error, "Usage:\n pgsync [options]"
|
@@ -32,89 +37,41 @@ module PgSync
|
|
32
37
|
print_description("From", source)
|
33
38
|
print_description("To", destination)
|
34
39
|
|
35
|
-
|
36
|
-
|
40
|
+
if (opts[:preserve] || opts[:overwrite]) && destination.server_version_num < 90500
|
41
|
+
raise Error, "Postgres 9.5+ is required for --preserve and --overwrite"
|
42
|
+
end
|
37
43
|
|
38
|
-
|
39
|
-
|
44
|
+
resolver = TaskResolver.new(args: args, opts: opts, source: source, destination: destination, config: config, first_schema: first_schema)
|
45
|
+
tasks =
|
46
|
+
resolver.tasks.map do |task|
|
47
|
+
Task.new(source: source, destination: destination, config: config, table: task[:table], opts: opts.merge(sql: task[:sql]))
|
48
|
+
end
|
40
49
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
end
|
50
|
+
if opts[:in_batches] && tasks.size > 1
|
51
|
+
raise Error, "Cannot use --in-batches with multiple tables"
|
52
|
+
end
|
53
|
+
|
54
|
+
confirm_tables_exist(source, tasks, "source")
|
47
55
|
|
48
|
-
|
56
|
+
if opts[:list]
|
57
|
+
confirm_tables_exist(destination, tasks, "destination")
|
58
|
+
tasks.each do |task|
|
59
|
+
log task_name(task)
|
60
|
+
end
|
49
61
|
else
|
50
62
|
if opts[:schema_first] || opts[:schema_only]
|
51
|
-
|
52
|
-
raise Error, "Cannot use --preserve with --schema-first or --schema-only"
|
53
|
-
end
|
54
|
-
|
55
|
-
log "* Dumping schema"
|
56
|
-
schema_tables = tables if !opts[:all_schemas] || opts[:tables] || opts[:groups] || args[0] || opts[:exclude]
|
57
|
-
sync_schema(source, destination, schema_tables)
|
63
|
+
SchemaSync.new(source: source, destination: destination, tasks: tasks, args: args, opts: opts).perform
|
58
64
|
end
|
59
65
|
|
60
66
|
unless opts[:schema_only]
|
61
|
-
|
62
|
-
|
63
|
-
in_parallel(tables, first_schema: source.search_path.find { |sp| sp != "pg_catalog" }) do |table, table_opts, source, destination|
|
64
|
-
TableSync.new(source: source, destination: destination).sync(config, table, opts.merge(table_opts))
|
65
|
-
end
|
67
|
+
TableSync.new(source: source, destination: destination, tasks: tasks, opts: opts, resolver: resolver).perform
|
66
68
|
end
|
67
69
|
|
68
|
-
log_completed(
|
70
|
+
log_completed(started_at)
|
69
71
|
end
|
70
72
|
end
|
71
73
|
|
72
|
-
|
73
|
-
tables.keys.each do |table|
|
74
|
-
unless data_source.table_exists?(table)
|
75
|
-
raise Error, "Table does not exist in #{description}: #{table}"
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def map_deprecations(args, opts)
|
81
|
-
command = args[0]
|
82
|
-
|
83
|
-
case command
|
84
|
-
when "schema"
|
85
|
-
args.shift
|
86
|
-
opts[:schema_only] = true
|
87
|
-
deprecated "Use `psync --schema-only` instead"
|
88
|
-
when "tables"
|
89
|
-
args.shift
|
90
|
-
opts[:tables] = args.shift
|
91
|
-
deprecated "Use `pgsync #{opts[:tables]}` instead"
|
92
|
-
when "groups"
|
93
|
-
args.shift
|
94
|
-
opts[:groups] = args.shift
|
95
|
-
deprecated "Use `pgsync #{opts[:groups]}` instead"
|
96
|
-
end
|
97
|
-
|
98
|
-
if opts[:where]
|
99
|
-
opts[:sql] ||= String.new
|
100
|
-
opts[:sql] << " WHERE #{opts[:where]}"
|
101
|
-
deprecated "Use `\"WHERE #{opts[:where]}\"` instead"
|
102
|
-
end
|
103
|
-
|
104
|
-
if opts[:limit]
|
105
|
-
opts[:sql] ||= String.new
|
106
|
-
opts[:sql] << " LIMIT #{opts[:limit]}"
|
107
|
-
deprecated "Use `\"LIMIT #{opts[:limit]}\"` instead"
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def sync_schema(source, destination, tables = nil)
|
112
|
-
dump_command = source.dump_command(tables)
|
113
|
-
restore_command = destination.restore_command
|
114
|
-
unless system("#{dump_command} | #{restore_command}")
|
115
|
-
raise Error, "Schema sync returned non-zero exit code"
|
116
|
-
end
|
117
|
-
end
|
74
|
+
private
|
118
75
|
|
119
76
|
def config
|
120
77
|
@config ||= begin
|
@@ -124,6 +81,8 @@ module PgSync
|
|
124
81
|
YAML.load_file(file) || {}
|
125
82
|
rescue Psych::SyntaxError => e
|
126
83
|
raise Error, e.message
|
84
|
+
rescue Errno::ENOENT
|
85
|
+
raise Error, "Config file not found: #{file}"
|
127
86
|
end
|
128
87
|
else
|
129
88
|
{}
|
@@ -131,147 +90,71 @@ module PgSync
|
|
131
90
|
end
|
132
91
|
end
|
133
92
|
|
134
|
-
def
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
spinners = TTY::Spinner::Multi.new(format: :dots, output: output)
|
141
|
-
item_spinners = {}
|
142
|
-
|
143
|
-
start = lambda do |item, i|
|
144
|
-
table, opts = item
|
145
|
-
message = String.new(":spinner ")
|
146
|
-
message << table.sub("#{first_schema}.", "")
|
147
|
-
message << " #{opts[:sql]}" if opts[:sql]
|
148
|
-
spinner = spinners.register(message)
|
149
|
-
if @options[:in_batches]
|
150
|
-
# log instead of spin for non-tty
|
151
|
-
log message.sub(":spinner", "⠋")
|
152
|
-
else
|
153
|
-
spinner.auto_spin
|
154
|
-
end
|
155
|
-
item_spinners[item] = spinner
|
156
|
-
end
|
157
|
-
|
158
|
-
failed_tables = []
|
159
|
-
|
160
|
-
finish = lambda do |item, i, result|
|
161
|
-
spinner = item_spinners[item]
|
162
|
-
table_name = item.first.sub("#{first_schema}.", "")
|
163
|
-
|
164
|
-
if result[:status] == "success"
|
165
|
-
spinner.success(display_message(result))
|
166
|
-
else
|
167
|
-
# TODO add option to fail fast
|
168
|
-
spinner.error(display_message(result))
|
169
|
-
failed_tables << table_name
|
170
|
-
fail_sync(failed_tables) if @options[:fail_fast]
|
171
|
-
end
|
172
|
-
|
173
|
-
unless spinner.send(:tty?)
|
174
|
-
status = result[:status] == "success" ? "✔" : "✖"
|
175
|
-
log [status, table_name, item.last[:sql], display_message(result)].compact.join(" ")
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
options = {start: start, finish: finish}
|
180
|
-
|
181
|
-
jobs = @options[:jobs]
|
182
|
-
if @options[:debug] || @options[:in_batches] || @options[:defer_constraints]
|
183
|
-
warn "--jobs ignored" if jobs
|
184
|
-
jobs = 0
|
185
|
-
end
|
186
|
-
|
187
|
-
if windows?
|
188
|
-
options[:in_threads] = jobs || 4
|
189
|
-
else
|
190
|
-
options[:in_processes] = jobs if jobs
|
191
|
-
end
|
192
|
-
|
193
|
-
maybe_defer_constraints do
|
194
|
-
# could try to use `raise Parallel::Kill` to fail faster with --fail-fast
|
195
|
-
# see `fast_faster` branch
|
196
|
-
# however, need to make sure connections are cleaned up properly
|
197
|
-
Parallel.each(tables, **options) do |table, table_opts|
|
198
|
-
# must reconnect for new thread or process
|
199
|
-
# TODO only reconnect first time
|
200
|
-
unless options[:in_processes] == 0
|
201
|
-
source.reconnect
|
202
|
-
destination.reconnect
|
203
|
-
end
|
204
|
-
|
205
|
-
# TODO warn if there are non-deferrable constraints on the table
|
206
|
-
|
207
|
-
yield table, table_opts, source, destination
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
fail_sync(failed_tables) if failed_tables.any?
|
212
|
-
end
|
213
|
-
|
214
|
-
def maybe_defer_constraints
|
215
|
-
if @options[:defer_constraints]
|
216
|
-
destination.transaction do
|
217
|
-
destination.execute("SET CONSTRAINTS ALL DEFERRED")
|
218
|
-
|
219
|
-
# create a transaction on the source
|
220
|
-
# to ensure we get a consistent snapshot
|
221
|
-
source.transaction do
|
222
|
-
yield
|
223
|
-
end
|
224
|
-
end
|
93
|
+
def config_file
|
94
|
+
if @options[:config]
|
95
|
+
@options[:config]
|
96
|
+
elsif @options[:db]
|
97
|
+
file = db_config_file(@options[:db])
|
98
|
+
search_tree(file) || file
|
225
99
|
else
|
226
|
-
|
100
|
+
search_tree(".pgsync.yml")
|
227
101
|
end
|
228
102
|
end
|
229
103
|
|
230
|
-
def
|
231
|
-
|
232
|
-
end
|
233
|
-
|
234
|
-
def display_message(result)
|
235
|
-
messages = []
|
236
|
-
messages << "- #{result[:time]}s" if result[:time]
|
237
|
-
messages << "(#{result[:message].lines.first.to_s.strip})" if result[:message]
|
238
|
-
messages.join(" ")
|
239
|
-
end
|
104
|
+
def search_tree(file)
|
105
|
+
return file if File.exist?(file)
|
240
106
|
|
241
|
-
|
242
|
-
|
243
|
-
|
107
|
+
path = Dir.pwd
|
108
|
+
# prevent infinite loop
|
109
|
+
20.times do
|
110
|
+
absolute_file = File.join(path, file)
|
111
|
+
break absolute_file if File.exist?(absolute_file)
|
112
|
+
path = File.dirname(path)
|
113
|
+
break if path == "/"
|
244
114
|
end
|
245
115
|
end
|
246
116
|
|
247
|
-
def
|
248
|
-
|
117
|
+
def print_description(prefix, source)
|
118
|
+
location = " on #{source.host}:#{source.port}" if source.host
|
119
|
+
log "#{prefix}: #{source.dbname}#{location}"
|
249
120
|
end
|
250
121
|
|
251
|
-
def log_completed(
|
252
|
-
time = Time.now -
|
122
|
+
def log_completed(started_at)
|
123
|
+
time = Time.now - started_at
|
253
124
|
message = "Completed in #{time.round(1)}s"
|
254
125
|
log colorize(message, :green)
|
255
126
|
end
|
256
127
|
|
257
|
-
def windows?
|
258
|
-
Gem.win_platform?
|
259
|
-
end
|
260
|
-
|
261
128
|
def source
|
262
|
-
@source ||= data_source(@options[:from])
|
129
|
+
@source ||= data_source(@options[:from], "from")
|
263
130
|
end
|
264
131
|
|
265
132
|
def destination
|
266
|
-
@destination ||= data_source(@options[:to])
|
133
|
+
@destination ||= data_source(@options[:to], "to")
|
267
134
|
end
|
268
135
|
|
269
|
-
def data_source(url)
|
270
|
-
ds = DataSource.new(url)
|
136
|
+
def data_source(url, name)
|
137
|
+
ds = DataSource.new(url, name: name, debug: @options[:debug])
|
271
138
|
ObjectSpace.define_finalizer(self, self.class.finalize(ds))
|
272
139
|
ds
|
273
140
|
end
|
274
141
|
|
142
|
+
# ideally aliases would work, but haven't found a nice way to do this
|
143
|
+
def resolve_source(source)
|
144
|
+
if source
|
145
|
+
source = source.dup
|
146
|
+
source.gsub!(/\$\([^)]+\)/) do |m|
|
147
|
+
command = m[2..-2]
|
148
|
+
result = `#{command}`.chomp
|
149
|
+
unless $?.success?
|
150
|
+
raise Error, "Command exited with non-zero status:\n#{command}"
|
151
|
+
end
|
152
|
+
result
|
153
|
+
end
|
154
|
+
end
|
155
|
+
source
|
156
|
+
end
|
157
|
+
|
275
158
|
def self.finalize(ds)
|
276
159
|
# must use proc instead of stabby lambda
|
277
160
|
proc { ds.close }
|