pgcp 0.0.0

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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/bin/pgcp +9 -0
  3. data/lib/pgcp.rb +67 -0
  4. data/lib/postgres.rb +347 -0
  5. data/lib/transport.rb +138 -0
  6. metadata +92 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e0bf53cb613c6859915d664646ef18c6ef216767
4
+ data.tar.gz: b3e0ace6249b20249efba309f40e7385d1f4cd24
5
+ SHA512:
6
+ metadata.gz: 41ae7a1c3b5c39e29b698a402f7c336d77de4a2f69c2cf92c2512624c788d6e37f61300a66b570cc5148b34100cd77e8e136332df81a64bea1395501de992a43
7
+ data.tar.gz: 21534f4d98783c90a57d54bcbb978864711206dc24686f275c286de031a79acbd668e1ea01ce4799a5b884821f50f07c579fb1a99dc55f6c81d26bad5713bbc2
data/bin/pgcp ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'thor'
4
+ require 'active_support'
5
+ require 'active_support/core_ext'
6
+ require './lib/transport'
7
+ require './lib/pgcp'
8
+
9
+ PgcpRunner.start
data/lib/pgcp.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'logger'
2
+
3
+ class PgcpRunner < Thor
4
+ desc 'cp', 'Perform copies of tables between Postgres databases'
5
+ method_option :source, type: :string, aliases: '-s', desc: 'Source database', required: true
6
+ method_option :dest, type: :string, aliases: '-d', desc: 'Destination database', required: true
7
+ method_option :table, type: :string, aliases: '-t', desc: 'Table to be copied', required: true
8
+ method_option :config, type: :string, aliases: '-c', desc: 'Path to config file'
9
+ method_option :force_schema, type: :string, aliases: '-f', desc: 'Force destination schema'
10
+ method_option :log, type: :string, aliases: '-l', desc: 'Path to log file'
11
+
12
+ def cp
13
+ config = load_config_file(options['config'] || File.join(ENV['HOME'], '.pgcp.yml'))
14
+ if options['log']
15
+ Pgcp.log_file = options['log']
16
+ end
17
+
18
+ src = config['databases'][options['source']].symbolize_keys!
19
+ dest = config['databases'][options['dest']].symbolize_keys!
20
+
21
+ begin
22
+ tr = Transport.new(src, dest)
23
+ if options['table'].include? '*'
24
+ if (not options['table'].include? '.') or (options['table'].count('.') > 1)
25
+ Pgcp.logger.error 'Globbed tables must have schema name, e.g. public.test* is valid but test* is not.'
26
+ return
27
+ end
28
+
29
+ tr.copy_tables(options['table'], force_schema: options['force_schema'])
30
+ else
31
+ tr.copy_table(options['table'], nil, force_schema: options['force_schema'])
32
+ end
33
+ rescue Exception => e
34
+ Pgcp.logger.error(e.message)
35
+ return
36
+ end
37
+ end
38
+
39
+ default_task :cp
40
+
41
+ private
42
+ def load_config_file(path)
43
+ config = {}
44
+ if not path.nil? and File.exists?(path)
45
+ config = YAML::load_file(path)
46
+ end
47
+
48
+ config
49
+ end
50
+ end
51
+
52
+
53
+ class Pgcp
54
+ @@logger = nil
55
+
56
+ def self.logger
57
+ if not @@logger
58
+ @@logger = Logger.new STDOUT
59
+ end
60
+
61
+ @@logger
62
+ end
63
+
64
+ def self.log_file=(path)
65
+ @@logger = Logger.new(path)
66
+ end
67
+ end
data/lib/postgres.rb ADDED
@@ -0,0 +1,347 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'pg'
4
+
5
+ class Postgres
6
+ # Initialize Postgres instance
7
+ #
8
+ # @param [Hash] dbconfig Database config
9
+ def initialize(dbconfig)
10
+ @dbconfig = dbconfig
11
+ end
12
+
13
+ def exec(sql, val=[])
14
+ with_connection do |conn|
15
+ conn.exec sql, val
16
+
17
+ end
18
+ end
19
+
20
+ def list_tables(schema_name)
21
+ with_connection do |conn|
22
+ sql = <<-SQL.strip_heredoc
23
+ SELECT table_name
24
+ FROM information_schema.tables
25
+ WHERE table_type = 'BASE TABLE'
26
+ AND table_schema NOT IN ('pg_catalog', 'information_schema')
27
+ AND table_schema = '#{schema_name}'
28
+ ORDER BY 1
29
+ SQL
30
+
31
+ rs = conn.exec sql
32
+
33
+ rs.values.map(&:first)
34
+ end
35
+
36
+ end
37
+
38
+ def drop_table(schema_name, table_name)
39
+ with_connection do |conn|
40
+ if _table_exists?(conn, schema_name, table_name)
41
+ fq_table_name = conn.escape_string("#{schema_name}.#{table_name}")
42
+
43
+ sql = <<-SQL.strip_heredoc
44
+ DROP TABLE #{fq_table_name}
45
+ SQL
46
+
47
+ conn.exec sql
48
+ end
49
+ end
50
+ end
51
+
52
+ def create_table(schema_name, table_name, columns, options={})
53
+ with_connection do |conn|
54
+ if options[:temporary]
55
+ table_name = conn.escape_string(table_name)
56
+ create_sql = create_table_statement(conn, columns,
57
+ table_name,
58
+ options)
59
+ conn.transaction do
60
+ conn.exec create_sql
61
+ conn.exec "DROP TABLE IF EXISTS #{table_name}"
62
+ end
63
+
64
+ true
65
+ else
66
+ unless _table_exists?(conn, schema_name, table_name)
67
+ create_sql = create_table_statement(conn, columns,
68
+ "#{schema_name}.#{table_name}",
69
+ options)
70
+ conn.exec create_sql
71
+
72
+ true
73
+ end
74
+
75
+ false
76
+ end
77
+ end
78
+ end
79
+
80
+ def create_table_from_query(query, schema_name, table_name, columns, options={})
81
+ with_connection do |conn|
82
+ create_sql = create_table_statement(conn, columns,
83
+ "#{schema_name}.#{table_name}",
84
+ options)
85
+ conn.transaction do
86
+ conn.exec "DROP TABLE IF EXISTS #{schema_name}.#{table_name}"
87
+ conn.exec create_sql
88
+ conn.exec "INSERT INTO #{schema_name}.#{table_name}\n#{query}"
89
+ end
90
+ end
91
+ end
92
+
93
+ def hotswap_table(schema_name, src_table_name, dst_table_name)
94
+ with_connection do |conn|
95
+ conn.transaction do
96
+ schema_name = conn.escape_string(schema_name)
97
+ dst_table_name = conn.escape_string(dst_table_name)
98
+ conn.exec "DROP TABLE #{schema_name}.#{dst_table_name}" if _table_exists?(conn, schema_name, dst_table_name)
99
+ conn.exec "ALTER TABLE #{schema_name}.#{src_table_name} RENAME TO #{dst_table_name}"
100
+ #conn.exec "TRUNCATE TABLE #{schema_name}.#{dst_table_name}" if _table_exists?(conn, schema_name, dst_table_name)
101
+ #conn.exec "INSERT INTO #{schema_name}.#{dst_table_name}\n(SELECT * FROM #{schema_name}.#{src_table_name})"
102
+ #conn.exec "DROP TABLE #{schema_name}.#{src_table_name}"
103
+ end
104
+
105
+ end
106
+ end
107
+
108
+ def schema_names
109
+ with_connection do |conn|
110
+ sql = <<-SQL.strip_heredoc
111
+ SELECT schema_name
112
+ FROM information_schema.schemata
113
+ WHERE schema_name <> 'information_schema'
114
+ AND schema_name NOT LIKE 'pg_%'
115
+ SQL
116
+
117
+ rs = conn.exec sql
118
+ rs.values.map(&:first)
119
+ end
120
+ end
121
+
122
+ def copy_from_file(schema_name, table_name, csv_file, options={})
123
+ with_connection do |conn|
124
+ schema_name = conn.escape_string(schema_name)
125
+ table_name = conn.escape_string(table_name)
126
+
127
+ conn.copy_data "COPY #{schema_name}.#{table_name} FROM STDIN CSV #{options[:header]?'HEADER':''}" do
128
+ buf = ''
129
+ while csv_file.read(256, buf)
130
+ conn.put_copy_data(buf)
131
+ end
132
+ end
133
+
134
+ end
135
+ end
136
+
137
+ def column_definitions(schema_name, table_name)
138
+ with_connection do |conn|
139
+ sql = <<-SQL.strip_heredoc
140
+ SELECT
141
+ c.relname, a.attname AS column_name,
142
+ pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
143
+ case
144
+ when a.attnotnull
145
+ then 'NOT NULL'
146
+ else 'NULL'
147
+ END as not_null
148
+ FROM pg_class c,
149
+ pg_attribute a,
150
+ pg_type t,
151
+ pg_namespace n
152
+ WHERE c.relname = '#{table_name}'
153
+ AND n.nspname = '#{schema_name}'
154
+ AND a.attnum > 0
155
+ AND a.attrelid = c.oid
156
+ AND a.atttypid = t.oid
157
+ AND c.relnamespace = n.oid
158
+ ORDER BY a.attnum
159
+ SQL
160
+
161
+ rs = conn.exec sql
162
+
163
+ rs.values.map do |col|
164
+ {name: col[1], type: col[2], null: col[3]}
165
+ end
166
+ end
167
+ end
168
+
169
+ def index_names(schema_name, table_name)
170
+ with_connection do |conn|
171
+ sql = <<-SQL.strip_heredoc
172
+ SELECT
173
+ C.relname AS "index_name"
174
+ FROM pg_catalog.pg_class C,
175
+ pg_catalog.pg_namespace N,
176
+ pg_catalog.pg_index I,
177
+ pg_catalog.pg_class C2
178
+ WHERE C.relkind IN ( 'i', '' )
179
+ AND N.oid = C.relnamespace
180
+ AND N.nspname = '#{schema_name}'
181
+ AND I.indexrelid = C.oid
182
+ AND C2.oid = I.indrelid
183
+ AND C2.relname = '#{table_name}';
184
+ SQL
185
+
186
+ rs = conn.exec sql
187
+
188
+ rs.values.map(&:first)
189
+ end
190
+ end
191
+
192
+ def get_indexes(schema_name, table_name)
193
+ idx_names = self.index_names(schema_name, table_name)
194
+
195
+ with_connection do |conn|
196
+ idx_names.map do |name|
197
+ index_info = index_info(conn, name, schema_name)
198
+ index_info['name'] = name
199
+ index_info['columns'] = index_column_names conn, index_info['oid']
200
+
201
+ index_info
202
+ end
203
+ end
204
+ end
205
+
206
+ def index_info(conn, index_name, schema_name)
207
+ sql = <<-SQL.strip_heredoc
208
+ SELECT
209
+ C.oid,
210
+ I.indisunique AS "unique",
211
+ I.indisprimary AS "primary",
212
+ pg_get_expr(I.indpred, I.indrelid) AS "where"
213
+ FROM pg_catalog.pg_class C,
214
+ pg_catalog.pg_namespace N,
215
+ pg_catalog.pg_index I
216
+ WHERE C.relname = '#{index_name}'
217
+ AND C.relnamespace = N.oid
218
+ AND I.indexrelid = C.oid
219
+ AND N.nspname = '#{schema_name}';
220
+ SQL
221
+
222
+ rs = conn.exec sql
223
+ rs[0].tap do |info|
224
+ info['unique'] = info['unique'] != 'f'
225
+ info['primary'] = info['primary'] != 'f'
226
+ info['where'] = info['where'][1..-2] if info['where'].present?
227
+ end
228
+ end
229
+
230
+ def index_column_names conn, oid
231
+ sql = <<-SQL.strip_heredoc
232
+ SELECT
233
+ pg_catalog.pg_get_indexdef(A.attrelid, A.attnum, TRUE) AS "column_name"
234
+ FROM pg_catalog.pg_attribute A
235
+ WHERE A.attrelid = $1
236
+ AND A.attnum > 0
237
+ AND NOT A.attisdropped
238
+ ORDER BY A.attnum;
239
+ SQL
240
+ conn.exec(sql, [oid]).map { |row| row['column_name'] }
241
+ end
242
+
243
+ def create_indexes(schema_name, table_name, indexes)
244
+ with_connection do |conn|
245
+ indexes.each do |index|
246
+ if index['primary']
247
+ sql = <<-SQL.strip_heredoc
248
+ ALTER TABLE #{schema_name}.#{table_name} ADD PRIMARY KEY (#{index['columns'][0]})
249
+ SQL
250
+ else
251
+ sql = <<-SQL.strip_heredoc
252
+ CREATE #{index['unique'] ? 'UNIQUE': ''} INDEX #{index['name']}
253
+ ON #{schema_name}.#{table_name} (#{index['columns'].join(', ')})
254
+ #{index['where'] ? 'WHERE ' + index['where'] : ''}
255
+ SQL
256
+ end
257
+
258
+ conn.exec(sql)
259
+ end
260
+ end
261
+ end
262
+
263
+ def get_create_table_statement(src_schema_name, src_table_name, dest_schema_name=nil, dest_table_name=nil)
264
+ dest_schema_name ||= src_schema_name
265
+ dest_table_name ||= dest_schema_name
266
+
267
+ columns = column_definitions(src_schema_name, src_table_name)
268
+
269
+ statement = "CREATE TABLE #{dest_schema_name}.#{dest_table_name} (\n"
270
+ columns.each_with_index do |col, index|
271
+ statement << " #{col[:name]} #{col[:type]} #{col[:null]}"
272
+ statement << ',' if index != columns.size - 1
273
+ statement << "\n"
274
+ end
275
+ statement << ");\n"
276
+
277
+ statement
278
+ end
279
+
280
+ def table_exist?(schema_name, table_name)
281
+ with_connection do |conn|
282
+ _table_exists?(conn, schema_name, table_name)
283
+ end
284
+ end
285
+
286
+ private
287
+
288
+ def with_connection(&block)
289
+ conn = get_connection
290
+
291
+ block.call(conn)
292
+ ensure
293
+ conn.close if not conn.nil?
294
+ end
295
+
296
+ def get_connection
297
+ PG::Connection.connect(@dbconfig)
298
+ end
299
+
300
+ def column_line(column)
301
+ name, data_type, nullable = column.symbolize_keys.values_at(:column_name, :data_type, :is_nullable)
302
+
303
+ # default type to varchar
304
+ data_type ||= "VARCHAR(1000)"
305
+
306
+ line_tokens = ["\"#{name}\""]
307
+ line_tokens << data_type
308
+ line_tokens << (nullable ? '' : 'NOT NULL')
309
+
310
+ line_tokens
311
+ .select { |token| token != '' }
312
+ .join " "
313
+ end
314
+
315
+ def create_table_statement(connection, columns, table_name, options={})
316
+ statement = "CREATE #{options[:temporary] ? 'TEMPORARY' : ''} TABLE #{connection.escape_string(table_name)} (\n"
317
+ statement << columns
318
+ .map { |column| column_line(column) }
319
+ .map(&:strip)
320
+ .map { |column| connection.escape_string(column) }
321
+ .join(",\n")
322
+ statement << "\n);"
323
+
324
+ statement
325
+ end
326
+
327
+ def _table_exists?(connection, schema_name, table_name)
328
+ sql = <<-SQL.strip_heredoc
329
+ SELECT
330
+ count(table_name)
331
+ FROM
332
+ information_schema.tables
333
+ WHERE
334
+ table_schema <> 'pg_catalog'
335
+ AND table_schema <> 'information_schema'
336
+ AND table_schema !~ '^pg_toast'
337
+ AND table_schema = '#{connection.escape_string(schema_name)}'
338
+ AND table_name = '#{connection.escape_string(table_name)}'
339
+ GROUP BY
340
+ table_schema,table_name;
341
+ SQL
342
+
343
+ res = connection.exec(sql)
344
+
345
+ res.values.size > 0
346
+ end
347
+ end
data/lib/transport.rb ADDED
@@ -0,0 +1,138 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require './lib/postgres'
4
+ require './lib/qualified_name'
5
+ require './lib/pgcp'
6
+ require 'securerandom'
7
+
8
+ class Transport
9
+ # Initialize Transport instance
10
+ #
11
+ # @param [Hash] src_dbconfig Source database config
12
+ # @param [Hash] dest_dbconfig Destination database config
13
+ def initialize(src_dbconfig, dest_dbconfig, options={})
14
+ @src_dbconfig = src_dbconfig
15
+ @src_dbconfig[:port] ||= 5432
16
+ @dest_dbconfig = dest_dbconfig
17
+ @dest_dbconfig[:port] ||= 5432
18
+ end
19
+
20
+ def copy_tables(src_tablenames, options={})
21
+ schema_name = src_tablenames.split('.')[0]
22
+ table_glob = src_tablenames.split('.')[1]
23
+
24
+ dest_conn = Postgres.new(@dest_dbconfig)
25
+ tables = dest_conn.list_tables(schema_name)
26
+ tables.each do |table|
27
+ if File.fnmatch(table_glob, table)
28
+ copy_table("#{schema_name}.#{table}", options)
29
+ end
30
+ end
31
+ end
32
+
33
+ def copy_table(src_tablename, dest_tablename=nil, options={})
34
+ dest_tablename ||= src_tablename
35
+ options[:create_schema] = true if options[:create_schema].nil?
36
+ options[:skip_indexes] = false if options[:skipe_indexes].nil?
37
+
38
+ Pgcp.logger.info "Start to copy from table #{src_tablename} to table #{dest_tablename}"
39
+ src_table = QualifiedName.new(src_tablename)
40
+ dest_table = QualifiedName.new(dest_tablename)
41
+ dest_table.schema_name = options[:force_schema] if not options[:force_schema].nil?
42
+
43
+ src_conn = Postgres.new(@src_dbconfig)
44
+ dest_conn = Postgres.new(@dest_dbconfig)
45
+
46
+ dest_conn.exec "CREATE SCHEMA IF NOT EXISTS #{dest_table.schema_name};" if options[:create_schema]
47
+
48
+ src_indexes = src_conn.get_indexes(src_table.schema_name, src_table.table_name)
49
+ if dest_conn.table_exist?(src_table.schema_name, src_table.table_name)
50
+ Pgcp.logger.info "Destination table already exists, creating temporary table"
51
+ temp_table = QualifiedName.new("#{dest_table.schema_name}.temp_#{SecureRandom.hex}")
52
+ create_table_statement =
53
+ src_conn.get_create_table_statement(src_table.schema_name,
54
+ src_table.table_name,
55
+ temp_table.schema_name,
56
+ temp_table.table_name)
57
+ begin
58
+ dest_conn.exec(create_table_statement)
59
+ Pgcp.logger.info "Copying table data to temporary table. This could take a while..."
60
+ direct_copy(src_table.full_name, temp_table.full_name)
61
+ Pgcp.logger.info "Hotswapping to destination table #{dest_tablename}"
62
+ dest_conn.hotswap_table(dest_table.schema_name, temp_table.table_name, dest_table.table_name)
63
+ Pgcp.logger.info "Done copying table data."
64
+ rescue Exception => e
65
+ Pgcp.logger.error(e.message)
66
+ return
67
+ ensure
68
+ dest_conn.drop_table(temp_table.schema_name, temp_table.table_name)
69
+ end
70
+ else
71
+ Pgcp.logger.info "Destination table does not exist, creating destination table."
72
+ create_table_statement =
73
+ src_conn.get_create_table_statement(src_table.schema_name,
74
+ src_table.table_name,
75
+ dest_table.schema_name,
76
+ dest_table.table_name)
77
+ dest_conn.exec(create_table_statement)
78
+ Pgcp.logger.info "Copying table data to destination table. This could take a while..."
79
+ direct_copy(src_table.full_name, dest_table.full_name)
80
+ Pgcp.logger.info "Copying table data to destination table done."
81
+ end
82
+
83
+ unless options[:skip_indexes]
84
+ Pgcp.logger.info "Copying table indexes to destination table..."
85
+ dest_conn.create_indexes(dest_table.schema_name, dest_table.table_name, src_indexes)
86
+ Pgcp.logger.info "Done copying table indexes."
87
+ end
88
+
89
+ end
90
+
91
+ private
92
+
93
+ def direct_copy(src_tablename, dest_tablename)
94
+ sql_in = sql_copy_from_stdin(dest_tablename)
95
+ sql_out = sql_copy_to_stdout(src_tablename)
96
+ command = transfer_command(@src_dbconfig, @dest_dbconfig, sql_in, sql_out)
97
+ `#{command}`
98
+ exit_code = `echo $?`.to_i
99
+ if exit_code != 0
100
+ raise 'Failed to directly copy data'
101
+ end
102
+ end
103
+
104
+ def sql_copy_from_stdin(q_tablename)
105
+ <<-SQL.strip_heredoc
106
+ COPY #{q_tablename} FROM STDIN
107
+ SQL
108
+ end
109
+
110
+ def sql_copy_to_stdout(q_tablename)
111
+ <<-SQL.strip_heredoc
112
+ COPY (SELECT * FROM #{q_tablename}) TO STDOUT
113
+ SQL
114
+ end
115
+
116
+ def transfer_command(src_dbconfig, dest_dbconfig, sql_in, sql_out)
117
+ copy_to_command = %Q{
118
+ env PGPASSWORD="#{src_dbconfig[:password]}"
119
+ psql
120
+ -U #{src_dbconfig[:user]}
121
+ -h #{src_dbconfig[:host]}
122
+ -p #{src_dbconfig[:port]}
123
+ -c "#{sql_out}"
124
+ #{src_dbconfig[:dbname]}
125
+ }.gsub(/\n/, ' ')
126
+ copy_from_command = %Q{
127
+ env PGPASSWORD="#{dest_dbconfig[:password]}"
128
+ psql
129
+ -U #{dest_dbconfig[:user]}
130
+ -h #{dest_dbconfig[:host]}
131
+ -p #{dest_dbconfig[:port]}
132
+ -c "#{sql_in}"
133
+ #{dest_dbconfig[:dbname]}
134
+ }.gsub(/\n/, ' ')
135
+
136
+ "#{copy_to_command} | #{copy_from_command}"
137
+ end
138
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pgcp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Thanh Dinh Khac
8
+ - Huy Nguyen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-11-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '4.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '4.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: thor
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.19'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.19'
42
+ - !ruby/object:Gem::Dependency
43
+ name: pg
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.18'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.18'
56
+ description: A simple command line tool to copy tables from one Postgres database
57
+ to another
58
+ email: thanh@holistics.io
59
+ executables:
60
+ - pgcp
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - bin/pgcp
65
+ - lib/pgcp.rb
66
+ - lib/postgres.rb
67
+ - lib/transport.rb
68
+ homepage: http://rubygems.org/gems/pgcp
69
+ licenses:
70
+ - GPL
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 2.4.8
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: A simple command line tool to copy tables from one Postgres database to another
92
+ test_files: []