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.

@@ -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(source, timeout: 3)
6
- @url = resolve_url(source)
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 = "SELECT table_schema, table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_schema NOT IN ('information_schema', 'pg_catalog') ORDER BY 1, 2"
34
- execute(query).map { |row| "#{row["table_schema"]}.#{row["table_name"]}" }
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(", ")}")[0].values.compact
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}")[0]["max"].to_i
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}")[0]["min"].to_i
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("select last_value from #{seq}")[0]["last_value"]
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 = <<-SQL
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
- row = execute(query, [table.split(".", 2)[0], quote_ident_full(table)])[0]
85
- row && row["attname"]
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"] ||= @timeout.to_s
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
- def dump_command(tables)
112
- tables = tables ? tables.keys.map { |t| "-t #{Shellwords.escape(quote_ident_full(t))}" }.join(" ") : ""
113
- "pg_dump -Fc --verbose --schema-only --no-owner --no-acl #{tables} -d #{@url}"
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)")[0]["current_schemas"][1..-2].split(",")
143
+ @search_path ||= execute("SELECT unnest(current_schemas(true)) AS schema").map { |r| r["schema"] }
133
144
  end
134
145
 
135
- private
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 table_set
144
- @table_set ||= Set.new(tables)
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 quote_ident_full(ident)
152
- ident.split(".", 2).map { |v| quote_ident(v) }.join(".")
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
- def execute(query, params = [])
156
- conn.exec_params(query, params).to_a
157
- end
165
+ private
158
166
 
159
- def quote_ident(value)
160
- PG::Connection.quote_ident(value)
167
+ def concurrent_id
168
+ [Process.pid, Thread.current.object_id]
161
169
  end
162
170
 
163
- def escape(value)
164
- if value.is_a?(String)
165
- "'#{quote_string(value)}'"
166
- else
167
- value
168
- end
171
+ def reconnect
172
+ @conn.reset
173
+ @concurrent_id = concurrent_id
169
174
  end
170
175
 
171
- # activerecord
172
- def quote_string(s)
173
- s.gsub(/\\/, '\&\&').gsub(/'/, "''")
176
+ def table_set
177
+ @table_set ||= Set.new(tables)
174
178
  end
175
179
 
176
- def resolve_url(source)
177
- if source
178
- source = source.dup
179
- source.gsub!(/\$\([^)]+\)/) do |m|
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
@@ -2,40 +2,78 @@ module PgSync
2
2
  class Init
3
3
  include Utils
4
4
 
5
- def perform(opts)
6
- # needed for config_file method
7
- @options = opts.to_hash
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 = db_config_file(opts.arguments[0]) || config_file || ".pgsync.yml"
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 rails_app?
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
- # - schema_migrations
25
- # - ar_internal_metadata
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
- # TODO maybe check parent directories
38
- def rails_app?
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
@@ -2,147 +2,87 @@ module PgSync
2
2
  class Sync
3
3
  include Utils
4
4
 
5
- def perform(options)
6
- args = options.arguments
7
- opts = options.to_hash
8
- @options = opts
5
+ def initialize(arguments, options)
6
+ @arguments = arguments
7
+ @options = options
8
+ end
9
9
 
10
- # merge config
11
- [:to, :from, :to_safe, :exclude, :schemas].each do |opt|
12
- opts[opt] ||= config[opt.to_s]
13
- end
10
+ def perform
11
+ started_at = Time.now
12
+
13
+ args = @arguments
14
+ opts = @options
14
15
 
15
- # TODO remove deprecations
16
- map_deprecations(args, opts)
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
- # start
19
- start_time = Time.now
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
- begin
32
- # start connections
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
- tables = nil
48
- begin
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
- confirm_tables_exist(source, tables, "source")
41
+ print_description("From", source)
42
+ print_description("To", destination)
55
43
 
56
- if opts[:list]
57
- confirm_tables_exist(destination, tables, "destination")
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
- list_items =
60
- if args[0] == "groups"
61
- (config["groups"] || {}).keys
62
- else
63
- tables.keys
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
- pretty_list list_items
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
- if opts[:preserve]
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
- confirm_tables_exist(destination, tables, "destination")
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(start_time)
70
+ log_completed(started_at)
87
71
  end
88
72
  end
89
73
 
90
- def confirm_tables_exist(data_source, tables, description)
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
- if config_file
78
+ file = config_file
79
+ if file
142
80
  begin
143
- YAML.load_file(config_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 print_description(prefix, source)
154
- location = " on #{source.host}:#{source.port}" if source.host
155
- log "#{prefix}: #{source.dbname}#{location}"
156
- end
157
-
158
- def in_parallel(tables, first_schema:, &block)
159
- spinners = TTY::Spinner::Multi.new(format: :dots)
160
- item_spinners = {}
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
- failed_tables = []
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
- options = {start: start, finish: finish}
195
- if @options[:debug] || @options[:in_batches]
196
- options[:in_processes] = 0
197
- else
198
- options[:in_threads] = 4 if windows?
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
- # could try to use `raise Parallel::Kill` to fail faster with --fail-fast
202
- # see `fast_faster` branch
203
- # however, need to make sure connections are cleaned up properly
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 fail_sync(failed_tables)
210
- raise Error, "Sync failed for #{failed_tables.size} table#{failed_tables.size == 1 ? nil : "s"}: #{failed_tables.join(", ")}"
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 display_message(result)
214
- messages = []
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 pretty_list(items)
221
- items.each do |item|
222
- log item
223
- end
132
+ def destination
133
+ @destination ||= data_source(@options[:to])
224
134
  end
225
135
 
226
- def deprecated(message)
227
- log colorize("[DEPRECATED] #{message}", 33) # yellow
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
- def log_completed(start_time)
231
- time = Time.now - start_time
232
- message = "Completed in #{time.round(1)}s"
233
- log colorize(message, 32) # green
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 windows?
237
- Gem.win_platform?
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