pgslice 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08f3d08e8d7f2f60615587f8c842b8a80e59f10d4f00d299422ca44caef031d7'
4
- data.tar.gz: 889bcd29c7b381ffdcdc0a32150a512243950dc106dec9d34d791f50794a3bb1
3
+ metadata.gz: d02cf8d6926bc4ed8908f1c9cc06bf406a3a926d99c08cba1e23f1ef8279f8fc
4
+ data.tar.gz: 4d68a0f7e771714d488f5e1c0bb4410f342f9f2e7e561fece795ee8d90b0e479
5
5
  SHA512:
6
- metadata.gz: 91319129faa191528b4808ade4138310969cd95c256b7beaba307947f5b962027cde2965a90122bceb84bcc5c4eafcca65d3aa27638b6cce4383963f5e5c1d10
7
- data.tar.gz: 9b270632d217dc19e74d479bf15ef9378aae34b813c56904fa8d7ccd10f8cdfc8d8db1a049826b1bb0a03539929de551510a1520a1df9c6ccb4cfbf6fb71cb56
6
+ metadata.gz: 419d9ca61bc0fd990d5680fbb35c9b8a2de88b169b2882c5b28a8233082bf5f2c75e0cb9c531fb9538d0815777500ea773e2a61fff4d93b1e481ae81dc2d48a2
7
+ data.tar.gz: 55555b66db960941e2d766af939637d10b6fac19301ee5b3eb141ccb860963fdbdfd98bb2fac0f459184f75b084d1d463e095d644efc16161dae18285b36f66a
@@ -1,3 +1,8 @@
1
+ ## 0.4.3
2
+
3
+ - Fixed sequence ownership
4
+ - Improved help
5
+
1
6
  ## 0.4.2
2
7
 
3
8
  - Added support for Postgres 11 index improvements
data/README.md CHANGED
@@ -365,6 +365,20 @@ Once a table is partitioned, here’s how to change the schema:
365
365
 
366
366
  Postgres 10 introduces [declarative partitioning](https://www.postgresql.org/docs/10/static/ddl-partitioning.html#ddl-partitioning-declarative). A major benefit is `INSERT` statements with a `RETURNING` clause work as expected. If you prefer to use trigger-based partitioning instead (not recommended), pass the `--trigger-based` option to the `prep` command.
367
367
 
368
+ ## Data Protection
369
+
370
+ When connecting to a remote database, make sure your connection is secure.
371
+
372
+ If you do not use a VPN, you must use `sslmode=verify-full` with a root certificate to [protect against MITM attacks](https://www.postgresql.org/docs/current/static/libpq-ssl.html). If you don’t do this, your database credentials can be compromised. This cannot be understated!
373
+
374
+ Surprisingly and unfortunately, there’s [not a secure way](https://thusoy.com/2016/mitming-postgres) to connect to Heroku Postgres with any client.
375
+
376
+ For Amazon RDS, download the [root certificate](https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem) and append to your database url:
377
+
378
+ ```
379
+ ?sslmode=verify-full&sslrootcert=rds-combined-ca-bundle.pem
380
+ ```
381
+
368
382
  ## Upgrading
369
383
 
370
384
  Run:
@@ -2,9 +2,7 @@
2
2
 
3
3
  require "pgslice"
4
4
  begin
5
- PgSlice::Client.new(ARGV).perform
6
- rescue PgSlice::Error => e
7
- abort e.message
8
- rescue Interrupt => e
5
+ PgSlice::Client.start
6
+ rescue Interrupt
9
7
  abort
10
8
  end
@@ -1,744 +1,10 @@
1
- require "pgslice/version"
2
- require "slop"
3
- require "pg"
1
+ # dependencies
4
2
  require "cgi"
3
+ require "thor"
4
+ require "pg"
5
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
- if server_version_num >= 110000
86
- index_defs = execute("SELECT pg_get_indexdef(indexrelid) FROM pg_index WHERE indrelid = #{regclass(table)} AND indisprimary = 'f'").map { |r| r["pg_get_indexdef"] }
87
- index_defs.each do |index_def|
88
- queries << index_def.sub(/ ON \S+ USING /, " ON #{quote_table(intermediate_table)} USING ").sub(/ INDEX .+ ON /, " INDEX ON ") + ";"
89
- end
90
- end
91
-
92
- # add comment
93
- cast = column_cast(table, column)
94
- queries << <<-SQL
95
- COMMENT ON TABLE #{quote_table(intermediate_table)} is 'column:#{column},period:#{period},cast:#{cast}';
96
- SQL
97
- else
98
- queries << <<-SQL
99
- CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} INCLUDING ALL);
100
- SQL
101
-
102
- foreign_keys(table).each do |fk_def|
103
- queries << "ALTER TABLE #{quote_table(intermediate_table)} ADD #{fk_def};"
104
- end
105
- end
106
-
107
- if !options[:no_partition] && !declarative
108
- sql_format = SQL_FORMAT[period.to_sym]
109
- queries << <<-SQL
110
- CREATE FUNCTION #{quote_ident(trigger_name)}()
111
- RETURNS trigger AS $$
112
- BEGIN
113
- RAISE EXCEPTION 'Create partitions first.';
114
- END;
115
- $$ LANGUAGE plpgsql;
116
- SQL
117
-
118
- queries << <<-SQL
119
- CREATE TRIGGER #{quote_ident(trigger_name)}
120
- BEFORE INSERT ON #{quote_table(intermediate_table)}
121
- FOR EACH ROW EXECUTE PROCEDURE #{quote_ident(trigger_name)}();
122
- SQL
123
-
124
- cast = column_cast(table, column)
125
- queries << <<-SQL
126
- COMMENT ON TRIGGER #{quote_ident(trigger_name)} ON #{quote_table(intermediate_table)} is 'column:#{column},period:#{period},cast:#{cast}';
127
- SQL
128
- end
129
-
130
- run_queries(queries)
131
- end
132
-
133
- def unprep
134
- table = qualify_table(arguments.first)
135
- intermediate_table = "#{table}_intermediate"
136
- trigger_name = self.trigger_name(table)
137
-
138
- abort "Usage: pgslice unprep <table>" if arguments.length != 1
139
- abort "Table not found: #{intermediate_table}" unless table_exists?(intermediate_table)
140
-
141
- queries = [
142
- "DROP TABLE #{quote_table(intermediate_table)} CASCADE;",
143
- "DROP FUNCTION IF EXISTS #{quote_ident(trigger_name)}();"
144
- ]
145
- run_queries(queries)
146
- end
147
-
148
- def add_partitions
149
- original_table = qualify_table(arguments.first)
150
- table = options[:intermediate] ? "#{original_table}_intermediate" : original_table
151
- trigger_name = self.trigger_name(original_table)
152
-
153
- abort "Usage: pgslice add_partitions <table>" if arguments.length != 1
154
- abort "Table not found: #{table}" unless table_exists?(table)
155
-
156
- future = options[:future]
157
- past = options[:past]
158
- range = (-1 * past)..future
159
-
160
- period, field, cast, needs_comment, declarative = settings_from_trigger(original_table, table)
161
- unless period
162
- message = "No settings found: #{table}"
163
- message = "#{message}\nDid you mean to use --intermediate?" unless options[:intermediate]
164
- abort message
165
- end
166
-
167
- queries = []
168
-
169
- if needs_comment
170
- queries << "COMMENT ON TRIGGER #{quote_ident(trigger_name)} ON #{quote_table(table)} is 'column:#{field},period:#{period},cast:#{cast}';"
171
- end
172
-
173
- # today = utc date
174
- today = round_date(DateTime.now.new_offset(0).to_date, period)
175
-
176
- schema_table =
177
- if !declarative
178
- table
179
- elsif options[:intermediate]
180
- original_table
181
- else
182
- existing_partitions(original_table, period).last
183
- end
184
-
185
- # indexes automatically propagate in Postgres 11+
186
- index_defs =
187
- if !declarative || server_version_num < 110000
188
- execute("SELECT pg_get_indexdef(indexrelid) FROM pg_index WHERE indrelid = #{regclass(schema_table)} AND indisprimary = 'f'").map { |r| r["pg_get_indexdef"] }
189
- else
190
- []
191
- end
192
-
193
- fk_defs = foreign_keys(schema_table)
194
- primary_key = self.primary_key(schema_table)
195
-
196
- added_partitions = []
197
- range.each do |n|
198
- day = advance_date(today, period, n)
199
-
200
- partition_name = "#{original_table}_#{day.strftime(name_format(period))}"
201
- next if table_exists?(partition_name)
202
- added_partitions << partition_name
203
-
204
- if declarative
205
- queries << <<-SQL
206
- 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)});
207
- SQL
208
- else
209
- queries << <<-SQL
210
- CREATE TABLE #{quote_table(partition_name)}
211
- (CHECK (#{quote_ident(field)} >= #{sql_date(day, cast)} AND #{quote_ident(field)} < #{sql_date(advance_date(day, period, 1), cast)}))
212
- INHERITS (#{quote_table(table)});
213
- SQL
214
- end
215
-
216
- queries << "ALTER TABLE #{quote_table(partition_name)} ADD PRIMARY KEY (#{primary_key.map { |k| quote_ident(k) }.join(", ")});" if primary_key.any?
217
-
218
- index_defs.each do |index_def|
219
- queries << index_def.sub(/ ON \S+ USING /, " ON #{quote_table(partition_name)} USING ").sub(/ INDEX .+ ON /, " INDEX ON ") + ";"
220
- end
221
-
222
- fk_defs.each do |fk_def|
223
- queries << "ALTER TABLE #{quote_table(partition_name)} ADD #{fk_def};"
224
- end
225
- end
226
-
227
- unless declarative
228
- # update trigger based on existing partitions
229
- current_defs = []
230
- future_defs = []
231
- past_defs = []
232
- name_format = self.name_format(period)
233
- existing_tables = existing_partitions(original_table, period)
234
- existing_tables = (existing_tables + added_partitions).uniq.sort
235
-
236
- existing_tables.each do |table|
237
- day = DateTime.strptime(table.split("_").last, name_format)
238
- partition_name = "#{original_table}_#{day.strftime(name_format(period))}"
239
-
240
- sql = "(NEW.#{quote_ident(field)} >= #{sql_date(day, cast)} AND NEW.#{quote_ident(field)} < #{sql_date(advance_date(day, period, 1), cast)}) THEN
241
- INSERT INTO #{quote_table(partition_name)} VALUES (NEW.*);"
242
-
243
- if day.to_date < today
244
- past_defs << sql
245
- elsif advance_date(day, period, 1) < today
246
- current_defs << sql
247
- else
248
- future_defs << sql
249
- end
250
- end
251
-
252
- # order by current period, future periods asc, past periods desc
253
- trigger_defs = current_defs + future_defs + past_defs.reverse
254
-
255
- if trigger_defs.any?
256
- queries << <<-SQL
257
- CREATE OR REPLACE FUNCTION #{quote_ident(trigger_name)}()
258
- RETURNS trigger AS $$
259
- BEGIN
260
- IF #{trigger_defs.join("\n ELSIF ")}
261
- ELSE
262
- RAISE EXCEPTION 'Date out of range. Ensure partitions are created.';
263
- END IF;
264
- RETURN NULL;
265
- END;
266
- $$ LANGUAGE plpgsql;
267
- SQL
268
- end
269
- end
270
-
271
- run_queries(queries) if queries.any?
272
- end
273
-
274
- def fill
275
- table = qualify_table(arguments.first)
276
-
277
- abort "Usage: pgslice fill <table>" if arguments.length != 1
278
-
279
- source_table = options[:source_table]
280
- dest_table = options[:dest_table]
281
-
282
- if options[:swapped]
283
- source_table ||= retired_name(table)
284
- dest_table ||= table
285
- else
286
- source_table ||= table
287
- dest_table ||= intermediate_name(table)
288
- end
289
-
290
- abort "Table not found: #{source_table}" unless table_exists?(source_table)
291
- abort "Table not found: #{dest_table}" unless table_exists?(dest_table)
292
-
293
- period, field, cast, needs_comment, declarative = settings_from_trigger(table, dest_table)
294
-
295
- if period
296
- name_format = self.name_format(period)
297
-
298
- existing_tables = existing_partitions(table, period)
299
- if existing_tables.any?
300
- starting_time = DateTime.strptime(existing_tables.first.split("_").last, name_format)
301
- ending_time = advance_date(DateTime.strptime(existing_tables.last.split("_").last, name_format), period, 1)
302
- end
303
- end
304
-
305
- schema_table = period && declarative ? existing_tables.last : table
306
-
307
- primary_key = self.primary_key(schema_table)[0]
308
- abort "No primary key" unless primary_key
309
-
310
- max_source_id = nil
311
- begin
312
- max_source_id = max_id(source_table, primary_key)
313
- rescue PG::UndefinedFunction
314
- abort "Only numeric primary keys are supported"
315
- end
316
-
317
- max_dest_id =
318
- if options[:start]
319
- options[:start]
320
- elsif options[:swapped]
321
- max_id(dest_table, primary_key, where: options[:where], below: max_source_id)
322
- else
323
- max_id(dest_table, primary_key, where: options[:where])
324
- end
325
-
326
- if max_dest_id == 0 && !options[:swapped]
327
- min_source_id = min_id(source_table, primary_key, field, cast, starting_time, options[:where])
328
- max_dest_id = min_source_id - 1 if min_source_id
329
- end
330
-
331
- starting_id = max_dest_id
332
- fields = columns(source_table).map { |c| quote_ident(c) }.join(", ")
333
- batch_size = options[:batch_size]
334
-
335
- i = 1
336
- batch_count = ((max_source_id - starting_id) / batch_size.to_f).ceil
337
-
338
- if batch_count == 0
339
- log_sql "/* nothing to fill */"
340
- end
341
-
342
- while starting_id < max_source_id
343
- where = "#{quote_ident(primary_key)} > #{starting_id} AND #{quote_ident(primary_key)} <= #{starting_id + batch_size}"
344
- if starting_time
345
- where << " AND #{quote_ident(field)} >= #{sql_date(starting_time, cast)} AND #{quote_ident(field)} < #{sql_date(ending_time, cast)}"
346
- end
347
- if options[:where]
348
- where << " AND #{options[:where]}"
349
- end
350
-
351
- query = <<-SQL
352
- /* #{i} of #{batch_count} */
353
- INSERT INTO #{quote_table(dest_table)} (#{fields})
354
- SELECT #{fields} FROM #{quote_table(source_table)}
355
- WHERE #{where}
356
- SQL
357
-
358
- run_query(query)
359
-
360
- starting_id += batch_size
361
- i += 1
362
-
363
- if options[:sleep] && starting_id <= max_source_id
364
- sleep(options[:sleep])
365
- end
366
- end
367
- end
368
-
369
- def swap
370
- table = qualify_table(arguments.first)
371
- intermediate_table = intermediate_name(table)
372
- retired_table = retired_name(table)
373
-
374
- abort "Usage: pgslice swap <table>" if arguments.length != 1
375
- abort "Table not found: #{table}" unless table_exists?(table)
376
- abort "Table not found: #{intermediate_table}" unless table_exists?(intermediate_table)
377
- abort "Table already exists: #{retired_table}" if table_exists?(retired_table)
378
-
379
- queries = [
380
- "ALTER TABLE #{quote_table(table)} RENAME TO #{quote_no_schema(retired_table)};",
381
- "ALTER TABLE #{quote_table(intermediate_table)} RENAME TO #{quote_no_schema(table)};"
382
- ]
383
-
384
- self.sequences(table).each do |sequence|
385
- queries << "ALTER SEQUENCE #{quote_ident(sequence["sequence_name"])} OWNED BY #{quote_ident(table)}.#{quote_ident(sequence["related_column"])};"
386
- end
387
-
388
- queries.unshift("SET LOCAL lock_timeout = '#{options[:lock_timeout]}';") if server_version_num >= 90300
389
-
390
- run_queries(queries)
391
- end
392
-
393
- def unswap
394
- table = qualify_table(arguments.first)
395
- intermediate_table = intermediate_name(table)
396
- retired_table = retired_name(table)
397
-
398
- abort "Usage: pgslice unswap <table>" if arguments.length != 1
399
- abort "Table not found: #{table}" unless table_exists?(table)
400
- abort "Table not found: #{retired_table}" unless table_exists?(retired_table)
401
- abort "Table already exists: #{intermediate_table}" if table_exists?(intermediate_table)
402
-
403
- queries = [
404
- "ALTER TABLE #{quote_table(table)} RENAME TO #{quote_no_schema(intermediate_table)};",
405
- "ALTER TABLE #{quote_table(retired_table)} RENAME TO #{quote_no_schema(table)};"
406
- ]
407
-
408
- self.sequences(table).each do |sequence|
409
- queries << "ALTER SEQUENCE #{quote_ident(sequence["sequence_name"])} OWNED BY #{quote_ident(table)}.#{quote_ident(sequence["related_column"])};"
410
- end
411
-
412
- run_queries(queries)
413
- end
414
-
415
- def analyze
416
- table = qualify_table(arguments.first)
417
- parent_table = options[:swapped] ? table : intermediate_name(table)
418
-
419
- abort "Usage: pgslice analyze <table>" if arguments.length != 1
420
-
421
- existing_tables = existing_partitions(table)
422
- analyze_list = existing_tables + [parent_table]
423
- run_queries_without_transaction analyze_list.map { |t| "ANALYZE VERBOSE #{quote_table(t)};" }
424
- end
425
-
426
- # arguments
427
-
428
- def parse_args(args)
429
- opts = Slop.parse(args) do |o|
430
- o.boolean "--intermediate"
431
- o.boolean "--swapped"
432
- o.float "--sleep"
433
- o.integer "--future", default: 0
434
- o.integer "--past", default: 0
435
- o.integer "--batch-size", default: 10000
436
- o.boolean "--dry-run", default: false
437
- o.boolean "--no-partition", default: false
438
- o.boolean "--trigger-based", default: false
439
- o.integer "--start"
440
- o.string "--url"
441
- o.string "--source-table"
442
- o.string "--dest-table"
443
- o.string "--where"
444
- o.string "--lock-timeout", default: "5s"
445
- o.on "-v", "--version", "print the version" do
446
- log PgSlice::VERSION
447
- @exit = true
448
- end
449
- end
450
- @arguments = opts.arguments
451
- @options = opts.to_hash
452
- rescue Slop::Error => e
453
- abort e.message
454
- end
455
-
456
- # output
457
-
458
- def log(message = nil)
459
- $stderr.puts message
460
- end
461
-
462
- def log_sql(message = nil)
463
- $stdout.puts message
464
- end
465
-
466
- def abort(message)
467
- raise PgSlice::Error, message
468
- end
469
-
470
- # database connection
471
-
472
- def connection
473
- @connection ||= begin
474
- url = options[:url] || ENV["PGSLICE_URL"]
475
- abort "Set PGSLICE_URL or use the --url option" unless url
476
-
477
- uri = URI.parse(url)
478
- params = CGI.parse(uri.query.to_s)
479
- # remove schema
480
- @schema = Array(params.delete("schema") || "public")[0]
481
- uri.query = URI.encode_www_form(params)
482
-
483
- ENV["PGCONNECT_TIMEOUT"] ||= "1"
484
- PG::Connection.new(uri.to_s)
485
- end
486
- rescue PG::ConnectionBad => e
487
- abort e.message
488
- rescue URI::InvalidURIError
489
- abort "Invalid url"
490
- end
491
-
492
- def schema
493
- connection # ensure called first
494
- @schema
495
- end
496
-
497
- def execute(query, params = [])
498
- connection.exec_params(query, params).to_a
499
- end
500
-
501
- def run_queries(queries)
502
- connection.transaction do
503
- execute("SET LOCAL client_min_messages TO warning") unless options[:dry_run]
504
- log_sql "BEGIN;"
505
- log_sql
506
- run_queries_without_transaction(queries)
507
- log_sql "COMMIT;"
508
- end
509
- end
510
-
511
- def run_query(query)
512
- log_sql query
513
- unless options[:dry_run]
514
- begin
515
- execute(query)
516
- rescue PG::ServerError => e
517
- abort("#{e.class.name}: #{e.message}")
518
- end
519
- end
520
- log_sql
521
- end
522
-
523
- def run_queries_without_transaction(queries)
524
- queries.each do |query|
525
- run_query(query)
526
- end
527
- end
528
-
529
- def server_version_num
530
- execute("SHOW server_version_num")[0]["server_version_num"].to_i
531
- end
532
-
533
- def existing_partitions(table, period = nil)
534
- count =
535
- case period
536
- when "day"
537
- 8
538
- when "month"
539
- 6
540
- else
541
- "6,8"
542
- end
543
-
544
- existing_tables(like: "#{table}_%").select { |t| /\A#{Regexp.escape("#{table}_")}\d{#{count}}\z/.match(t) }
545
- end
546
-
547
- def existing_tables(like:)
548
- query = "SELECT schemaname, tablename FROM pg_catalog.pg_tables WHERE schemaname = $1 AND tablename LIKE $2"
549
- execute(query, like.split(".", 2)).map { |r| "#{r["schemaname"]}.#{r["tablename"]}" }.sort
550
- end
551
-
552
- def table_exists?(table)
553
- existing_tables(like: table).any?
554
- end
555
-
556
- def columns(table)
557
- execute("SELECT column_name FROM information_schema.columns WHERE table_schema || '.' || table_name = $1", [table]).map{ |r| r["column_name"] }
558
- end
559
-
560
- # http://stackoverflow.com/a/20537829
561
- def primary_key(table)
562
- query = <<-SQL
563
- SELECT
564
- pg_attribute.attname,
565
- format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
566
- FROM
567
- pg_index, pg_class, pg_attribute, pg_namespace
568
- WHERE
569
- nspname || '.' || relname = $1 AND
570
- indrelid = pg_class.oid AND
571
- pg_class.relnamespace = pg_namespace.oid AND
572
- pg_attribute.attrelid = pg_class.oid AND
573
- pg_attribute.attnum = any(pg_index.indkey) AND
574
- indisprimary
575
- SQL
576
- execute(query, [table]).map { |r| r["attname"] }
577
- end
578
-
579
- def max_id(table, primary_key, below: nil, where: nil)
580
- query = "SELECT MAX(#{quote_ident(primary_key)}) FROM #{quote_table(table)}"
581
- conditions = []
582
- conditions << "#{quote_ident(primary_key)} <= #{below}" if below
583
- conditions << where if where
584
- query << " WHERE #{conditions.join(" AND ")}" if conditions.any?
585
- execute(query)[0]["max"].to_i
586
- end
587
-
588
- def min_id(table, primary_key, column, cast, starting_time, where)
589
- query = "SELECT MIN(#{quote_ident(primary_key)}) FROM #{quote_table(table)}"
590
- conditions = []
591
- conditions << "#{quote_ident(column)} >= #{sql_date(starting_time, cast)}" if starting_time
592
- conditions << where if where
593
- query << " WHERE #{conditions.join(" AND ")}" if conditions.any?
594
- (execute(query)[0]["min"] || 1).to_i
595
- end
596
-
597
- def has_trigger?(trigger_name, table)
598
- !fetch_trigger(trigger_name, table).nil?
599
- end
600
-
601
- def fetch_comment(table)
602
- execute("SELECT obj_description(#{regclass(table)}) AS comment")[0]
603
- end
604
-
605
- # http://www.dbforums.com/showthread.php?1667561-How-to-list-sequences-and-the-columns-by-SQL
606
- def sequences(table)
607
- query = <<-SQL
608
- SELECT
609
- a.attname as related_column,
610
- s.relname as sequence_name
611
- FROM pg_class s
612
- JOIN pg_depend d ON d.objid = s.oid
613
- JOIN pg_class t ON d.objid = s.oid AND d.refobjid = t.oid
614
- JOIN pg_attribute a ON (d.refobjid, d.refobjsubid) = (a.attrelid, a.attnum)
615
- JOIN pg_namespace n ON n.oid = s.relnamespace
616
- WHERE s.relkind = 'S'
617
- AND n.nspname = $1
618
- AND t.relname = $2
619
- SQL
620
- execute(query, [schema, table])
621
- end
622
-
623
- # helpers
624
-
625
- def trigger_name(table)
626
- "#{table.split(".")[-1]}_insert_trigger"
627
- end
628
-
629
- def intermediate_name(table)
630
- "#{table}_intermediate"
631
- end
632
-
633
- def retired_name(table)
634
- "#{table}_retired"
635
- end
636
-
637
- def column_cast(table, column)
638
- 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"]
639
- data_type == "timestamp with time zone" ? "timestamptz" : "date"
640
- end
641
-
642
- def sql_date(time, cast, add_cast = true)
643
- if cast == "timestamptz"
644
- fmt = "%Y-%m-%d %H:%M:%S UTC"
645
- else
646
- fmt = "%Y-%m-%d"
647
- end
648
- str = "'#{time.strftime(fmt)}'"
649
- add_cast ? "#{str}::#{cast}" : str
650
- end
651
-
652
- def name_format(period)
653
- case period.to_sym
654
- when :day
655
- "%Y%m%d"
656
- else
657
- "%Y%m"
658
- end
659
- end
660
-
661
- def round_date(date, period)
662
- date = date.to_date
663
- case period.to_sym
664
- when :day
665
- date
666
- else
667
- Date.new(date.year, date.month)
668
- end
669
- end
670
-
671
- def advance_date(date, period, count = 1)
672
- date = date.to_date
673
- case period.to_sym
674
- when :day
675
- date.next_day(count)
676
- else
677
- date.next_month(count)
678
- end
679
- end
680
-
681
- def quote_ident(value)
682
- PG::Connection.quote_ident(value)
683
- end
684
-
685
- def quote_table(table)
686
- table.split(".", 2).map { |v| quote_ident(v) }.join(".")
687
- end
688
-
689
- def quote_no_schema(table)
690
- quote_ident(table.split(".", 2)[-1])
691
- end
692
-
693
- def regclass(table)
694
- "'#{quote_table(table)}'::regclass"
695
- end
696
-
697
- def fetch_trigger(trigger_name, table)
698
- execute("SELECT obj_description(oid, 'pg_trigger') AS comment FROM pg_trigger WHERE tgname = $1 AND tgrelid = #{regclass(table)}", [trigger_name])[0]
699
- end
700
-
701
- def qualify_table(table)
702
- table.to_s.include?(".") ? table : [schema, table].join(".")
703
- end
704
-
705
- def settings_from_trigger(original_table, table)
706
- trigger_name = self.trigger_name(original_table)
707
-
708
- needs_comment = false
709
- trigger_comment = fetch_trigger(trigger_name, table)
710
- comment = trigger_comment || fetch_comment(table)
711
- if comment
712
- field, period, cast = comment["comment"].split(",").map { |v| v.split(":").last } rescue [nil, nil, nil]
713
- end
714
-
715
- unless period
716
- needs_comment = true
717
- function_def = execute("select pg_get_functiondef(oid) from pg_proc where proname = $1", [trigger_name])[0]
718
- return [] unless function_def
719
- function_def = function_def["pg_get_functiondef"]
720
- sql_format = SQL_FORMAT.find { |_, f| function_def.include?("'#{f}'") }
721
- return [] unless sql_format
722
- period = sql_format[0]
723
- field = /to_char\(NEW\.(\w+),/.match(function_def)[1]
724
- end
725
-
726
- # backwards compatibility with 0.2.3 and earlier (pre-timestamptz support)
727
- unless cast
728
- cast = "date"
729
- # update comment to explicitly define cast
730
- needs_comment = true
731
- end
732
-
733
- [period, field, cast, needs_comment, !trigger_comment]
734
- end
735
-
736
- def foreign_keys(table)
737
- execute("SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = #{regclass(table)} AND contype ='f'").map { |r| r["pg_get_constraintdef"] }
738
- end
739
-
740
- def server_version_num
741
- execute("SHOW server_version_num").first["server_version_num"].to_i
742
- end
743
- end
744
- end
6
+ # modules
7
+ require "pgslice/client"
8
+ require "pgslice/generic_table"
9
+ require "pgslice/table"
10
+ require "pgslice/version"