pgslice 0.4.1 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"