pgslice 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/README.md +107 -0
- data/Rakefile +10 -0
- data/exe/pgslice +10 -0
- data/lib/pgslice.rb +382 -0
- data/lib/pgslice/version.rb +3 -0
- data/pgslice.gemspec +26 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 09c384b3832fb897a3efde949c8d1d9c8e3e2d64
|
4
|
+
data.tar.gz: 6b28c7e711c74e88683a565af2f7b0b0f3de2f8a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 219a357d1eef81483fd613ed19d4e24cfc8ae3c0082a9791f01b3160df9e7d30ec77dac317d25255c76fdeb775425e0cf4c4aa19000bf75e1402e33ba6b48033
|
7
|
+
data.tar.gz: 073a77f7ab738968c466bbc05ba6b4415b451dd6808b110373844f321d337a671d9a999c4a7ba373d97f531179469204422ec93e3fbd26b742504cc6ca431c49
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# pgslice
|
2
|
+
|
3
|
+
Postgres partitioning as easy as pie
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
Run
|
8
|
+
|
9
|
+
```sh
|
10
|
+
gem install pgslice
|
11
|
+
```
|
12
|
+
|
13
|
+
## Steps
|
14
|
+
|
15
|
+
1. Specify your database credentials
|
16
|
+
|
17
|
+
```sh
|
18
|
+
export PGSLICE_URL=postgres://localhost/myapp_development
|
19
|
+
```
|
20
|
+
|
21
|
+
2. Create an intermediate table
|
22
|
+
|
23
|
+
```sh
|
24
|
+
pgslice prep <table> <column> <period>
|
25
|
+
```
|
26
|
+
|
27
|
+
Period can be `day` or `month`.
|
28
|
+
|
29
|
+
This creates a table named `<table>_intermediate` with the appropriate trigger for partitioning.
|
30
|
+
|
31
|
+
3. Add partitions
|
32
|
+
|
33
|
+
```sh
|
34
|
+
pgslice add_partitions <table> --intermediate --past 3 --future 3
|
35
|
+
```
|
36
|
+
|
37
|
+
This creates child tables that inherit from the intermediate table.
|
38
|
+
|
39
|
+
Use the `--past` and `--future` options to control the number of partitions.
|
40
|
+
|
41
|
+
4. *Optional, for tables with data* - Fill the partitions in batches with data from the original table
|
42
|
+
|
43
|
+
```sh
|
44
|
+
pgslice fill <table>
|
45
|
+
```
|
46
|
+
|
47
|
+
Use the `--batch-size` and `--sleep` options to control the speed.
|
48
|
+
|
49
|
+
5. Swap the intermediate table with the original table
|
50
|
+
|
51
|
+
```sh
|
52
|
+
pgslice swap <table>
|
53
|
+
```
|
54
|
+
|
55
|
+
The original table is renamed `<table>_retired` and the intermediate table is renamed `<table>`.
|
56
|
+
|
57
|
+
6. Fill the rest
|
58
|
+
|
59
|
+
```sh
|
60
|
+
pgslice fill <table> --swapped
|
61
|
+
```
|
62
|
+
|
63
|
+
7. Archive and drop the original table
|
64
|
+
|
65
|
+
## Adding Partitions
|
66
|
+
|
67
|
+
```sh
|
68
|
+
pgslice add_partitions <table> --future 3
|
69
|
+
```
|
70
|
+
|
71
|
+
## Additional Commands
|
72
|
+
|
73
|
+
To undo prep and delete partitions, use:
|
74
|
+
|
75
|
+
```sh
|
76
|
+
pgslice unprep <table>
|
77
|
+
```
|
78
|
+
|
79
|
+
To undo swap, use:
|
80
|
+
|
81
|
+
```sh
|
82
|
+
pgslice unswap <table>
|
83
|
+
```
|
84
|
+
|
85
|
+
## Upgrading
|
86
|
+
|
87
|
+
Run:
|
88
|
+
|
89
|
+
```sh
|
90
|
+
gem install pgslice
|
91
|
+
```
|
92
|
+
|
93
|
+
To use master, run:
|
94
|
+
|
95
|
+
```sh
|
96
|
+
gem install specific_install
|
97
|
+
gem specific_install ankane/pgslice
|
98
|
+
```
|
99
|
+
|
100
|
+
## Contributing
|
101
|
+
|
102
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
103
|
+
|
104
|
+
- [Report bugs](https://github.com/ankane/pgslice/issues)
|
105
|
+
- Fix bugs and [submit pull requests](https://github.com/ankane/pgslice/pulls)
|
106
|
+
- Write, clarify, or fix documentation
|
107
|
+
- Suggest or add new features
|
data/Rakefile
ADDED
data/exe/pgslice
ADDED
data/lib/pgslice.rb
ADDED
@@ -0,0 +1,382 @@
|
|
1
|
+
require "pgslice/version"
|
2
|
+
require "slop"
|
3
|
+
require "pg"
|
4
|
+
require "active_support/all"
|
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
|
+
parse_args(args)
|
19
|
+
@command = @arguments.shift
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
return if @exit
|
24
|
+
|
25
|
+
case @command
|
26
|
+
when "prep"
|
27
|
+
prep
|
28
|
+
when "add_partitions"
|
29
|
+
add_partitions
|
30
|
+
when "fill"
|
31
|
+
fill
|
32
|
+
when "swap"
|
33
|
+
swap
|
34
|
+
when "unswap"
|
35
|
+
unswap
|
36
|
+
when "unprep"
|
37
|
+
unprep
|
38
|
+
when nil
|
39
|
+
log "Commands: add_partitions, fill, prep, swap, unprep, unswap"
|
40
|
+
else
|
41
|
+
abort "Unknown command: #{@command}"
|
42
|
+
end
|
43
|
+
ensure
|
44
|
+
@connection.close if @connection
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
# commands
|
50
|
+
|
51
|
+
def prep
|
52
|
+
table, column, period = arguments
|
53
|
+
intermediate_table = "#{table}_intermediate"
|
54
|
+
trigger_name = self.trigger_name(table)
|
55
|
+
|
56
|
+
abort "Usage: pgslice prep <table> <column> <period>" if arguments.length != 3
|
57
|
+
abort "Table not found: #{table}" unless table_exists?(table)
|
58
|
+
abort "Table already exists: #{intermediate_table}" if table_exists?(intermediate_table)
|
59
|
+
abort "Column not found: #{column}" unless columns(table).include?(column)
|
60
|
+
abort "Invalid period: #{period}" unless SQL_FORMAT[period.to_sym]
|
61
|
+
|
62
|
+
log "Creating #{intermediate_table} from #{table}"
|
63
|
+
|
64
|
+
queries = []
|
65
|
+
|
66
|
+
queries << <<-SQL
|
67
|
+
CREATE TABLE #{intermediate_table} (
|
68
|
+
LIKE #{table} INCLUDING INDEXES INCLUDING DEFAULTS
|
69
|
+
)
|
70
|
+
SQL
|
71
|
+
|
72
|
+
sql_format = SQL_FORMAT[period.to_sym]
|
73
|
+
queries << <<-SQL
|
74
|
+
CREATE FUNCTION #{trigger_name}()
|
75
|
+
RETURNS trigger AS $$
|
76
|
+
BEGIN
|
77
|
+
EXECUTE 'INSERT INTO public.#{table}_' || to_char(NEW.#{column}, '#{sql_format}') || ' VALUES ($1.*)' USING NEW;
|
78
|
+
RETURN NULL;
|
79
|
+
END;
|
80
|
+
$$ LANGUAGE plpgsql
|
81
|
+
SQL
|
82
|
+
|
83
|
+
queries << <<-SQL
|
84
|
+
CREATE TRIGGER #{trigger_name}
|
85
|
+
BEFORE INSERT ON #{intermediate_table}
|
86
|
+
FOR EACH ROW EXECUTE PROCEDURE #{trigger_name}()
|
87
|
+
SQL
|
88
|
+
|
89
|
+
run_queries(queries)
|
90
|
+
end
|
91
|
+
|
92
|
+
def unprep
|
93
|
+
table = arguments.first
|
94
|
+
intermediate_table = "#{table}_intermediate"
|
95
|
+
trigger_name = self.trigger_name(table)
|
96
|
+
|
97
|
+
abort "Usage: pgslice unprep <table>" if arguments.length != 1
|
98
|
+
abort "Table not found: #{intermediate_table}" unless table_exists?(intermediate_table)
|
99
|
+
|
100
|
+
log "Dropping #{intermediate_table}"
|
101
|
+
|
102
|
+
queries = [
|
103
|
+
"DROP TABLE #{intermediate_table} CASCADE",
|
104
|
+
"DROP FUNCTION #{trigger_name}()"
|
105
|
+
]
|
106
|
+
run_queries(queries)
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_partitions
|
110
|
+
original_table = arguments.first
|
111
|
+
table = options[:intermediate] ? "#{original_table}_intermediate" : original_table
|
112
|
+
trigger_name = self.trigger_name(original_table)
|
113
|
+
|
114
|
+
abort "Usage: pgslice add_partitions <table>" if arguments.length != 1
|
115
|
+
abort "Table not found: #{table}" unless table_exists?(table)
|
116
|
+
|
117
|
+
future = options[:future]
|
118
|
+
past = options[:past]
|
119
|
+
range = (-1 * past)..future
|
120
|
+
|
121
|
+
# ensure table has trigger
|
122
|
+
abort "No trigger on table: #{table}\nDid you mean to use --intermediate?" unless has_trigger?(trigger_name, table)
|
123
|
+
|
124
|
+
period, field, name_format, inc, today = settings_from_table(original_table, table)
|
125
|
+
|
126
|
+
days = range.map { |n| today + (n * inc) }
|
127
|
+
queries = []
|
128
|
+
|
129
|
+
days.each do |day|
|
130
|
+
partition_name = "#{original_table}_#{day.strftime(name_format)}"
|
131
|
+
next if table_exists?(partition_name)
|
132
|
+
|
133
|
+
log "Creating #{partition_name} from #{table}"
|
134
|
+
date_format = "%Y-%m-%d"
|
135
|
+
|
136
|
+
queries << <<-SQL
|
137
|
+
CREATE TABLE #{partition_name} (
|
138
|
+
LIKE #{table} INCLUDING INDEXES INCLUDING DEFAULTS,
|
139
|
+
CHECK (#{field} >= '#{day.strftime(date_format)}'::date AND #{field} < '#{(day + inc).strftime(date_format)}'::date)
|
140
|
+
) INHERITS (#{table})
|
141
|
+
SQL
|
142
|
+
end
|
143
|
+
|
144
|
+
run_queries(queries) if queries.any?
|
145
|
+
end
|
146
|
+
|
147
|
+
def fill
|
148
|
+
table = arguments.first
|
149
|
+
|
150
|
+
abort "Usage: pgslice fill <table>" if arguments.length != 1
|
151
|
+
|
152
|
+
if options[:swapped]
|
153
|
+
source_table = retired_name(table)
|
154
|
+
dest_table = table
|
155
|
+
else
|
156
|
+
source_table = table
|
157
|
+
dest_table = intermediate_name(table)
|
158
|
+
end
|
159
|
+
|
160
|
+
abort "Table not found: #{source_table}" unless table_exists?(source_table)
|
161
|
+
abort "Table not found: #{dest_table}" unless table_exists?(dest_table)
|
162
|
+
|
163
|
+
period, field, name_format, inc, today = settings_from_table(table, dest_table)
|
164
|
+
|
165
|
+
date_format = "%Y-%m-%d"
|
166
|
+
existing_tables = self.existing_tables(like: "#{table}_%").select { |t| /#{Regexp.escape("#{table}_")}(\d{4,6})/.match(t) }
|
167
|
+
starting_time = DateTime.strptime(existing_tables.first.last(8), name_format)
|
168
|
+
ending_time = DateTime.strptime(existing_tables.last.last(8), name_format) + inc
|
169
|
+
|
170
|
+
primary_key = self.primary_key(table)
|
171
|
+
max_source_id = max_id(source_table, primary_key)
|
172
|
+
max_dest_id = max_id(dest_table, primary_key)
|
173
|
+
|
174
|
+
starting_id = max_dest_id + 1
|
175
|
+
fields = columns(source_table).join(", ")
|
176
|
+
batch_size = options[:batch_size]
|
177
|
+
|
178
|
+
if starting_id < max_source_id
|
179
|
+
log "#{primary_key}: #{starting_id} -> #{max_source_id}"
|
180
|
+
log "time: #{starting_time.to_date} -> #{ending_time.to_date}"
|
181
|
+
|
182
|
+
while starting_id < max_source_id
|
183
|
+
log "#{starting_id}..#{[starting_id + batch_size - 1, max_source_id].min}"
|
184
|
+
|
185
|
+
query = "INSERT INTO #{dest_table} (#{fields}) SELECT #{fields} FROM #{source_table} WHERE #{primary_key} >= #{starting_id} AND #{primary_key} < #{starting_id + batch_size} AND #{field} >= '#{starting_time.strftime(date_format)}'::date AND #{field} < '#{ending_time.strftime(date_format)}'::date"
|
186
|
+
log query if options[:debug]
|
187
|
+
execute(query)
|
188
|
+
|
189
|
+
starting_id += batch_size
|
190
|
+
|
191
|
+
if options[:sleep] && starting_id < max_source_id
|
192
|
+
sleep(options[:sleep])
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def swap
|
199
|
+
table = arguments.first
|
200
|
+
intermediate_table = intermediate_name(table)
|
201
|
+
retired_table = retired_name(table)
|
202
|
+
|
203
|
+
abort "Usage: pgslice swap <table>" if arguments.length != 1
|
204
|
+
abort "Table not found: #{table}" unless table_exists?(table)
|
205
|
+
abort "Table not found: #{intermediate_table}" unless table_exists?(intermediate_table)
|
206
|
+
abort "Table already exists: #{retired_table}" if table_exists?(retired_table)
|
207
|
+
|
208
|
+
log "Renaming #{table} to #{retired_table}"
|
209
|
+
log "Renaming #{intermediate_table} to #{table}"
|
210
|
+
|
211
|
+
queries = [
|
212
|
+
"ALTER TABLE #{table} RENAME TO #{retired_table}",
|
213
|
+
"ALTER TABLE #{intermediate_table} RENAME TO #{table}"
|
214
|
+
]
|
215
|
+
run_queries(queries)
|
216
|
+
end
|
217
|
+
|
218
|
+
def unswap
|
219
|
+
table = arguments.first
|
220
|
+
intermediate_table = intermediate_name(table)
|
221
|
+
retired_table = retired_name(table)
|
222
|
+
|
223
|
+
abort "Usage: pgslice unswap <table>" if arguments.length != 1
|
224
|
+
abort "Table not found: #{table}" unless table_exists?(table)
|
225
|
+
abort "Table not found: #{retired_table}" unless table_exists?(retired_table)
|
226
|
+
abort "Table already exists: #{intermediate_table}" if table_exists?(intermediate_table)
|
227
|
+
|
228
|
+
log "Renaming #{table} to #{intermediate_table}"
|
229
|
+
log "Renaming #{retired_table} to #{table}"
|
230
|
+
|
231
|
+
queries = [
|
232
|
+
"ALTER TABLE #{table} RENAME TO #{intermediate_table}",
|
233
|
+
"ALTER TABLE #{retired_table} RENAME TO #{table}"
|
234
|
+
]
|
235
|
+
run_queries(queries)
|
236
|
+
end
|
237
|
+
|
238
|
+
# arguments
|
239
|
+
|
240
|
+
def parse_args(args)
|
241
|
+
opts = Slop.parse(args) do |o|
|
242
|
+
o.boolean "--intermediate"
|
243
|
+
o.boolean "--swapped"
|
244
|
+
o.boolean "--debug"
|
245
|
+
o.float "--sleep"
|
246
|
+
o.integer "--future", default: 3
|
247
|
+
o.integer "--past", default: 3
|
248
|
+
o.integer "--batch-size", default: 10000
|
249
|
+
o.on "-v", "--version", "print the version" do
|
250
|
+
log PgSlice::VERSION
|
251
|
+
@exit = true
|
252
|
+
end
|
253
|
+
end
|
254
|
+
@arguments = opts.arguments
|
255
|
+
@options = opts.to_hash
|
256
|
+
rescue Slop::Error => e
|
257
|
+
abort e.message
|
258
|
+
end
|
259
|
+
|
260
|
+
# output
|
261
|
+
|
262
|
+
def log(message)
|
263
|
+
$stderr.puts message
|
264
|
+
end
|
265
|
+
|
266
|
+
def abort(message)
|
267
|
+
raise PgSlice::Error, message
|
268
|
+
end
|
269
|
+
|
270
|
+
# database connection
|
271
|
+
|
272
|
+
def connection
|
273
|
+
@connection ||= begin
|
274
|
+
abort "Set PGSLICE_URL" unless ENV["PGSLICE_URL"]
|
275
|
+
uri = URI.parse(ENV["PGSLICE_URL"])
|
276
|
+
uri_parser = URI::Parser.new
|
277
|
+
config = {
|
278
|
+
host: uri.host,
|
279
|
+
port: uri.port,
|
280
|
+
dbname: uri.path.sub(/\A\//, ""),
|
281
|
+
user: uri.user,
|
282
|
+
password: uri.password,
|
283
|
+
connect_timeout: 1
|
284
|
+
}.reject { |_, value| value.to_s.empty? }
|
285
|
+
config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a?(String) }
|
286
|
+
PG::Connection.new(config)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def execute(query, params = [])
|
291
|
+
connection.exec_params(query, params).to_a
|
292
|
+
end
|
293
|
+
|
294
|
+
def run_queries(queries)
|
295
|
+
connection.transaction do
|
296
|
+
execute("SET client_min_messages TO warning")
|
297
|
+
queries.map(&:squish).each do |query|
|
298
|
+
execute(query)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def existing_tables(like:)
|
304
|
+
query = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = $1 AND tablename LIKE $2"
|
305
|
+
execute(query, ["public", like]).map { |r| r["tablename"] }.sort
|
306
|
+
end
|
307
|
+
|
308
|
+
def table_exists?(table)
|
309
|
+
existing_tables(like: table).any?
|
310
|
+
end
|
311
|
+
|
312
|
+
def columns(table)
|
313
|
+
execute("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1", [table]).map{ |r| r["column_name"] }
|
314
|
+
end
|
315
|
+
|
316
|
+
# http://stackoverflow.com/a/20537829
|
317
|
+
def primary_key(table)
|
318
|
+
query = <<-SQL
|
319
|
+
SELECT
|
320
|
+
pg_attribute.attname,
|
321
|
+
format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
|
322
|
+
FROM
|
323
|
+
pg_index, pg_class, pg_attribute, pg_namespace
|
324
|
+
WHERE
|
325
|
+
pg_class.oid = $2::regclass AND
|
326
|
+
indrelid = pg_class.oid AND
|
327
|
+
nspname = $1 AND
|
328
|
+
pg_class.relnamespace = pg_namespace.oid AND
|
329
|
+
pg_attribute.attrelid = pg_class.oid AND
|
330
|
+
pg_attribute.attnum = any(pg_index.indkey) AND
|
331
|
+
indisprimary
|
332
|
+
SQL
|
333
|
+
row = execute(query, ["public", table])[0]
|
334
|
+
row && row["attname"]
|
335
|
+
end
|
336
|
+
|
337
|
+
def max_id(table, primary_key)
|
338
|
+
execute("SELECT MAX(#{primary_key}) FROM #{table}")[0]["max"].to_i
|
339
|
+
end
|
340
|
+
|
341
|
+
def has_trigger?(trigger_name, table)
|
342
|
+
execute("SELECT 1 FROM pg_trigger WHERE tgname = $1 AND tgrelid = $2::regclass", [trigger_name, "public.#{table}"]).any?
|
343
|
+
end
|
344
|
+
|
345
|
+
# helpers
|
346
|
+
|
347
|
+
def trigger_name(table)
|
348
|
+
"#{table}_insert_trigger"
|
349
|
+
end
|
350
|
+
|
351
|
+
def intermediate_name(table)
|
352
|
+
"#{table}_intermediate"
|
353
|
+
end
|
354
|
+
|
355
|
+
def retired_name(table)
|
356
|
+
"#{table}_retired"
|
357
|
+
end
|
358
|
+
|
359
|
+
def settings_from_table(original_table, table)
|
360
|
+
trigger_name = self.trigger_name(original_table)
|
361
|
+
function_def = execute("select pg_get_functiondef(oid) from pg_proc where proname = $1", [trigger_name])[0]["pg_get_functiondef"]
|
362
|
+
sql_format = SQL_FORMAT.find { |_, f| function_def.include?("'#{f}'") }
|
363
|
+
abort "Could not read settings" unless sql_format
|
364
|
+
period = sql_format[0]
|
365
|
+
field = /to_char\(NEW\.(\w+),/.match(function_def)[1]
|
366
|
+
|
367
|
+
today = Time.now
|
368
|
+
case period
|
369
|
+
when :day
|
370
|
+
name_format = "%Y%m%d"
|
371
|
+
inc = 1.day
|
372
|
+
today = today.beginning_of_day
|
373
|
+
else
|
374
|
+
name_format = "%Y%m"
|
375
|
+
inc = 1.month
|
376
|
+
today = today.beginning_of_month
|
377
|
+
end
|
378
|
+
|
379
|
+
[period, field, name_format, inc, today]
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
data/pgslice.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "pgslice/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pgslice"
|
8
|
+
spec.version = PgSlice::VERSION
|
9
|
+
spec.authors = ["Andrew Kane"]
|
10
|
+
spec.email = ["andrew@chartkick.com"]
|
11
|
+
|
12
|
+
spec.summary = "Postgres partitioning as easy as pie"
|
13
|
+
spec.homepage = "https://github.com/ankane/pgslice"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "slop", ">= 4.2.0"
|
21
|
+
spec.add_dependency "pg"
|
22
|
+
spec.add_dependency "activesupport"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pgslice
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Kane
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: slop
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pg
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- andrew@chartkick.com
|
86
|
+
executables:
|
87
|
+
- pgslice
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- Gemfile
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- exe/pgslice
|
96
|
+
- lib/pgslice.rb
|
97
|
+
- lib/pgslice/version.rb
|
98
|
+
- pgslice.gemspec
|
99
|
+
homepage: https://github.com/ankane/pgslice
|
100
|
+
licenses: []
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.6.1
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: Postgres partitioning as easy as pie
|
122
|
+
test_files: []
|
123
|
+
has_rdoc:
|