pgslice 0.4.1 → 0.4.6

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.
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # handle interrupts
4
+ trap("SIGINT") { abort }
5
+
3
6
  require "pgslice"
4
- begin
5
- PgSlice::Client.new(ARGV).perform
6
- rescue PgSlice::Error => e
7
- abort e.message
8
- rescue Interrupt => e
9
- abort
10
- end
7
+ PgSlice::CLI.start
@@ -1,727 +1,22 @@
1
- require "pgslice/version"
2
- require "slop"
1
+ # dependencies
3
2
  require "pg"
4
- require "cgi"
5
-
6
- module PgSlice
7
- class Error < StandardError; end
8
-
9
- class Client
10
- attr_reader :arguments, :options
11
-
12
- SQL_FORMAT = {
13
- day: "YYYYMMDD",
14
- month: "YYYYMM"
15
- }
16
-
17
- def initialize(args)
18
- $stdout.sync = true
19
- $stderr.sync = true
20
- parse_args(args)
21
- @command = @arguments.shift
22
- end
23
-
24
- def perform
25
- return if @exit
26
-
27
- case @command
28
- when "prep"
29
- prep
30
- when "add_partitions"
31
- add_partitions
32
- when "fill"
33
- fill
34
- when "swap"
35
- swap
36
- when "unswap"
37
- unswap
38
- when "unprep"
39
- unprep
40
- when "analyze"
41
- analyze
42
- when nil
43
- log "Commands: add_partitions, analyze, fill, prep, swap, unprep, unswap"
44
- else
45
- abort "Unknown command: #{@command}"
46
- end
47
- ensure
48
- @connection.close if @connection
49
- end
50
-
51
- protected
52
-
53
- # commands
54
-
55
- def prep
56
- table, column, period = arguments
57
- table = qualify_table(table)
58
- intermediate_table = "#{table}_intermediate"
59
-
60
- trigger_name = self.trigger_name(table)
61
-
62
- if options[:no_partition]
63
- abort "Usage: pgslice prep <table> --no-partition" if arguments.length != 1
64
- abort "Can't use --trigger-based and --no-partition" if options[:trigger_based]
65
- else
66
- abort "Usage: pgslice prep <table> <column> <period>" if arguments.length != 3
67
- end
68
- abort "Table not found: #{table}" unless table_exists?(table)
69
- abort "Table already exists: #{intermediate_table}" if table_exists?(intermediate_table)
70
-
71
- unless options[:no_partition]
72
- abort "Column not found: #{column}" unless columns(table).include?(column)
73
- abort "Invalid period: #{period}" unless SQL_FORMAT[period.to_sym]
74
- end
75
-
76
- queries = []
77
-
78
- declarative = server_version_num >= 100000 && !options[:trigger_based]
79
-
80
- if declarative && !options[:no_partition]
81
- queries << <<-SQL
82
- CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING STORAGE INCLUDING COMMENTS) PARTITION BY RANGE (#{quote_table(column)});
83
- SQL
84
-
85
- # add comment
86
- cast = column_cast(table, column)
87
- queries << <<-SQL
88
- COMMENT ON TABLE #{quote_table(intermediate_table)} is 'column:#{column},period:#{period},cast:#{cast}';
89
- SQL
90
- else
91
- queries << <<-SQL
92
- CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} INCLUDING ALL);
93
- SQL
94
-
95
- foreign_keys(table).each do |fk_def|
96
- queries << "ALTER TABLE #{quote_table(intermediate_table)} ADD #{fk_def};"
97
- end
98
- end
99
-
100
- if !options[:no_partition] && !declarative
101
- sql_format = SQL_FORMAT[period.to_sym]
102
- queries << <<-SQL
103
- CREATE FUNCTION #{quote_ident(trigger_name)}()
104
- RETURNS trigger AS $$
105
- BEGIN
106
- RAISE EXCEPTION 'Create partitions first.';
107
- END;
108
- $$ LANGUAGE plpgsql;
109
- SQL
110
-
111
- queries << <<-SQL
112
- CREATE TRIGGER #{quote_ident(trigger_name)}
113
- BEFORE INSERT ON #{quote_table(intermediate_table)}
114
- FOR EACH ROW EXECUTE PROCEDURE #{quote_ident(trigger_name)}();
115
- SQL
116
-
117
- cast = column_cast(table, column)
118
- queries << <<-SQL
119
- COMMENT ON TRIGGER #{quote_ident(trigger_name)} ON #{quote_table(intermediate_table)} is 'column:#{column},period:#{period},cast:#{cast}';
120
- SQL
121
- end
122
-
123
- run_queries(queries)
124
- end
125
-
126
- def unprep
127
- table = qualify_table(arguments.first)
128
- intermediate_table = "#{table}_intermediate"
129
- trigger_name = self.trigger_name(table)
130
-
131
- abort "Usage: pgslice unprep <table>" if arguments.length != 1
132
- abort "Table not found: #{intermediate_table}" unless table_exists?(intermediate_table)
133
-
134
- queries = [
135
- "DROP TABLE #{quote_table(intermediate_table)} CASCADE;",
136
- "DROP FUNCTION IF EXISTS #{quote_ident(trigger_name)}();"
137
- ]
138
- run_queries(queries)
139
- end
140
-
141
- def add_partitions
142
- original_table = qualify_table(arguments.first)
143
- table = options[:intermediate] ? "#{original_table}_intermediate" : original_table
144
- trigger_name = self.trigger_name(original_table)
145
-
146
- abort "Usage: pgslice add_partitions <table>" if arguments.length != 1
147
- abort "Table not found: #{table}" unless table_exists?(table)
148
-
149
- future = options[:future]
150
- past = options[:past]
151
- range = (-1 * past)..future
152
-
153
- period, field, cast, needs_comment, declarative = settings_from_trigger(original_table, table)
154
- unless period
155
- message = "No settings found: #{table}"
156
- message = "#{message}\nDid you mean to use --intermediate?" unless options[:intermediate]
157
- abort message
158
- end
159
-
160
- queries = []
161
-
162
- if needs_comment
163
- queries << "COMMENT ON TRIGGER #{quote_ident(trigger_name)} ON #{quote_table(table)} is 'column:#{field},period:#{period},cast:#{cast}';"
164
- end
165
-
166
- # today = utc date
167
- today = round_date(DateTime.now.new_offset(0).to_date, period)
168
-
169
- schema_table =
170
- if !declarative
171
- table
172
- elsif options[:intermediate]
173
- original_table
174
- else
175
- existing_partitions(original_table, period).last
176
- end
177
-
178
- index_defs = execute("SELECT pg_get_indexdef(indexrelid) FROM pg_index WHERE indrelid = #{regclass(schema_table)} AND indisprimary = 'f'").map { |r| r["pg_get_indexdef"] }
179
- fk_defs = foreign_keys(schema_table)
180
- primary_key = self.primary_key(schema_table)
181
-
182
- added_partitions = []
183
- range.each do |n|
184
- day = advance_date(today, period, n)
185
-
186
- partition_name = "#{original_table}_#{day.strftime(name_format(period))}"
187
- next if table_exists?(partition_name)
188
- added_partitions << partition_name
189
-
190
- if declarative
191
- queries << <<-SQL
192
- CREATE TABLE #{quote_table(partition_name)} PARTITION OF #{quote_table(table)} FOR VALUES FROM (#{sql_date(day, cast, false)}) TO (#{sql_date(advance_date(day, period, 1), cast, false)});
193
- SQL
194
- else
195
- queries << <<-SQL
196
- CREATE TABLE #{quote_table(partition_name)}
197
- (CHECK (#{quote_ident(field)} >= #{sql_date(day, cast)} AND #{quote_ident(field)} < #{sql_date(advance_date(day, period, 1), cast)}))
198
- INHERITS (#{quote_table(table)});
199
- SQL
200
- end
201
-
202
- queries << "ALTER TABLE #{quote_table(partition_name)} ADD PRIMARY KEY (#{primary_key.map { |k| quote_ident(k) }.join(", ")});" if primary_key.any?
203
-
204
- index_defs.each do |index_def|
205
- queries << index_def.sub(/ ON \S+ USING /, " ON #{quote_table(partition_name)} USING ").sub(/ INDEX .+ ON /, " INDEX ON ") + ";"
206
- end
207
-
208
- fk_defs.each do |fk_def|
209
- queries << "ALTER TABLE #{quote_table(partition_name)} ADD #{fk_def};"
210
- end
211
- end
212
-
213
- unless declarative
214
- # update trigger based on existing partitions
215
- current_defs = []
216
- future_defs = []
217
- past_defs = []
218
- name_format = self.name_format(period)
219
- existing_tables = existing_partitions(original_table, period)
220
- existing_tables = (existing_tables + added_partitions).uniq.sort
221
-
222
- existing_tables.each do |table|
223
- day = DateTime.strptime(table.split("_").last, name_format)
224
- partition_name = "#{original_table}_#{day.strftime(name_format(period))}"
225
-
226
- sql = "(NEW.#{quote_ident(field)} >= #{sql_date(day, cast)} AND NEW.#{quote_ident(field)} < #{sql_date(advance_date(day, period, 1), cast)}) THEN
227
- INSERT INTO #{quote_table(partition_name)} VALUES (NEW.*);"
228
-
229
- if day.to_date < today
230
- past_defs << sql
231
- elsif advance_date(day, period, 1) < today
232
- current_defs << sql
233
- else
234
- future_defs << sql
235
- end
236
- end
237
-
238
- # order by current period, future periods asc, past periods desc
239
- trigger_defs = current_defs + future_defs + past_defs.reverse
240
-
241
- if trigger_defs.any?
242
- queries << <<-SQL
243
- CREATE OR REPLACE FUNCTION #{quote_ident(trigger_name)}()
244
- RETURNS trigger AS $$
245
- BEGIN
246
- IF #{trigger_defs.join("\n ELSIF ")}
247
- ELSE
248
- RAISE EXCEPTION 'Date out of range. Ensure partitions are created.';
249
- END IF;
250
- RETURN NULL;
251
- END;
252
- $$ LANGUAGE plpgsql;
253
- SQL
254
- end
255
- end
256
-
257
- run_queries(queries) if queries.any?
258
- end
259
-
260
- def fill
261
- table = qualify_table(arguments.first)
262
-
263
- abort "Usage: pgslice fill <table>" if arguments.length != 1
264
-
265
- source_table = options[:source_table]
266
- dest_table = options[:dest_table]
267
-
268
- if options[:swapped]
269
- source_table ||= retired_name(table)
270
- dest_table ||= table
271
- else
272
- source_table ||= table
273
- dest_table ||= intermediate_name(table)
274
- end
275
-
276
- abort "Table not found: #{source_table}" unless table_exists?(source_table)
277
- abort "Table not found: #{dest_table}" unless table_exists?(dest_table)
278
-
279
- period, field, cast, needs_comment, declarative = settings_from_trigger(table, dest_table)
280
-
281
- if period
282
- name_format = self.name_format(period)
283
-
284
- existing_tables = existing_partitions(table, period)
285
- if existing_tables.any?
286
- starting_time = DateTime.strptime(existing_tables.first.split("_").last, name_format)
287
- ending_time = advance_date(DateTime.strptime(existing_tables.last.split("_").last, name_format), period, 1)
288
- end
289
- end
290
-
291
- schema_table = period && declarative ? existing_tables.last : table
292
-
293
- primary_key = self.primary_key(schema_table)[0]
294
- abort "No primary key" unless primary_key
295
-
296
- max_source_id = max_id(source_table, primary_key)
297
-
298
- max_dest_id =
299
- if options[:start]
300
- options[:start]
301
- elsif options[:swapped]
302
- max_id(dest_table, primary_key, where: options[:where], below: max_source_id)
303
- else
304
- max_id(dest_table, primary_key, where: options[:where])
305
- end
306
-
307
- if max_dest_id == 0 && !options[:swapped]
308
- min_source_id = min_id(source_table, primary_key, field, cast, starting_time, options[:where])
309
- max_dest_id = min_source_id - 1 if min_source_id
310
- end
311
-
312
- starting_id = max_dest_id
313
- fields = columns(source_table).map { |c| quote_ident(c) }.join(", ")
314
- batch_size = options[:batch_size]
315
-
316
- i = 1
317
- batch_count = ((max_source_id - starting_id) / batch_size.to_f).ceil
318
-
319
- if batch_count == 0
320
- log_sql "/* nothing to fill */"
321
- end
322
-
323
- while starting_id < max_source_id
324
- where = "#{quote_ident(primary_key)} > #{starting_id} AND #{quote_ident(primary_key)} <= #{starting_id + batch_size}"
325
- if starting_time
326
- where << " AND #{quote_ident(field)} >= #{sql_date(starting_time, cast)} AND #{quote_ident(field)} < #{sql_date(ending_time, cast)}"
327
- end
328
- if options[:where]
329
- where << " AND #{options[:where]}"
330
- end
3
+ require "thor"
331
4
 
332
- query = <<-SQL
333
- /* #{i} of #{batch_count} */
334
- INSERT INTO #{quote_table(dest_table)} (#{fields})
335
- SELECT #{fields} FROM #{quote_table(source_table)}
336
- WHERE #{where}
337
- SQL
338
-
339
- run_query(query)
340
-
341
- starting_id += batch_size
342
- i += 1
343
-
344
- if options[:sleep] && starting_id <= max_source_id
345
- sleep(options[:sleep])
346
- end
347
- end
348
- end
349
-
350
- def swap
351
- table = qualify_table(arguments.first)
352
- intermediate_table = intermediate_name(table)
353
- retired_table = retired_name(table)
354
-
355
- abort "Usage: pgslice swap <table>" if arguments.length != 1
356
- abort "Table not found: #{table}" unless table_exists?(table)
357
- abort "Table not found: #{intermediate_table}" unless table_exists?(intermediate_table)
358
- abort "Table already exists: #{retired_table}" if table_exists?(retired_table)
359
-
360
- queries = [
361
- "ALTER TABLE #{quote_table(table)} RENAME TO #{quote_no_schema(retired_table)};",
362
- "ALTER TABLE #{quote_table(intermediate_table)} RENAME TO #{quote_no_schema(table)};"
363
- ]
364
-
365
- self.sequences(table).each do |sequence|
366
- queries << "ALTER SEQUENCE #{quote_ident(sequence["sequence_name"])} OWNED BY #{quote_ident(table)}.#{quote_ident(sequence["related_column"])};"
367
- end
368
-
369
- queries.unshift("SET LOCAL lock_timeout = '#{options[:lock_timeout]}';") if server_version_num >= 90300
370
-
371
- run_queries(queries)
372
- end
373
-
374
- def unswap
375
- table = qualify_table(arguments.first)
376
- intermediate_table = intermediate_name(table)
377
- retired_table = retired_name(table)
378
-
379
- abort "Usage: pgslice unswap <table>" if arguments.length != 1
380
- abort "Table not found: #{table}" unless table_exists?(table)
381
- abort "Table not found: #{retired_table}" unless table_exists?(retired_table)
382
- abort "Table already exists: #{intermediate_table}" if table_exists?(intermediate_table)
383
-
384
- queries = [
385
- "ALTER TABLE #{quote_table(table)} RENAME TO #{quote_no_schema(intermediate_table)};",
386
- "ALTER TABLE #{quote_table(retired_table)} RENAME TO #{quote_no_schema(table)};"
387
- ]
388
-
389
- self.sequences(table).each do |sequence|
390
- queries << "ALTER SEQUENCE #{quote_ident(sequence["sequence_name"])} OWNED BY #{quote_ident(table)}.#{quote_ident(sequence["related_column"])};"
391
- end
392
-
393
- run_queries(queries)
394
- end
395
-
396
- def analyze
397
- table = qualify_table(arguments.first)
398
- parent_table = options[:swapped] ? table : intermediate_name(table)
399
-
400
- abort "Usage: pgslice analyze <table>" if arguments.length != 1
401
-
402
- existing_tables = existing_partitions(table)
403
- analyze_list = existing_tables + [parent_table]
404
- run_queries_without_transaction analyze_list.map { |t| "ANALYZE VERBOSE #{quote_table(t)};" }
405
- end
406
-
407
- # arguments
408
-
409
- def parse_args(args)
410
- opts = Slop.parse(args) do |o|
411
- o.boolean "--intermediate"
412
- o.boolean "--swapped"
413
- o.float "--sleep"
414
- o.integer "--future", default: 0
415
- o.integer "--past", default: 0
416
- o.integer "--batch-size", default: 10000
417
- o.boolean "--dry-run", default: false
418
- o.boolean "--no-partition", default: false
419
- o.boolean "--trigger-based", default: false
420
- o.integer "--start"
421
- o.string "--url"
422
- o.string "--source-table"
423
- o.string "--dest-table"
424
- o.string "--where"
425
- o.string "--lock-timeout", default: "5s"
426
- o.on "-v", "--version", "print the version" do
427
- log PgSlice::VERSION
428
- @exit = true
429
- end
430
- end
431
- @arguments = opts.arguments
432
- @options = opts.to_hash
433
- rescue Slop::Error => e
434
- abort e.message
435
- end
436
-
437
- # output
438
-
439
- def log(message = nil)
440
- $stderr.puts message
441
- end
442
-
443
- def log_sql(message = nil)
444
- $stdout.puts message
445
- end
446
-
447
- def abort(message)
448
- raise PgSlice::Error, message
449
- end
450
-
451
- # database connection
452
-
453
- def connection
454
- @connection ||= begin
455
- url = options[:url] || ENV["PGSLICE_URL"]
456
- abort "Set PGSLICE_URL or use the --url option" unless url
457
- uri = URI.parse(url)
458
- uri_parser = URI::Parser.new
459
- config = {
460
- host: uri.host,
461
- port: uri.port,
462
- dbname: uri.path.sub(/\A\//, ""),
463
- user: uri.user,
464
- password: uri.password,
465
- connect_timeout: 1
466
- }.reject { |_, value| value.to_s.empty? }
467
- config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a?(String) }
468
- @schema = CGI.parse(uri.query.to_s)["schema"][0] || "public"
469
- PG::Connection.new(config)
470
- end
471
- rescue PG::ConnectionBad => e
472
- abort e.message
473
- end
474
-
475
- def schema
476
- connection # ensure called first
477
- @schema
478
- end
479
-
480
- def execute(query, params = [])
481
- connection.exec_params(query, params).to_a
482
- end
483
-
484
- def run_queries(queries)
485
- connection.transaction do
486
- execute("SET LOCAL client_min_messages TO warning") unless options[:dry_run]
487
- log_sql "BEGIN;"
488
- log_sql
489
- run_queries_without_transaction(queries)
490
- log_sql "COMMIT;"
491
- end
492
- end
493
-
494
- def run_query(query)
495
- log_sql query
496
- unless options[:dry_run]
497
- begin
498
- execute(query)
499
- rescue PG::ServerError => e
500
- abort("#{e.class.name}: #{e.message}")
501
- end
502
- end
503
- log_sql
504
- end
505
-
506
- def run_queries_without_transaction(queries)
507
- queries.each do |query|
508
- run_query(query)
509
- end
510
- end
511
-
512
- def server_version_num
513
- execute("SHOW server_version_num")[0]["server_version_num"].to_i
514
- end
515
-
516
- def existing_partitions(table, period = nil)
517
- count =
518
- case period
519
- when "day"
520
- 8
521
- when "month"
522
- 6
523
- else
524
- "6,8"
525
- end
526
-
527
- existing_tables(like: "#{table}_%").select { |t| /\A#{Regexp.escape("#{table}_")}\d{#{count}}\z/.match(t) }
528
- end
529
-
530
- def existing_tables(like:)
531
- query = "SELECT schemaname, tablename FROM pg_catalog.pg_tables WHERE schemaname = $1 AND tablename LIKE $2"
532
- execute(query, like.split(".", 2)).map { |r| "#{r["schemaname"]}.#{r["tablename"]}" }.sort
533
- end
534
-
535
- def table_exists?(table)
536
- existing_tables(like: table).any?
537
- end
538
-
539
- def columns(table)
540
- execute("SELECT column_name FROM information_schema.columns WHERE table_schema || '.' || table_name = $1", [table]).map{ |r| r["column_name"] }
541
- end
542
-
543
- # http://stackoverflow.com/a/20537829
544
- def primary_key(table)
545
- query = <<-SQL
546
- SELECT
547
- pg_attribute.attname,
548
- format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
549
- FROM
550
- pg_index, pg_class, pg_attribute, pg_namespace
551
- WHERE
552
- nspname || '.' || relname = $1 AND
553
- indrelid = pg_class.oid AND
554
- pg_class.relnamespace = pg_namespace.oid AND
555
- pg_attribute.attrelid = pg_class.oid AND
556
- pg_attribute.attnum = any(pg_index.indkey) AND
557
- indisprimary
558
- SQL
559
- execute(query, [table]).map { |r| r["attname"] }
560
- end
561
-
562
- def max_id(table, primary_key, below: nil, where: nil)
563
- query = "SELECT MAX(#{quote_ident(primary_key)}) FROM #{quote_table(table)}"
564
- conditions = []
565
- conditions << "#{quote_ident(primary_key)} <= #{below}" if below
566
- conditions << where if where
567
- query << " WHERE #{conditions.join(" AND ")}" if conditions.any?
568
- execute(query)[0]["max"].to_i
569
- end
570
-
571
- def min_id(table, primary_key, column, cast, starting_time, where)
572
- query = "SELECT MIN(#{quote_ident(primary_key)}) FROM #{quote_table(table)}"
573
- conditions = []
574
- conditions << "#{quote_ident(column)} >= #{sql_date(starting_time, cast)}" if starting_time
575
- conditions << where if where
576
- query << " WHERE #{conditions.join(" AND ")}" if conditions.any?
577
- (execute(query)[0]["min"] || 1).to_i
578
- end
579
-
580
- def has_trigger?(trigger_name, table)
581
- !fetch_trigger(trigger_name, table).nil?
582
- end
583
-
584
- def fetch_comment(table)
585
- execute("SELECT obj_description(#{regclass(table)}) AS comment")[0]
586
- end
587
-
588
- # http://www.dbforums.com/showthread.php?1667561-How-to-list-sequences-and-the-columns-by-SQL
589
- def sequences(table)
590
- query = <<-SQL
591
- SELECT
592
- a.attname as related_column,
593
- s.relname as sequence_name
594
- FROM pg_class s
595
- JOIN pg_depend d ON d.objid = s.oid
596
- JOIN pg_class t ON d.objid = s.oid AND d.refobjid = t.oid
597
- JOIN pg_attribute a ON (d.refobjid, d.refobjsubid) = (a.attrelid, a.attnum)
598
- JOIN pg_namespace n ON n.oid = s.relnamespace
599
- WHERE s.relkind = 'S'
600
- AND n.nspname = $1
601
- AND t.relname = $2
602
- SQL
603
- execute(query, [schema, table])
604
- end
605
-
606
- # helpers
607
-
608
- def trigger_name(table)
609
- "#{table.split(".")[-1]}_insert_trigger"
610
- end
611
-
612
- def intermediate_name(table)
613
- "#{table}_intermediate"
614
- end
615
-
616
- def retired_name(table)
617
- "#{table}_retired"
618
- end
619
-
620
- def column_cast(table, column)
621
- data_type = execute("SELECT data_type FROM information_schema.columns WHERE table_schema || '.' || table_name = $1 AND column_name = $2", [table, column])[0]["data_type"]
622
- data_type == "timestamp with time zone" ? "timestamptz" : "date"
623
- end
624
-
625
- def sql_date(time, cast, add_cast = true)
626
- if cast == "timestamptz"
627
- fmt = "%Y-%m-%d %H:%M:%S UTC"
628
- else
629
- fmt = "%Y-%m-%d"
630
- end
631
- str = "'#{time.strftime(fmt)}'"
632
- add_cast ? "#{str}::#{cast}" : str
633
- end
634
-
635
- def name_format(period)
636
- case period.to_sym
637
- when :day
638
- "%Y%m%d"
639
- else
640
- "%Y%m"
641
- end
642
- end
643
-
644
- def round_date(date, period)
645
- date = date.to_date
646
- case period.to_sym
647
- when :day
648
- date
649
- else
650
- Date.new(date.year, date.month)
651
- end
652
- end
653
-
654
- def advance_date(date, period, count = 1)
655
- date = date.to_date
656
- case period.to_sym
657
- when :day
658
- date.next_day(count)
659
- else
660
- date.next_month(count)
661
- end
662
- end
663
-
664
- def quote_ident(value)
665
- PG::Connection.quote_ident(value)
666
- end
667
-
668
- def quote_table(table)
669
- table.split(".", 2).map { |v| quote_ident(v) }.join(".")
670
- end
671
-
672
- def quote_no_schema(table)
673
- quote_ident(table.split(".", 2)[-1])
674
- end
675
-
676
- def regclass(table)
677
- "'#{quote_table(table)}'::regclass"
678
- end
679
-
680
- def fetch_trigger(trigger_name, table)
681
- execute("SELECT obj_description(oid, 'pg_trigger') AS comment FROM pg_trigger WHERE tgname = $1 AND tgrelid = #{regclass(table)}", [trigger_name])[0]
682
- end
683
-
684
- def qualify_table(table)
685
- table.to_s.include?(".") ? table : [schema, table].join(".")
686
- end
687
-
688
- def settings_from_trigger(original_table, table)
689
- trigger_name = self.trigger_name(original_table)
690
-
691
- needs_comment = false
692
- trigger_comment = fetch_trigger(trigger_name, table)
693
- comment = trigger_comment || fetch_comment(table)
694
- if comment
695
- field, period, cast = comment["comment"].split(",").map { |v| v.split(":").last } rescue [nil, nil, nil]
696
- end
697
-
698
- unless period
699
- needs_comment = true
700
- function_def = execute("select pg_get_functiondef(oid) from pg_proc where proname = $1", [trigger_name])[0]
701
- return [] unless function_def
702
- function_def = function_def["pg_get_functiondef"]
703
- sql_format = SQL_FORMAT.find { |_, f| function_def.include?("'#{f}'") }
704
- return [] unless sql_format
705
- period = sql_format[0]
706
- field = /to_char\(NEW\.(\w+),/.match(function_def)[1]
707
- end
708
-
709
- # backwards compatibility with 0.2.3 and earlier (pre-timestamptz support)
710
- unless cast
711
- cast = "date"
712
- # update comment to explicitly define cast
713
- needs_comment = true
714
- end
715
-
716
- [period, field, cast, needs_comment, !trigger_comment]
717
- end
5
+ # stdlib
6
+ require "cgi"
7
+ require "time"
718
8
 
719
- def foreign_keys(table)
720
- execute("SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = #{regclass(table)} AND contype ='f'").map { |r| r["pg_get_constraintdef"] }
721
- end
9
+ # modules
10
+ require "pgslice/helpers"
11
+ require "pgslice/table"
12
+ require "pgslice/version"
722
13
 
723
- def server_version_num
724
- execute("SHOW server_version_num").first["server_version_num"].to_i
725
- end
726
- end
727
- end
14
+ # commands
15
+ require "pgslice/cli"
16
+ require "pgslice/cli/add_partitions"
17
+ require "pgslice/cli/analyze"
18
+ require "pgslice/cli/fill"
19
+ require "pgslice/cli/prep"
20
+ require "pgslice/cli/swap"
21
+ require "pgslice/cli/unprep"
22
+ require "pgslice/cli/unswap"