pg 0.13.2-x86-mingw32 → 0.14.0.pre.353-x86-mingw32

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.
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # A script to wrap ssh and rsync for PostgreSQL WAL files shipping.
4
+ # Mahlon E. Smith <mahlon@martini.nu>
5
+ #
6
+ # Based off of Joshua Drake's PITRTools concept, but with some important
7
+ # differences:
8
+ #
9
+ # - Only supports PostgreSQL >= 8.3
10
+ # - No support for rsync version < 3
11
+ # - Only shipping, no client side sync (too much opportunity for failure,
12
+ # and it's easy to get a base backup manually)
13
+ # - WAL files are only stored once, regardless of how many
14
+ # slaves are configured or not responding, and are removed from
15
+ # the master when they are no longer needed.
16
+ # - Each slave can have completely distinct settings, instead
17
+ # of a single set of options applied to all slaves
18
+ # - slave sync can be individually paused from the master
19
+ # - can run synchronously, or if you have a lot of slaves, threaded async mode
20
+ # - It's ruby, instead of python. :)
21
+ #
22
+ # wal_shipper is configurable via an external YAML file, and will create
23
+ # a template on its first run -- you'll need to modify it! It expects
24
+ # a directory structure like so:
25
+ #
26
+ # postgres/
27
+ # data/...
28
+ # bin/wal_shipper.rb
29
+ # etc/wal_shipper.conf <-- YAML settings!
30
+ # wal/
31
+ #
32
+ # It should be loaded from the PostgreSQL master's postgresql.conf
33
+ # as such, after putting it into your postgres user homedir under 'bin':
34
+ #
35
+ # archive_command = '/path/to/postgres_home/bin/wal_shipper.rb %p'
36
+ #
37
+ # Passwordless ssh keys need to be set up for the postgres user on all
38
+ # participating masters and slaves.
39
+ #
40
+ # You can use any replay method of your choosing on the slaves.
41
+ # Here's a nice example using pg_standby, to be put in data/recovery.conf:
42
+ #
43
+ # restore_command = 'pg_standby -t /tmp/pgrecovery.done -s5 -w0 -c /path/to/postgres_home/wal_files/ %f %p %r'
44
+ #
45
+ # Or, here's another simple alternative data/recovery.conf, for using WAL shipping
46
+ # alongside streaming replication:
47
+ #
48
+ # standby_mode = 'on'
49
+ # primary_conninfo = 'host=master.example.com port=5432 user=repl password=XXXXXXX'
50
+ # restore_command = 'cp /usr/local/pgsql/wal/%f %p'
51
+ # trigger_file = '/usr/local/pgsql/pg.become_primary'
52
+ # archive_cleanup_command = '/usr/local/bin/pg_archivecleanup /usr/local/pgsql/wal %r'
53
+ #
54
+ #========================================================================================
55
+
56
+
57
+ require 'pathname'
58
+ require 'yaml'
59
+ require 'fileutils'
60
+ require 'ostruct'
61
+
62
+
63
+ ### Encapsulate WAL shipping functionality.
64
+ ###
65
+ module WalShipper
66
+
67
+ ### Send messages to the PostgreSQL log files.
68
+ ###
69
+ def log( msg )
70
+ return unless @debug
71
+ puts "WAL Shipper: %s" % [ msg ]
72
+ end
73
+
74
+
75
+ ### An object that represents a single destination from the
76
+ ### configuration file.
77
+ ###
78
+ class Destination < OpenStruct
79
+ include WalShipper
80
+
81
+ ### Create a new WalShipper::Destination object.
82
+ def initialize( dest, debug=false )
83
+ @debug = debug
84
+ super( dest )
85
+ self.validate
86
+ end
87
+
88
+ #########
89
+ protected
90
+ #########
91
+
92
+
93
+ ### Check for required keys and normalize various keys.
94
+ ###
95
+ def validate
96
+ # Check for required destination keys
97
+ %w[ label kind ].each do |key|
98
+ if self.send( key.to_sym ).nil?
99
+ self.log "Destination %p missing required '%s' key." % [ self, key ]
100
+ self.invalid = true
101
+ end
102
+ end
103
+
104
+ # Ensure paths are Pathnames for the 'file' destination type.
105
+ self.path = Pathname.new( self.path ) if self.kind == 'file'
106
+
107
+ if self.kind == 'rsync-ssh'
108
+ self.port ||= 22
109
+ self.user = self.user ? "#{self.user}@" : ''
110
+ end
111
+ end
112
+ end # Class Destination
113
+
114
+
115
+
116
+ ### Class for creating new Destination objects and determining how to
117
+ ### ship WAL files to them.
118
+ ###
119
+ class Dispatcher
120
+ include WalShipper
121
+
122
+ ### Create a new Shipper object, given a +conf+ hash and a +wal+ file
123
+ ### Pathname object.
124
+ ###
125
+ def initialize( wal, conf )
126
+ # Make the config keys instance variables.
127
+ conf.each_pair {|key, val| self.instance_variable_set( "@#{key}", val ) }
128
+
129
+ # Spool directory check.
130
+ #
131
+ @spool = Pathname.new( @spool )
132
+ @spool.exist? or raise "The configured spool directory (%s) doesn't exist." % [ @spool ]
133
+
134
+ # Stop right away if we have disabled shipping.
135
+ #
136
+ unless @enabled
137
+ self.log "WAL shipping is disabled, queuing segment %s" % [ wal.basename ]
138
+ exit 1
139
+ end
140
+
141
+ # Instantiate Destination objects, creating new spool directories
142
+ # for each.
143
+ #
144
+ @destinations.
145
+ collect!{|dest| WalShipper::Destination.new( dest, @debug ) }.
146
+ reject {|dest| dest.invalid }.
147
+ collect do |dest|
148
+ dest.spool = @spool + dest.label
149
+ dest.spool.mkdir( 0711 ) unless dest.spool.exist?
150
+ dest
151
+ end
152
+
153
+ # Put the WAL file into the spool for processing!
154
+ #
155
+ @waldir = @spool + 'wal_segments'
156
+ @waldir.mkdir( 0711 ) unless @waldir.exist?
157
+
158
+ self.log "Copying %s to %s" % [ wal.basename, @waldir ]
159
+ FileUtils::cp wal, @waldir
160
+
161
+ # 'wal' now references the copy. The original is managed and auto-expired
162
+ # by PostgreSQL when a new checkpoint segment it reached.
163
+ @wal = @waldir + wal.basename
164
+ end
165
+
166
+
167
+ ### Create hardlinks for the WAL file into each of the destination directories
168
+ ### for separate queueing and recording of what was shipped successfully.
169
+ ###
170
+ def link
171
+ @destinations.each do |dest|
172
+ self.log "Linking %s into %s" % [ @wal.basename, dest.spool.basename ]
173
+ FileUtils::ln @wal, dest.spool, :force => true
174
+ end
175
+ end
176
+
177
+
178
+ ### Decide to be synchronous or threaded, and delegate each destination
179
+ ### to the proper ship method.
180
+ ###
181
+ def dispatch
182
+ # Synchronous mode.
183
+ #
184
+ unless @async
185
+ self.log "Performing a synchronous dispatch."
186
+ @destinations.each {|dest| self.dispatch_dest( dest ) }
187
+ return
188
+ end
189
+
190
+ tg = ThreadGroup.new
191
+
192
+ # Async, one thread per destination
193
+ #
194
+ if @async_max.nil? || @async_max.to_i.zero?
195
+ self.log "Performing an asynchronous dispatch: one thread per destination."
196
+ @destinations.each do |dest|
197
+ t = Thread.new do
198
+ Thread.current.abort_on_exception = true
199
+ self.dispatch_dest( dest )
200
+ end
201
+ tg.add( t )
202
+ end
203
+ tg.list.each {|t| t.join }
204
+ return
205
+ end
206
+
207
+ # Async, one thread per destination, in groups of asynx_max size.
208
+ #
209
+ self.log "Performing an asynchronous dispatch: one thread per destination, %d at a time." % [ @async_max ]
210
+ all_dests = @destinations.dup
211
+ dest_chunks = []
212
+ until all_dests.empty? do
213
+ dest_chunks << all_dests.slice!( 0, @async_max )
214
+ end
215
+
216
+ dest_chunks.each do |chunk|
217
+ chunk.each do |dest|
218
+ t = Thread.new do
219
+ Thread.current.abort_on_exception = true
220
+ self.dispatch_dest( dest )
221
+ end
222
+ tg.add( t )
223
+ end
224
+
225
+ tg.list.each {|t| t.join }
226
+ end
227
+
228
+ return
229
+ end
230
+
231
+
232
+ ### Remove any WAL segments no longer needed by slaves.
233
+ ###
234
+ def clean_spool
235
+ total = 0
236
+ @waldir.children.each do |wal|
237
+ if wal.stat.nlink == 1
238
+ total += wal.unlink
239
+ end
240
+ end
241
+
242
+ self.log "Removed %d WAL segment%s." % [ total, total == 1 ? '' : 's' ]
243
+ end
244
+
245
+
246
+
247
+ #########
248
+ protected
249
+ #########
250
+
251
+ ### Send WAL segments to remote +dest+ via rsync+ssh.
252
+ ### Passwordless keys between the user running this script (postmaster owner)
253
+ ### and remote user need to be set up in advance.
254
+ ###
255
+ def ship_rsync_ssh( dest )
256
+ if dest.host.nil?
257
+ self.log "Destination %p missing required 'host' key. WAL is queued." % [ dest.host ]
258
+ return
259
+ end
260
+
261
+ rsync_flags = '-zc'
262
+ ssh_string = "%s -o ConnectTimeout=%d -o StrictHostKeyChecking=no -p %d" %
263
+ [ @ssh, @ssh_timeout || 10, dest.port ]
264
+ src_string = ''
265
+ dst_string = "%s%s:%s/" % [ dest.user, dest.host, dest.path ]
266
+
267
+ # If there are numerous files in the spool dir, it means there was
268
+ # an error transferring to this host in the past. Try and ship all
269
+ # WAL segments, instead of just the new one. PostgreSQL on the slave
270
+ # side will "do the right thing" as they come in, regardless of
271
+ # ordering.
272
+ #
273
+ if dest.spool.children.length > 1
274
+ src_string = dest.spool.to_s + '/'
275
+ rsync_flags << 'r'
276
+ else
277
+ src_string = dest.spool + @wal.basename
278
+ end
279
+
280
+
281
+ ship_wal_cmd = [
282
+ @rsync,
283
+ @debug ? (rsync_flags << 'vh') : (rsync_flags << 'q'),
284
+ '--remove-source-files',
285
+ '-e', ssh_string,
286
+ src_string, dst_string
287
+ ]
288
+
289
+ self.log "Running command '%s'" % [ ship_wal_cmd.join(' ') ]
290
+ system *ship_wal_cmd
291
+
292
+ # Run external notification program on error, if one is configured.
293
+ #
294
+ unless $?.success?
295
+ self.log "Ack! Error while shipping to %p, WAL is queued." % [ dest.label ]
296
+ system @error_cmd, dest.label if @error_cmd
297
+ end
298
+ end
299
+
300
+
301
+ ### Copy WAL segments to remote path as set in +dest+.
302
+ ### This is useful for longer term PITR, copying to NFS shares, etc.
303
+ ###
304
+ def ship_file( dest )
305
+ if dest.path.nil?
306
+ self.log "Destination %p missing required 'path' key. WAL is queued." % [ dest ]
307
+ return
308
+ end
309
+ dest.path.mkdir( 0711 ) unless dest.path.exist?
310
+
311
+ # If there are numerous files in the spool dir, it means there was
312
+ # an error transferring to this host in the past. Try and ship all
313
+ # WAL segments, instead of just the new one. PostgreSQL on the slave
314
+ # side will "do the right thing" as they come in, regardless of
315
+ # ordering.
316
+ #
317
+ if dest.spool.children.length > 1
318
+ dest.spool.children.each do |wal|
319
+ wal.unlink if self.copy_file( wal, dest.path, dest.label, dest.compress )
320
+ end
321
+ else
322
+ wal = dest.spool + @wal.basename
323
+ wal.unlink if self.copy_file( wal, dest.path, dest.label, dest.compress )
324
+ end
325
+ end
326
+
327
+
328
+ ### Given a +wal+ Pathname, a +path+ destination, and the destination
329
+ ### label, copy and optionally compress a WAL file.
330
+ ###
331
+ def copy_file( wal, path, label, compress=false )
332
+ dest_file = path + wal.basename
333
+ FileUtils::cp wal, dest_file
334
+ if compress
335
+ system *[ 'gzip', '-f', dest_file ]
336
+ raise "Error while compressing: %s" % [ wal.basename ] unless $?.success?
337
+ end
338
+ self.log "Copied %s%s to %s." %
339
+ [ wal.basename, compress ? ' (and compressed)' : '', path ]
340
+ return true
341
+ rescue => err
342
+ self.log "Ack! Error while copying '%s' (%s) to %p, WAL is queued." %
343
+ [ wal.basename, err.message, path ]
344
+ system @error_cmd, label if @error_cmd
345
+ return false
346
+ end
347
+
348
+
349
+ ### Figure out how to send the WAL file to its intended destination +dest+.
350
+ ###
351
+ def dispatch_dest( dest )
352
+ if ! dest.enabled.nil? && ! dest.enabled
353
+ self.log "Skipping explicity disabled destination %p, WAL is queued." % [ dest.label ]
354
+ return
355
+ end
356
+
357
+ # Send to the appropriate method. ( rsync-ssh --> ship_rsync_ssh )
358
+ #
359
+ meth = ( 'ship_' + dest.kind.gsub(/-/, '_') ).to_sym
360
+ if WalShipper::Dispatcher.method_defined?( meth )
361
+ self.send( meth, dest )
362
+ else
363
+ self.log "Unknown destination kind %p for %p. WAL is queued." % [ dest.kind, dest.label ]
364
+ end
365
+ end
366
+ end
367
+ end
368
+
369
+ # Ship the WAL file!
370
+ #
371
+ if __FILE__ == $0
372
+ CONFIG_DIR = Pathname.new( __FILE__ ).dirname.parent + 'etc'
373
+ CONFIG = CONFIG_DIR + 'wal_shipper.conf'
374
+
375
+ unless CONFIG.exist?
376
+ CONFIG_DIR.mkdir( 0711 ) unless CONFIG_DIR.exist?
377
+ CONFIG.open('w') {|conf| conf.print(DATA.read) }
378
+ CONFIG.chmod( 0644 )
379
+ puts "No WAL shipping configuration found, default file created."
380
+ end
381
+
382
+ wal = ARGV[0] or raise "No WAL file was specified on the command line."
383
+ wal = Pathname.new( wal )
384
+ conf = YAML.load( CONFIG.read )
385
+
386
+ shipper = WalShipper::Dispatcher.new( wal, conf )
387
+ shipper.link
388
+ shipper.dispatch
389
+ shipper.clean_spool
390
+ end
391
+
392
+
393
+ __END__
394
+ ---
395
+ # Spool from pg_xlog to the working area?
396
+ # This must be set to 'true' for wal shipping to function!
397
+ enabled: false
398
+
399
+ # Log everything to the PostgreSQL log files?
400
+ debug: true
401
+
402
+ # The working area for WAL segments.
403
+ spool: /opt/local/var/db/postgresql84/wal
404
+
405
+ # With multiple slaves, ship WAL in parallel, or be synchronous?
406
+ async: false
407
+
408
+ # Put a ceiling on the parallel threads?
409
+ # '0' or removing this option uses a thread for each destination,
410
+ # regardless of how many you have. Keep in mind that's 16 * destination
411
+ # count megs of simultaneous bandwidth.
412
+ async_max: 5
413
+
414
+ # Paths and settings for various binaries.
415
+ rsync: /usr/bin/rsync
416
+ ssh: /usr/bin/ssh
417
+ ssh_timeout: 10
418
+
419
+ destinations:
420
+
421
+ - label: rsync-example
422
+ port: 2222
423
+ kind: rsync-ssh
424
+ host: localhost
425
+ user: postgres
426
+ path: wal # relative to the user's homedir on the remote host
427
+ enabled: false
428
+
429
+ - label: file-example
430
+ kind: file
431
+ compress: true
432
+ enabled: true
433
+ path: /tmp/someplace
434
+
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ #
4
+ # Script to automatically move partitioned tables and their indexes
5
+ # to a separate area on disk.
6
+ #
7
+ # Mahlon E. Smith <mahlon@martini.nu>
8
+ #
9
+ # Example use case:
10
+ #
11
+ # - You've got a heavy insert table, such as syslog data.
12
+ # - This table has a partitioning trigger (or is manually partitioned)
13
+ # by date, to separate incoming stuff from archival/report stuff.
14
+ # - You have a tablespace on cheap or slower disk (maybe even
15
+ # ZFS compressed, or some such!)
16
+ #
17
+ # The only assumption this script makes is that your tables are dated, and
18
+ # the tablespace they're moving into already exists.
19
+ #
20
+ # A full example, using the syslog idea from above, where each child
21
+ # table is date partitioned by a convention of "syslog_YEAR-WEEKOFYEAR":
22
+ #
23
+ # syslog # <--- parent
24
+ # syslog_2012_06 # <--- inherited
25
+ # syslog_2012_07 # <--- inherited
26
+ # syslog_2012_08 # <--- inherited
27
+ # ...
28
+ #
29
+ # You'd run this script like so:
30
+ #
31
+ # ./warehouse_partitions.rb -F syslog_%Y_%U
32
+ #
33
+ # Assuming this was week 12 of the year, tables syslog_2012_06 through
34
+ # syslog_2012_11 would start sequentially migrating into the tablespace
35
+ # called 'warehouse'.
36
+ #
37
+
38
+
39
+ begin
40
+ require 'date'
41
+ require 'ostruct'
42
+ require 'optparse'
43
+ require 'pathname'
44
+ require 'etc'
45
+ require 'pg'
46
+
47
+ rescue LoadError # 1.8 support
48
+ unless Object.const_defined?( :Gem )
49
+ require 'rubygems'
50
+ retry
51
+ end
52
+ raise
53
+ end
54
+
55
+
56
+ ### A tablespace migration class.
57
+ ###
58
+ class PGWarehouse
59
+
60
+ def initialize( opts )
61
+ @opts = opts
62
+ @db = PG.connect(
63
+ :dbname => opts.database,
64
+ :host => opts.host,
65
+ :port => opts.port,
66
+ :user => opts.user,
67
+ :password => opts.pass,
68
+ :sslmode => 'prefer'
69
+ )
70
+ @db.exec "SET search_path TO %s" % [ opts.schema ] if opts.schema
71
+
72
+ @relations = self.relations
73
+ end
74
+
75
+ attr_reader :db
76
+
77
+ ######
78
+ public
79
+ ######
80
+
81
+ ### Perform the tablespace moves.
82
+ ###
83
+ def migrate
84
+ if @relations.empty?
85
+ $stderr.puts 'No tables were found for warehousing.'
86
+ return
87
+ end
88
+
89
+ $stderr.puts "Found %d relation%s to move." % [ relations.length, relations.length == 1 ? '' : 's' ]
90
+ @relations.sort_by{|_,v| v[:name] }.each do |_, val|
91
+ $stderr.print " - Moving table '%s' to '%s'... " % [
92
+ val[:name], @opts.tablespace
93
+ ]
94
+
95
+ if @opts.dryrun
96
+ $stderr.puts '(not really)'
97
+
98
+ else
99
+ age = self.timer do
100
+ db.exec "ALTER TABLE %s SET TABLESPACE %s;" % [
101
+ val[:name], @opts.tablespace
102
+ ]
103
+ end
104
+ puts age
105
+ end
106
+
107
+ val[ :indexes ].each do |idx|
108
+ $stderr.print " - Moving index '%s' to '%s'... " % [
109
+ idx, @opts.tablespace
110
+ ]
111
+ if @opts.dryrun
112
+ $stderr.puts '(not really)'
113
+
114
+ else
115
+ age = self.timer do
116
+ db.exec "ALTER INDEX %s SET TABLESPACE %s;" % [
117
+ idx, @opts.tablespace
118
+ ]
119
+ end
120
+ puts age
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+
127
+ #########
128
+ protected
129
+ #########
130
+
131
+ ### Get OIDs and current tablespaces for everything under the
132
+ ### specified schema.
133
+ ###
134
+ def relations
135
+ return @relations if @relations
136
+ relations = {}
137
+
138
+ query = %q{
139
+ SELECT c.oid AS oid,
140
+ c.relname AS name,
141
+ c.relkind AS kind,
142
+ t.spcname AS tspace
143
+ FROM pg_class AS c
144
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
145
+ LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
146
+ WHERE c.relkind = 'r' }
147
+ query << "AND n.nspname='#{@opts.schema}'" if @opts.schema
148
+
149
+ # Get the relations list, along with each element's current tablespace.
150
+ #
151
+ self.db.exec( query ) do |res|
152
+ res.each do |row|
153
+ relations[ row['oid'] ] = {
154
+ :name => row['name'],
155
+ :tablespace => row['tspace'],
156
+ :indexes => [],
157
+ :parent => nil
158
+ }
159
+ end
160
+ end
161
+
162
+ # Add table inheritence information.
163
+ #
164
+ db.exec 'SELECT inhrelid AS oid, inhparent AS parent FROM pg_inherits' do |res|
165
+ res.each do |row|
166
+ relations[ row['oid'] ][ :parent ] = row['parent']
167
+ end
168
+ end
169
+
170
+ # Remove tables that don't qualify for warehousing.
171
+ #
172
+ # - Tables that are not children of a parent
173
+ # - Tables that are already in the warehouse tablespace
174
+ # - The currently active child (it's likely being written to!)
175
+ # - Any table that can't be parsed into the specified format
176
+ #
177
+ relations.reject! do |oid, val|
178
+ begin
179
+ val[:parent].nil? ||
180
+ val[:tablespace] == @opts.tablespace ||
181
+ val[:name] == Time.now.strftime( @opts.format ) ||
182
+ ! DateTime.strptime( val[:name], @opts.format )
183
+ rescue ArgumentError
184
+ true
185
+ end
186
+ end
187
+
188
+ query = %q{
189
+ SELECT c.oid AS oid,
190
+ i.indexname AS name
191
+ FROM pg_class AS c
192
+ INNER JOIN pg_indexes AS i
193
+ ON i.tablename = c.relname }
194
+ query << "AND i.schemaname='#{@opts.schema}'" if @opts.schema
195
+
196
+ # Attach index names to tables.
197
+ #
198
+ db.exec( query ) do |res|
199
+ res.each do |row|
200
+ relations[ row['oid'] ][ :indexes ] << row['name'] if relations[ row['oid'] ]
201
+ end
202
+ end
203
+
204
+ return relations
205
+ end
206
+
207
+
208
+ ### Wrap arbitrary commands in a human readable timer.
209
+ ###
210
+ def timer
211
+ start = Time.now
212
+ yield
213
+ age = Time.now - start
214
+
215
+ diff = age
216
+ secs = diff % 60
217
+ diff = ( diff - secs ) / 60
218
+ mins = diff % 60
219
+ diff = ( diff - mins ) / 60
220
+ hour = diff % 24
221
+
222
+ return "%02d:%02d:%02d" % [ hour, mins, secs ]
223
+ end
224
+ end
225
+
226
+
227
+ ### Parse command line arguments. Return a struct of global options.
228
+ ###
229
+ def parse_args( args )
230
+ options = OpenStruct.new
231
+ options.database = Etc.getpwuid( Process.uid ).name
232
+ options.host = '127.0.0.1'
233
+ options.port = 5432
234
+ options.user = Etc.getpwuid( Process.uid ).name
235
+ options.sslmode = 'prefer'
236
+ options.tablespace = 'warehouse'
237
+
238
+ opts = OptionParser.new do |opts|
239
+ opts.banner = "Usage: #{$0} [options]"
240
+
241
+ opts.separator ''
242
+ opts.separator 'Connection options:'
243
+
244
+ opts.on( '-d', '--database DBNAME',
245
+ "specify the database to connect to (default: \"#{options.database}\")" ) do |db|
246
+ options.database = db
247
+ end
248
+
249
+ opts.on( '-h', '--host HOSTNAME', 'database server host' ) do |host|
250
+ options.host = host
251
+ end
252
+
253
+ opts.on( '-p', '--port PORT', Integer,
254
+ "database server port (default: \"#{options.port}\")" ) do |port|
255
+ options.port = port
256
+ end
257
+
258
+ opts.on( '-n', '--schema SCHEMA', String,
259
+ "operate on the named schema only (default: none)" ) do |schema|
260
+ options.schema = schema
261
+ end
262
+
263
+ opts.on( '-T', '--tablespace SPACE', String,
264
+ "move old tables to this tablespace (default: \"#{options.tablespace}\")" ) do |tb|
265
+ options.tablespace = tb
266
+ end
267
+
268
+ opts.on( '-F', '--tableformat FORMAT', String,
269
+ "The naming format (strftime) for the inherited tables (default: none)" ) do |format|
270
+ options.format = format
271
+ end
272
+
273
+ opts.on( '-U', '--user NAME',
274
+ "database user name (default: \"#{options.user}\")" ) do |user|
275
+ options.user = user
276
+ end
277
+
278
+ opts.on( '-W', 'force password prompt' ) do |pw|
279
+ print 'Password: '
280
+ begin
281
+ system 'stty -echo'
282
+ options.pass = gets.chomp
283
+ ensure
284
+ system 'stty echo'
285
+ puts
286
+ end
287
+ end
288
+
289
+ opts.separator ''
290
+ opts.separator 'Other options:'
291
+
292
+ opts.on_tail( '--dry-run', "don't actually do anything" ) do
293
+ options.dryrun = true
294
+ end
295
+
296
+ opts.on_tail( '--help', 'show this help, then exit' ) do
297
+ $stderr.puts opts
298
+ exit
299
+ end
300
+
301
+ opts.on_tail( '--version', 'output version information, then exit' ) do
302
+ puts Stats::VERSION
303
+ exit
304
+ end
305
+ end
306
+
307
+ opts.parse!( args )
308
+ return options
309
+ end
310
+
311
+
312
+ if __FILE__ == $0
313
+ opts = parse_args( ARGV )
314
+ raise ArgumentError, "A naming format (-F) is required." unless opts.format
315
+
316
+ $stdout.sync = true
317
+ PGWarehouse.new( opts ).migrate
318
+ end
319
+
320
+