pgsync 0.5.2 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pgsync might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/LICENSE.txt +1 -1
- data/README.md +121 -38
- data/config.yml +4 -0
- data/exe/pgsync +0 -4
- data/lib/pgsync.rb +5 -1
- data/lib/pgsync/client.rb +54 -52
- data/lib/pgsync/data_source.rb +78 -80
- data/lib/pgsync/init.rb +48 -10
- data/lib/pgsync/schema_sync.rb +83 -0
- data/lib/pgsync/sync.rb +98 -175
- data/lib/pgsync/table.rb +28 -0
- data/lib/pgsync/table_sync.rb +167 -219
- data/lib/pgsync/task.rb +315 -0
- data/lib/pgsync/task_resolver.rb +235 -0
- data/lib/pgsync/utils.rb +64 -24
- data/lib/pgsync/version.rb +1 -1
- metadata +8 -5
- data/lib/pgsync/table_list.rb +0 -143
data/lib/pgsync/data_source.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
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
|
-
@timeout = timeout
|
7
|
+
def initialize(url)
|
8
|
+
@url = url
|
8
9
|
end
|
9
10
|
|
10
11
|
def exists?
|
@@ -30,8 +31,18 @@ module PgSync
|
|
30
31
|
# gets visible tables
|
31
32
|
def tables
|
32
33
|
@tables ||= begin
|
33
|
-
query =
|
34
|
-
|
34
|
+
query = <<~SQL
|
35
|
+
SELECT
|
36
|
+
table_schema AS schema,
|
37
|
+
table_name AS table
|
38
|
+
FROM
|
39
|
+
information_schema.tables
|
40
|
+
WHERE
|
41
|
+
table_type = 'BASE TABLE' AND
|
42
|
+
table_schema NOT IN ('information_schema', 'pg_catalog')
|
43
|
+
ORDER BY 1, 2
|
44
|
+
SQL
|
45
|
+
execute(query).map { |row| Table.new(row["schema"], row["table"]) }
|
35
46
|
end
|
36
47
|
end
|
37
48
|
|
@@ -39,25 +50,21 @@ module PgSync
|
|
39
50
|
table_set.include?(table)
|
40
51
|
end
|
41
52
|
|
42
|
-
def columns(table)
|
43
|
-
query = "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2"
|
44
|
-
execute(query, table.split(".", 2)).map { |row| row["column_name"] }
|
45
|
-
end
|
46
|
-
|
47
53
|
def sequences(table, columns)
|
48
|
-
execute("SELECT #{columns.map { |f| "pg_get_serial_sequence(#{escape("#{quote_ident_full(table)}")}, #{escape(f)}) AS #{quote_ident(f)}" }.join(", ")}")
|
54
|
+
execute("SELECT #{columns.map { |f| "pg_get_serial_sequence(#{escape("#{quote_ident_full(table)}")}, #{escape(f)}) AS #{quote_ident(f)}" }.join(", ")}").first.values.compact
|
49
55
|
end
|
50
56
|
|
51
57
|
def max_id(table, primary_key, sql_clause = nil)
|
52
|
-
execute("SELECT MAX(#{quote_ident(primary_key)}) FROM #{quote_ident_full(table)}#{sql_clause}")[
|
58
|
+
execute("SELECT MAX(#{quote_ident(primary_key)}) FROM #{quote_ident_full(table)}#{sql_clause}").first["max"].to_i
|
53
59
|
end
|
54
60
|
|
55
61
|
def min_id(table, primary_key, sql_clause = nil)
|
56
|
-
execute("SELECT MIN(#{quote_ident(primary_key)}) FROM #{quote_ident_full(table)}#{sql_clause}")[
|
62
|
+
execute("SELECT MIN(#{quote_ident(primary_key)}) FROM #{quote_ident_full(table)}#{sql_clause}").first["min"].to_i
|
57
63
|
end
|
58
64
|
|
65
|
+
# this value comes from pg_get_serial_sequence which is already quoted
|
59
66
|
def last_value(seq)
|
60
|
-
execute("
|
67
|
+
execute("SELECT last_value FROM #{seq}").first["last_value"]
|
61
68
|
end
|
62
69
|
|
63
70
|
def truncate(table)
|
@@ -65,38 +72,57 @@ module PgSync
|
|
65
72
|
end
|
66
73
|
|
67
74
|
# https://stackoverflow.com/a/20537829
|
75
|
+
# TODO can simplify with array_position in Postgres 9.5+
|
68
76
|
def primary_key(table)
|
69
|
-
query =
|
77
|
+
query = <<~SQL
|
70
78
|
SELECT
|
71
79
|
pg_attribute.attname,
|
72
|
-
format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
|
80
|
+
format_type(pg_attribute.atttypid, pg_attribute.atttypmod),
|
81
|
+
pg_attribute.attnum,
|
82
|
+
pg_index.indkey
|
73
83
|
FROM
|
74
84
|
pg_index, pg_class, pg_attribute, pg_namespace
|
75
85
|
WHERE
|
76
|
-
pg_class.oid = $2::regclass AND
|
77
|
-
indrelid = pg_class.oid AND
|
78
86
|
nspname = $1 AND
|
87
|
+
relname = $2 AND
|
88
|
+
indrelid = pg_class.oid AND
|
79
89
|
pg_class.relnamespace = pg_namespace.oid AND
|
80
90
|
pg_attribute.attrelid = pg_class.oid AND
|
81
91
|
pg_attribute.attnum = any(pg_index.indkey) AND
|
82
92
|
indisprimary
|
83
93
|
SQL
|
84
|
-
|
85
|
-
|
94
|
+
rows = execute(query, [table.schema, table.name])
|
95
|
+
rows.sort_by { |r| r["indkey"].split(" ").index(r["attnum"]) }.map { |r| r["attname"] }
|
96
|
+
end
|
97
|
+
|
98
|
+
def triggers(table)
|
99
|
+
query = <<~SQL
|
100
|
+
SELECT
|
101
|
+
tgname AS name,
|
102
|
+
tgisinternal AS internal,
|
103
|
+
tgenabled != 'D' AS enabled,
|
104
|
+
tgconstraint != 0 AS integrity
|
105
|
+
FROM
|
106
|
+
pg_trigger
|
107
|
+
WHERE
|
108
|
+
pg_trigger.tgrelid = $1::regclass
|
109
|
+
SQL
|
110
|
+
execute(query, [quote_ident_full(table)])
|
86
111
|
end
|
87
112
|
|
88
113
|
def conn
|
89
114
|
@conn ||= begin
|
90
115
|
begin
|
91
|
-
ENV["PGCONNECT_TIMEOUT"] ||=
|
116
|
+
ENV["PGCONNECT_TIMEOUT"] ||= "3"
|
92
117
|
if @url =~ /\Apostgres(ql)?:\/\//
|
93
118
|
config = @url
|
94
119
|
else
|
95
120
|
config = {dbname: @url}
|
96
121
|
end
|
122
|
+
@concurrent_id = concurrent_id
|
97
123
|
PG::Connection.new(config)
|
98
124
|
rescue URI::InvalidURIError
|
99
|
-
raise Error, "Invalid connection string"
|
125
|
+
raise Error, "Invalid connection string. Make sure it works with `psql`"
|
100
126
|
end
|
101
127
|
end
|
102
128
|
end
|
@@ -108,84 +134,56 @@ module PgSync
|
|
108
134
|
end
|
109
135
|
end
|
110
136
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
end
|
115
|
-
|
116
|
-
def restore_command
|
117
|
-
if_exists = Gem::Version.new(pg_restore_version) >= Gem::Version.new("9.4.0")
|
118
|
-
"pg_restore --verbose --no-owner --no-acl --clean #{if_exists ? "--if-exists" : nil} -d #{@url}"
|
119
|
-
end
|
120
|
-
|
121
|
-
def fully_resolve_tables(tables)
|
122
|
-
no_schema_tables = {}
|
123
|
-
search_path_index = Hash[search_path.map.with_index.to_a]
|
124
|
-
self.tables.group_by { |t| t.split(".", 2)[-1] }.each do |group, t2|
|
125
|
-
no_schema_tables[group] = t2.sort_by { |t| [search_path_index[t.split(".", 2)[0]] || 1000000, t] }[0]
|
126
|
-
end
|
127
|
-
|
128
|
-
Hash[tables.map { |k, v| [no_schema_tables[k] || k, v] }]
|
137
|
+
# reconnect for new thread or process
|
138
|
+
def reconnect_if_needed
|
139
|
+
reconnect if @concurrent_id != concurrent_id
|
129
140
|
end
|
130
141
|
|
131
142
|
def search_path
|
132
|
-
@search_path ||= execute("SELECT current_schemas(true)")[
|
143
|
+
@search_path ||= execute("SELECT unnest(current_schemas(true)) AS schema").map { |r| r["schema"] }
|
133
144
|
end
|
134
145
|
|
135
|
-
|
136
|
-
|
137
|
-
def pg_restore_version
|
138
|
-
`pg_restore --version`.lines[0].chomp.split(" ")[-1].split(/[^\d.]/)[0]
|
139
|
-
rescue Errno::ENOENT
|
140
|
-
raise Error, "pg_restore not found"
|
146
|
+
def server_version_num
|
147
|
+
@server_version_num ||= execute("SHOW server_version_num").first["server_version_num"].to_i
|
141
148
|
end
|
142
149
|
|
143
|
-
def
|
144
|
-
|
145
|
-
end
|
146
|
-
|
147
|
-
def conninfo
|
148
|
-
@conninfo ||= conn.conninfo_hash
|
150
|
+
def execute(query, params = [])
|
151
|
+
conn.exec_params(query, params).to_a
|
149
152
|
end
|
150
153
|
|
151
|
-
def
|
152
|
-
|
154
|
+
def transaction
|
155
|
+
if conn.transaction_status == 0
|
156
|
+
# not currently in transaction
|
157
|
+
conn.transaction do
|
158
|
+
yield
|
159
|
+
end
|
160
|
+
else
|
161
|
+
yield
|
162
|
+
end
|
153
163
|
end
|
154
164
|
|
155
|
-
|
156
|
-
conn.exec_params(query, params).to_a
|
157
|
-
end
|
165
|
+
private
|
158
166
|
|
159
|
-
def
|
160
|
-
|
167
|
+
def concurrent_id
|
168
|
+
[Process.pid, Thread.current.object_id]
|
161
169
|
end
|
162
170
|
|
163
|
-
def
|
164
|
-
|
165
|
-
|
166
|
-
else
|
167
|
-
value
|
168
|
-
end
|
171
|
+
def reconnect
|
172
|
+
@conn.reset
|
173
|
+
@concurrent_id = concurrent_id
|
169
174
|
end
|
170
175
|
|
171
|
-
|
172
|
-
|
173
|
-
s.gsub(/\\/, '\&\&').gsub(/'/, "''")
|
176
|
+
def table_set
|
177
|
+
@table_set ||= Set.new(tables)
|
174
178
|
end
|
175
179
|
|
176
|
-
def
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
command = m[2..-2]
|
181
|
-
result = `#{command}`.chomp
|
182
|
-
unless $?.success?
|
183
|
-
raise Error, "Command exited with non-zero status:\n#{command}"
|
184
|
-
end
|
185
|
-
result
|
180
|
+
def conninfo
|
181
|
+
@conninfo ||= begin
|
182
|
+
unless conn.respond_to?(:conninfo_hash)
|
183
|
+
raise Error, "libpq is too old. Upgrade it and run `gem install pg`"
|
186
184
|
end
|
185
|
+
conn.conninfo_hash
|
187
186
|
end
|
188
|
-
source
|
189
187
|
end
|
190
188
|
end
|
191
189
|
end
|
data/lib/pgsync/init.rb
CHANGED
@@ -2,40 +2,78 @@ 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."
|
13
28
|
else
|
14
29
|
exclude =
|
15
|
-
if
|
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
|
23
49
|
# exclude:
|
24
|
-
# -
|
25
|
-
# -
|
50
|
+
# - table1
|
51
|
+
# - table2
|
26
52
|
EOS
|
27
53
|
end
|
28
54
|
|
29
55
|
# create file
|
30
56
|
contents = File.read(__dir__ + "/../../config.yml")
|
57
|
+
contents.sub!("$(some_command)", "$(heroku config:get DATABASE_URL)") if heroku?
|
31
58
|
File.write(file, contents % {exclude: exclude})
|
32
59
|
|
33
60
|
log "#{file} created. Add your database credentials."
|
34
61
|
end
|
35
62
|
end
|
36
63
|
|
37
|
-
|
38
|
-
|
64
|
+
def django?
|
65
|
+
(File.read("manage.py") =~ /django/i) rescue false
|
66
|
+
end
|
67
|
+
|
68
|
+
def heroku?
|
69
|
+
`git remote -v 2>&1`.include?("git.heroku.com") rescue false
|
70
|
+
end
|
71
|
+
|
72
|
+
def laravel?
|
73
|
+
File.exist?("artisan")
|
74
|
+
end
|
75
|
+
|
76
|
+
def rails?
|
39
77
|
File.exist?("bin/rails")
|
40
78
|
end
|
41
79
|
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
|
data/lib/pgsync/sync.rb
CHANGED
@@ -2,147 +2,87 @@ 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
|
-
|
13
|
-
|
10
|
+
def perform
|
11
|
+
started_at = Time.now
|
12
|
+
|
13
|
+
args = @arguments
|
14
|
+
opts = @options
|
14
15
|
|
15
|
-
#
|
16
|
-
|
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
|
17
20
|
|
18
|
-
#
|
19
|
-
|
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]"
|
23
28
|
end
|
24
29
|
|
25
|
-
source = DataSource.new(opts[:from])
|
26
30
|
raise Error, "No source" unless source.exists?
|
27
|
-
|
28
|
-
destination = DataSource.new(opts[:to])
|
29
31
|
raise Error, "No destination" unless destination.exists?
|
30
32
|
|
31
|
-
|
32
|
-
|
33
|
-
source.host
|
34
|
-
destination.host
|
35
|
-
|
36
|
-
unless opts[:to_safe] || destination.local?
|
37
|
-
raise Error, "Danger! Add `to_safe: true` to `.pgsync.yml` if the destination is not localhost or 127.0.0.1"
|
38
|
-
end
|
39
|
-
|
40
|
-
print_description("From", source)
|
41
|
-
print_description("To", destination)
|
42
|
-
ensure
|
43
|
-
source.close
|
44
|
-
destination.close
|
33
|
+
unless opts[:to_safe] || destination.local?
|
34
|
+
raise Error, "Danger! Add `to_safe: true` to `.pgsync.yml` if the destination is not localhost or 127.0.0.1"
|
45
35
|
end
|
46
36
|
|
47
|
-
|
48
|
-
|
49
|
-
tables = TableList.new(args, opts, source, config).tables
|
50
|
-
ensure
|
51
|
-
source.close
|
37
|
+
if (opts[:preserve] || opts[:overwrite]) && destination.server_version_num < 90500
|
38
|
+
raise Error, "Postgres 9.5+ is required for --preserve and --overwrite"
|
52
39
|
end
|
53
40
|
|
54
|
-
|
41
|
+
print_description("From", source)
|
42
|
+
print_description("To", destination)
|
55
43
|
|
56
|
-
|
57
|
-
|
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
|
58
49
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
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")
|
65
55
|
|
66
|
-
|
56
|
+
if opts[:list]
|
57
|
+
confirm_tables_exist(destination, tasks, "destination")
|
58
|
+
tasks.each do |task|
|
59
|
+
log task_name(task)
|
60
|
+
end
|
67
61
|
else
|
68
62
|
if opts[:schema_first] || opts[:schema_only]
|
69
|
-
|
70
|
-
raise Error, "Cannot use --preserve with --schema-first or --schema-only"
|
71
|
-
end
|
72
|
-
|
73
|
-
log "* Dumping schema"
|
74
|
-
schema_tables = tables if !opts[:all_schemas] || opts[:tables] || opts[:groups] || args[0] || opts[:exclude]
|
75
|
-
sync_schema(source, destination, schema_tables)
|
63
|
+
SchemaSync.new(source: source, destination: destination, tasks: tasks, args: args, opts: opts).perform
|
76
64
|
end
|
77
65
|
|
78
66
|
unless opts[:schema_only]
|
79
|
-
|
80
|
-
|
81
|
-
in_parallel(tables, first_schema: source.search_path.find { |sp| sp != "pg_catalog" }) do |table, table_opts|
|
82
|
-
TableSync.new.sync(config, table, opts.merge(table_opts), source.url, destination.url)
|
83
|
-
end
|
67
|
+
TableSync.new(source: source, destination: destination, tasks: tasks, opts: opts, resolver: resolver).perform
|
84
68
|
end
|
85
69
|
|
86
|
-
log_completed(
|
70
|
+
log_completed(started_at)
|
87
71
|
end
|
88
72
|
end
|
89
73
|
|
90
|
-
|
91
|
-
tables.keys.each do |table|
|
92
|
-
unless data_source.table_exists?(table)
|
93
|
-
raise Error, "Table does not exist in #{description}: #{table}"
|
94
|
-
end
|
95
|
-
end
|
96
|
-
ensure
|
97
|
-
data_source.close
|
98
|
-
end
|
99
|
-
|
100
|
-
def map_deprecations(args, opts)
|
101
|
-
command = args[0]
|
102
|
-
|
103
|
-
case command
|
104
|
-
when "schema"
|
105
|
-
args.shift
|
106
|
-
opts[:schema_only] = true
|
107
|
-
deprecated "Use `psync --schema-only` instead"
|
108
|
-
when "tables"
|
109
|
-
args.shift
|
110
|
-
opts[:tables] = args.shift
|
111
|
-
deprecated "Use `pgsync #{opts[:tables]}` instead"
|
112
|
-
when "groups"
|
113
|
-
args.shift
|
114
|
-
opts[:groups] = args.shift
|
115
|
-
deprecated "Use `pgsync #{opts[:groups]}` instead"
|
116
|
-
end
|
117
|
-
|
118
|
-
if opts[:where]
|
119
|
-
opts[:sql] ||= String.new
|
120
|
-
opts[:sql] << " WHERE #{opts[:where]}"
|
121
|
-
deprecated "Use `\"WHERE #{opts[:where]}\"` instead"
|
122
|
-
end
|
123
|
-
|
124
|
-
if opts[:limit]
|
125
|
-
opts[:sql] ||= String.new
|
126
|
-
opts[:sql] << " LIMIT #{opts[:limit]}"
|
127
|
-
deprecated "Use `\"LIMIT #{opts[:limit]}\"` instead"
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def sync_schema(source, destination, tables = nil)
|
132
|
-
dump_command = source.dump_command(tables)
|
133
|
-
restore_command = destination.restore_command
|
134
|
-
unless system("#{dump_command} | #{restore_command}")
|
135
|
-
raise Error, "Schema sync returned non-zero exit code"
|
136
|
-
end
|
137
|
-
end
|
74
|
+
private
|
138
75
|
|
139
76
|
def config
|
140
77
|
@config ||= begin
|
141
|
-
|
78
|
+
file = config_file
|
79
|
+
if file
|
142
80
|
begin
|
143
|
-
YAML.load_file(
|
81
|
+
YAML.load_file(file) || {}
|
144
82
|
rescue Psych::SyntaxError => e
|
145
83
|
raise Error, e.message
|
84
|
+
rescue Errno::ENOENT
|
85
|
+
raise Error, "Config file not found: #{file}"
|
146
86
|
end
|
147
87
|
else
|
148
88
|
{}
|
@@ -150,91 +90,74 @@ module PgSync
|
|
150
90
|
end
|
151
91
|
end
|
152
92
|
|
153
|
-
def
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
start = lambda do |item, i|
|
163
|
-
table, opts = item
|
164
|
-
message = String.new(":spinner ")
|
165
|
-
message << table.sub("#{first_schema}.", "")
|
166
|
-
# maybe output later
|
167
|
-
# message << " #{opts[:sql]}" if opts[:sql]
|
168
|
-
spinner = spinners.register(message)
|
169
|
-
spinner.auto_spin
|
170
|
-
item_spinners[item] = spinner
|
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
|
99
|
+
else
|
100
|
+
search_tree(".pgsync.yml")
|
171
101
|
end
|
102
|
+
end
|
172
103
|
|
173
|
-
|
174
|
-
|
175
|
-
finish = lambda do |item, i, result|
|
176
|
-
spinner = item_spinners[item]
|
177
|
-
table_name = item.first.sub("#{first_schema}.", "")
|
178
|
-
|
179
|
-
if result[:status] == "success"
|
180
|
-
spinner.success(display_message(result))
|
181
|
-
else
|
182
|
-
# TODO add option to fail fast
|
183
|
-
spinner.error(display_message(result))
|
184
|
-
failed_tables << table_name
|
185
|
-
fail_sync(failed_tables) if @options[:fail_fast]
|
186
|
-
end
|
187
|
-
|
188
|
-
unless spinner.send(:tty?)
|
189
|
-
status = result[:status] == "success" ? "✔" : "✖"
|
190
|
-
log [status, table_name, display_message(result)].compact.join(" ")
|
191
|
-
end
|
192
|
-
end
|
104
|
+
def search_tree(file)
|
105
|
+
return file if File.exist?(file)
|
193
106
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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 == "/"
|
199
114
|
end
|
115
|
+
end
|
200
116
|
|
201
|
-
|
202
|
-
#
|
203
|
-
#
|
204
|
-
Parallel.each(tables, **options, &block)
|
205
|
-
|
206
|
-
fail_sync(failed_tables) if failed_tables.any?
|
117
|
+
def print_description(prefix, source)
|
118
|
+
location = " on #{source.host}:#{source.port}" if source.host
|
119
|
+
log "#{prefix}: #{source.dbname}#{location}"
|
207
120
|
end
|
208
121
|
|
209
|
-
def
|
210
|
-
|
122
|
+
def log_completed(started_at)
|
123
|
+
time = Time.now - started_at
|
124
|
+
message = "Completed in #{time.round(1)}s"
|
125
|
+
log colorize(message, :green)
|
211
126
|
end
|
212
127
|
|
213
|
-
def
|
214
|
-
|
215
|
-
messages << "- #{result[:time]}s" if result[:time]
|
216
|
-
messages << "(#{result[:message].gsub("\n", " ").strip})" if result[:message]
|
217
|
-
messages.join(" ")
|
128
|
+
def source
|
129
|
+
@source ||= data_source(@options[:from])
|
218
130
|
end
|
219
131
|
|
220
|
-
def
|
221
|
-
|
222
|
-
log item
|
223
|
-
end
|
132
|
+
def destination
|
133
|
+
@destination ||= data_source(@options[:to])
|
224
134
|
end
|
225
135
|
|
226
|
-
def
|
227
|
-
|
136
|
+
def data_source(url)
|
137
|
+
ds = DataSource.new(url)
|
138
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(ds))
|
139
|
+
ds
|
228
140
|
end
|
229
141
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
234
156
|
end
|
235
157
|
|
236
|
-
def
|
237
|
-
|
158
|
+
def self.finalize(ds)
|
159
|
+
# must use proc instead of stabby lambda
|
160
|
+
proc { ds.close }
|
238
161
|
end
|
239
162
|
end
|
240
163
|
end
|