pg_party 1.3.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee226c4523ca2b7099761ad517be99938078f7977281d88920beff47396f0c51
4
- data.tar.gz: 2843ee1eb9fbe92e9998d8da0a3a20dfa989fa6c6dfcf3a261b80bac0caa47da
3
+ metadata.gz: f0edf666f9d4e7d3a7af82990be55d58b970949c5a549c1d8f1e1aeb2c066b09
4
+ data.tar.gz: 8c1164c72ffec0f00a892f9f4cf7028969a7aa39d056f4aae5e05674651a6142
5
5
  SHA512:
6
- metadata.gz: 6cb4e5e42f08dcca3ec065c5d73656b6075e5a647d60722607cb4a9ff3cfeee24a94f4048544f3442284c816e7e37fe196579b9037e1ddacaaf1136e440efd4a
7
- data.tar.gz: fdb593463dace1bdcfd844cac214cb6e6489dc7cd533226b90d11e506955ff48ad959fa80320423f3485637f89e3f6dbaeec5812ec0c1f640eb2d620e91de6dc
6
+ metadata.gz: 60baccd0471cd1628714d9d10e918d5b3be2708d07850bedf8751323cd91737321f93441fec5a576346de409580c3a7f2bb99a384c7edb0b1c70b0e96a2be84b
7
+ data.tar.gz: 12126fee3f2eae1b3a70771c79a07cab254b061c3b11ed20fbc8095f7521db05672219e93437f7a3cbb062b4ed95dd56aa2f09ae8e032b7a26cfaab7f6878def
data/README.md CHANGED
@@ -61,6 +61,20 @@ These values can be accessed and set via `PgParty.config` and `PgParty.configure
61
61
  - `schema_exclude_partitions`
62
62
  - Whether to exclude child partitions in `rake db:structure:dump`
63
63
  - Default: `true`
64
+ - `create_template_tables`
65
+ - Whether to create template tables by default. Use the `template:` option when creating partitioned tables to override this default.
66
+ - Default: `true`
67
+ - `create_with_primary_key`
68
+ - Whether to add primary key constraints to partitioned (parent) tables by default.
69
+ * This is not supported for Postgres 10 but is recommended for Postgres 11
70
+ * Primary key constraints must include all partition keys, for example: `primary_key: [:id, :created_at], partition_key: :created_at`
71
+ * Partition keys cannot use expressions
72
+ * Can be overridden via the `create_with_primary_key:` option when creating partitioned tables
73
+ - Default: `false`
74
+ - `include_subpartitions_in_partition_list`
75
+ - Whether to include nested subpartitions in the result of `YourModelClass.partiton_list` mby default.
76
+ You can always pass the `include_subpartitions:` option to override this.
77
+ - Default: `false` (for backward compatibility)
64
78
 
65
79
  Note that caching is done in-memory for each process of an application. Attaching / detaching partitions _will_ clear the cache, but only for the process that initiated the request. For multi-process web servers, it is recommended to use a TTL or disable caching entirely.
66
80
 
@@ -71,6 +85,10 @@ Note that caching is done in-memory for each process of an application. Attachin
71
85
  PgParty.configure do |c|
72
86
  c.caching_ttl = 60
73
87
  c.schema_exclude_partitions = false
88
+ c.include_subpartitions_in_partition_list = true
89
+ # Postgres 11+ users starting fresh may want to use below options
90
+ c.create_template_tables = false
91
+ c.create_with_primary_key = true
74
92
  end
75
93
  ```
76
94
 
@@ -88,25 +106,63 @@ These methods are available in migrations as well as `ActiveRecord::Base#connect
88
106
  - `create_list_partition`
89
107
  - Create partitioned table using the _list_ partitioning method
90
108
  - Required args: `table_name`, `partition_key:`
109
+ - `create_hash_partition` (Postgres 11+)
110
+ - Create partitioned table using the _hash_ partitioning method
111
+ - Required args: `table_name`, `partition_key:`
91
112
  - `create_range_partition_of`
92
113
  - Create partition in _range_ partitioned table with partition key between _range_ of values
93
114
  - Required args: `table_name`, `start_range:`, `end_range:`
115
+ - Create a subpartition by specifying a `partition_type:` of `:range`, `:list`, or `:hash` and a `partition_key:`
94
116
  - `create_list_partition_of`
95
117
  - Create partition in _list_ partitioned table with partition key in _list_ of values
96
118
  - Required args: `table_name`, `values:`
119
+ - Create a subpartition by specifying a `partition_type:` of `:range`, `:list`, or `:hash` and a `partition_key:`
120
+ - `create_hash_partition_of` (Postgres 11+)
121
+ - Create partition in _hash_ partitioned table for partition keys with hashed values having a specific remainder
122
+ - Required args: `table_name`, `modulus:`, `remainder`
123
+ - Create a subpartition by specifying a `partition_type:` of `:range`, `:list`, or `:hash` and a `partition_key:`
124
+ - Note that all partitions in a _hash_ partitioned table should have the same modulus. See [Examples](#examples) for more info.
125
+ - `create_default_partition_of` (Postgres 11+)
126
+ - Create a default partition for values not falling in the range or list constraints of any other partitions
127
+ - Required args: `table_name`
97
128
  - `attach_range_partition`
98
129
  - Attach existing table to _range_ partitioned table with partition key between _range_ of values
99
130
  - Required args: `parent_table_name`, `child_table_name`, `start_range:`, `end_range:`
100
131
  - `attach_list_partition`
101
132
  - Attach existing table to _list_ partitioned table with partition key in _list_ of values
102
133
  - Required args: `parent_table_name`, `child_table_name`, `values:`
134
+ - `attach_hash_partition` (Postgres 11+)
135
+ - Attach existing table to _hash_ partitioned table with partition key hashed values having a specific remainder
136
+ - Required args: `parent_table_name`, `child_table_name`, `modulus:`, `remainder`
137
+ - `attach_default_partition` (Postgres 11+)
138
+ - Attach existing table as the _default_ partition
139
+ - Required args: `parent_table_name`, `child_table_name`
103
140
  - `detach_partition`
104
141
  - Detach partition from both _range and list_ partitioned tables
105
142
  - Required args: `parent_table_name`, `child_table_name`
106
143
  - `create_table_like`
107
144
  - Clone _any_ existing table
108
145
  - Required args: `table_name`, `new_table_name`
109
-
146
+ - `partitions_for_table_name`
147
+ - List all attached partitions for a given table
148
+ - Required args: `table_name`, `include_subpartitions:` (true or false)
149
+ - `parent_for_table_name`
150
+ - Fetch the parent table for a partition
151
+ - Required args: `table_name`
152
+ - Pass optional `traverse: true` to return the top-level table in the hierarchy (for subpartitions)
153
+ - Returns `nil` if the table is not a partition / has no parent
154
+ - `table_partitioned?`
155
+ - Returns true if the table is partitioned (false for non-partitioned tables and partitions themselves)
156
+ - Required args: `table_name`
157
+ - `add_index_on_all_partitions`
158
+ - Recursively add an index to all partitions and subpartitions of `table_name` using Postgres's ADD INDEX CONCURRENTLY
159
+ algorithm which adds the index in a non-blocking manner.
160
+ - Required args: `table_name`, `column_name` (all `add_index` arguments are supported)
161
+ - Use the `in_threads:` option to add indexes in parallel threads when there are many partitions. A value of 2 to 4
162
+ may be reasonable for tables with many large partitions and hosts with 4+ CPUs/cores.
163
+ - Use `disable_ddl_transaction!` in your migration to disable transactions when using this command with `in_threads:`
164
+ or `algorithm: :concurrently`.
165
+
110
166
  #### Examples
111
167
 
112
168
  Create _range_ partitioned table on `created_at::date` with two partitions:
@@ -157,20 +213,130 @@ class CreateSomeListRecord < ActiveRecord::Migration[5.1]
157
213
  create_list_partition_of \
158
214
  :some_list_records,
159
215
  values: 101..200
216
+
217
+ # default partition support is available in Postgres 11 or higher
218
+ create_default_partition_of \
219
+ :some_list_records
220
+ end
221
+ end
222
+ ```
223
+
224
+ Create _hash_ partitioned table on `id` with two partitions (Postgres 11+ required):
225
+ * A hash partition can be used to spread keys evenly(ish) across partitions
226
+ * `modulus:` should always equal the total number of partitions planned for the table
227
+ * `remainder:` is an integer which should be in the range of 0 to modulus-1
228
+
229
+ ```ruby
230
+ class CreateSomeHashRecord < ActiveRecord::Migration[5.1]
231
+ def up
232
+ # symbol is used for partition keys referring to individual columns
233
+ # create_with_primary_key: true, template: false on Postgres 11 will rely on PostgreSQL's native partition schema
234
+ # management vs this gem's template tables
235
+ create_hash_partition :some_hash_records, partition_key: :id, create_with_primary_key: true, template: false do |t|
236
+ t.text :some_value
237
+ t.timestamps
238
+ end
239
+
240
+ # without name argument, child partition created as "some_list_records_<hash>"
241
+ create_hash_partition_of \
242
+ :some_hash_records,
243
+ modulus: 2,
244
+ remainder: 0
245
+
246
+ # without name argument, child partition created as "some_list_records_<hash>"
247
+ create_hash_partition_of \
248
+ :some_hash_records,
249
+ modulus: 2,
250
+ remainder: 1
160
251
  end
161
252
  end
162
253
  ```
163
254
 
255
+ Advanced example with subpartitioning: Create _list_ partitioned table on `id` subpartitioned by _range_ on `created_at`
256
+ with default partitions
257
+ * We can use Postgres 11's support for primary keys vs template tables, using the composite primary key `[:id, :created_at]`
258
+ to ensure all partition keys are present in the primary key
259
+ * Default partitions are only supported in Postgres 11+
260
+
261
+ ```ruby
262
+ class CreateSomeListSubpartitionedRecord < ActiveRecord::Migration[5.1]
263
+ def up
264
+ # when specifying a composite primary key, the primary keys must be specified as columns
265
+ create_list_partition \
266
+ :some_list_subpartitioned_records,
267
+ partition_key: [:id],
268
+ primary_key: [:id, :created_at],
269
+ template: false,
270
+ create_with_primary_key: true do |t|
271
+
272
+ t.integer :id
273
+ t.text :some_value
274
+ t.timestamps
275
+ end
276
+
277
+ create_default_partition_of \
278
+ :some_list_subpartitioned_records,
279
+ name: :some_list_subpartitioned_records_default,
280
+ partition_type: :range,
281
+ partition_key: :created_at
282
+
283
+ create_range_partition_of \
284
+ :some_list_subpartitioned_records_default,
285
+ name: :some_list_subpartitioned_records_default_2019,
286
+ start_range: '2019-01-01',
287
+ end_range: '2019-12-31T23:59:59'
288
+
289
+ create_default_partition_of \
290
+ :some_list_subpartitioned_records_default
291
+
292
+ create_list_partition_of \
293
+ :some_list_subpartitioned_records,
294
+ name: :some_list_subpartitioned_records_1,
295
+ values: 1..100,
296
+ partition_type: :range,
297
+ partition_key: :created_at
298
+
299
+ create_range_partition_of \
300
+ :some_list_subpartitioned_records_1,
301
+ name: :some_list_subpartitioned_records_1_2019,
302
+ start_range: '2019-01-01',
303
+ end_range: '2019-12-31T23:59:59'
304
+
305
+ create_default_partition_of
306
+ :some_list_subpartitioned_records_1
307
+
308
+ create_list_partition_of \
309
+ :some_list_subpartitioned_records,
310
+ name: :some_list_subpartitioned_records_2,
311
+ values: 101..200,
312
+ partition_type: :range,
313
+ partition_key: :created_at
314
+
315
+ create_range_partition_of \
316
+ :some_list_subpartitioned_records_2,
317
+ name: :some_list_subpartitioned_records_2_2019,
318
+ start_range: '2019-01-01',
319
+ end_range: '2019-12-31T23:59:59'
320
+
321
+ create_default_partition_of \
322
+ :some_list_subpartitioned_records_2
323
+ end
324
+ end
325
+ ```
326
+
327
+ #### Template Tables
164
328
  Unfortunately, PostgreSQL 10 doesn't support indexes on partitioned tables.
165
329
  However, individual _partitions_ can have indexes.
166
330
  To avoid explicit index creation for _every_ new partition, we've introduced the idea of template tables.
167
331
  For every call to `create_list_partition` and `create_range_partition`, a clone `<table_name>_template` is created.
168
332
  Indexes, constraints, etc. created on the template table will propagate to new partitions in calls to `create_list_partition_of` and `create_range_partition_of`:
333
+ * Subpartitions will correctly clone from template tables if a template table exists for the top-level ancestor
334
+ * When using Postgres 11 or higher, you may wish to disable template tables and use the native features instead, see [Configuration](#configuration)
169
335
 
170
336
  ```ruby
171
337
  class CreateSomeListRecord < ActiveRecord::Migration[5.1]
172
338
  def up
173
- # template table creation is enabled by default - use "template: false" to opt-out
339
+ # template table creation is enabled by default - use "template: false" or the config option to opt-out
174
340
  create_list_partition :some_list_records, partition_key: :id do |t|
175
341
  t.integer :some_foreign_id
176
342
  t.text :some_value
@@ -193,6 +359,8 @@ class CreateSomeListRecord < ActiveRecord::Migration[5.1]
193
359
  end
194
360
  ```
195
361
 
362
+ #### Attaching Existing Tables as Partitions
363
+
196
364
  Attach an existing table to a _range_ partitioned table:
197
365
 
198
366
  ```ruby
@@ -220,6 +388,20 @@ class AttachListPartition < ActiveRecord::Migration[5.1]
220
388
  end
221
389
  ```
222
390
 
391
+ Attach an existing table to a _hash_ partitioned table:
392
+
393
+ ```ruby
394
+ class AttachHashPartition < ActiveRecord::Migration[5.1]
395
+ def up
396
+ attach_hash_partition \
397
+ :some_hash_records,
398
+ :some_existing_table,
399
+ modulus: 2,
400
+ remainder: 1
401
+ end
402
+ end
403
+ ```
404
+
223
405
  Detach a partition from any partitioned table:
224
406
 
225
407
  ```ruby
@@ -230,6 +412,31 @@ class DetachPartition < ActiveRecord::Migration[5.1]
230
412
  end
231
413
  ```
232
414
 
415
+ #### Safely cascading `add_index` commands
416
+ Postgres 11+ will automatically cascade CREATE INDEX operations to partitions and subpartitions, however
417
+ CREATE INDEX CONCURRENTLY is not supported, meaning table locks will be taken on each table while the new index is built.
418
+ Postgres 10 provides no way to cascade index creation natively.
419
+ * The `add_index_on_all_partitions` method solves for these limitations by recursively creating the specified
420
+ index on all partitions and subpartitions. Index names on individual partitions will include a hash suffix to avoid conflicts.
421
+ * On Postgres 11+, the created indices are correctly attached to an index on the parent table
422
+ * On Postgres 10, if you are using [Template Tables](#template-tables-for-postgres-10), you will want to add the index to the template table separately.
423
+ * This command can also be used on subpartitions to cascade targeted indices starting at one level of the table hierarchy
424
+
425
+ ```ruby
426
+ class AddSomeValueIndexToSomeListRecord < ActiveRecord::Migration[5.1]
427
+ # add_index_on_all_partitions with in_threads option may not be used within a transaction
428
+ # (also, algorithm: :concurrently cannot be used within a transaction)
429
+ disable_ddl_transaction!
430
+
431
+ def up
432
+ add_index :some_records_template, :some_value # Only if using Postgres 10 with template tables
433
+
434
+ # Pass the `in_threads:` option to create indices in parallel across multiple Postgres connections
435
+ add_index_on_all_partitions :some_records, :some_value, algorithm: :concurrently, in_threads: 4
436
+ end
437
+ end
438
+ ```
439
+
233
440
  For more examples, take a look at the Combustion schema definition and integration specs:
234
441
 
235
442
  - https://github.com/rkrage/pg_party/blob/master/spec/dummy/db/schema.rb
@@ -250,12 +457,15 @@ Class methods available to _all_ ActiveRecord models:
250
457
  - `list_partition_by`
251
458
  - Configure a model backed by a _list_ partitioned table
252
459
  - Required arg: `key` (partition key column) or block returning partition key expression
460
+ - `hash_partition_by`
461
+ - Configure a model backed by a _hash_ partitioned table
462
+ - Required arg: `key` (partition key column) or block returning partition key expression
253
463
 
254
464
  Class methods available to both _range and list_ partitioned models:
255
465
 
256
466
  - `partitions`
257
467
  - Retrieve a list of currently attached partitions
258
- - No arguments
468
+ - Optional `include_subpartitions:` argument to include all subpartitions in the returned list
259
469
  - `in_partition`
260
470
  - Retrieve an ActiveRecord model scoped to an individual partition
261
471
  - Required arg: `child_table_name`
@@ -280,6 +490,16 @@ Class methods available to _list_ partitioned models:
280
490
  - `partition_key_in`
281
491
  - Query for records where partition key in _list_ of values
282
492
  - Required arg: list of `values`
493
+
494
+
495
+ Class methods available to _hash_ partitioned models:
496
+
497
+ - `create_partition`
498
+ - Dynamically create new partition with hashed partition key divided by _modulus_ equals _remainder_
499
+ - Required arg: `modulus:`, `remainder:`
500
+ - `partition_key_in`
501
+ - Query for records where partition key in _list_ of values (method operates the same as for _list_ partitions above)
502
+ - Required arg: list of `values`
283
503
 
284
504
  #### Examples
285
505
 
@@ -301,6 +521,15 @@ class SomeListRecord < ApplicationRecord
301
521
  end
302
522
  ```
303
523
 
524
+ Configure model backed by a _hash_ partitioned table to get access to the methods described above:
525
+
526
+ ```ruby
527
+ class SomeHashRecord < ApplicationRecord
528
+ # symbol is used for partition keys referring to individual columns
529
+ hash_partition_by :id
530
+ end
531
+ ```
532
+
304
533
  Dynamically create new partition from _range_ partitioned model:
305
534
 
306
535
  ```ruby
@@ -315,19 +544,26 @@ Dynamically create new partition from _list_ partitioned model:
315
544
  SomeListRecord.create_partition(values: 200..300)
316
545
  ```
317
546
 
547
+ Dynamically create new partition from _hash_ partitioned model:
548
+
549
+ ```ruby
550
+ # additional options include: "name:" and "primary_key:"
551
+ SomeHashRecord.create_partition(modulus: 2, remainder: 1)
552
+ ```
553
+
318
554
  For _range_ partitioned model, query for records where partition key in _range_ of values:
319
555
 
320
556
  ```ruby
321
557
  SomeRangeRecord.partition_key_in("2019-06-08", "2019-06-10")
322
558
  ```
323
559
 
324
- For _list_ partitioned model, query for records where partition key in _list_ of values:
560
+ For _list_ and _hash_ partitioned models, query for records where partition key in _list_ of values:
325
561
 
326
562
  ```ruby
327
563
  SomeListRecord.partition_key_in(1, 2, 3, 4)
328
564
  ```
329
565
 
330
- For both _range and list_ partitioned models, query for records matching partition key:
566
+ For all partitioned models, query for records matching partition key:
331
567
 
332
568
  ```ruby
333
569
  SomeRangeRecord.partition_key_eq(Date.current)
@@ -335,15 +571,15 @@ SomeRangeRecord.partition_key_eq(Date.current)
335
571
  SomeListRecord.partition_key_eq(100)
336
572
  ```
337
573
 
338
- For both _range and list_ partitioned models, retrieve currently attached partitions:
574
+ For all partitioned models, retrieve currently attached partitions:
339
575
 
340
576
  ```ruby
341
577
  SomeRangeRecord.partitions
342
578
 
343
- SomeListRecord.partitions
579
+ SomeListRecord.partitions(include_subpartitions: true) # Include nested subpartitions
344
580
  ```
345
581
 
346
- For both _range and list_ partitioned models, retrieve ActiveRecord model scoped to individual partition:
582
+ For both all partitioned models, retrieve ActiveRecord model scoped to individual partition:
347
583
 
348
584
  ```ruby
349
585
  SomeRangeRecord.in_partition(:some_range_records_partition_name)
@@ -11,6 +11,10 @@ module PgParty
11
11
  raise "#create_list_partition is not implemented"
12
12
  end
13
13
 
14
+ def create_hash_partition(*)
15
+ raise "#create_hash_partition is not implemented"
16
+ end
17
+
14
18
  def create_range_partition_of(*)
15
19
  raise "#create_range_partition_of is not implemented"
16
20
  end
@@ -19,6 +23,14 @@ module PgParty
19
23
  raise "#create_list_partition_of is not implemented"
20
24
  end
21
25
 
26
+ def create_hash_partition_of(*)
27
+ raise "#create_hash_partition_of is not implemented"
28
+ end
29
+
30
+ def create_default_partition_of(*)
31
+ raise "#create_default_partition_of is not implemented"
32
+ end
33
+
22
34
  def create_table_like(*)
23
35
  raise "#create_table_like is not implemented"
24
36
  end
@@ -31,9 +43,33 @@ module PgParty
31
43
  raise "#attach_list_partition is not implemented"
32
44
  end
33
45
 
46
+ def attach_hash_partition(*)
47
+ raise "#attach_hash_partition is not implemented"
48
+ end
49
+
50
+ def attach_default_partition(*)
51
+ raise "#attach_default_partition is not implemented"
52
+ end
53
+
34
54
  def detach_partition(*)
35
55
  raise "#detach_partition is not implemented"
36
56
  end
57
+
58
+ def parent_for_table_name(*)
59
+ raise "#parent_for_table_name is not implemented"
60
+ end
61
+
62
+ def partitions_for_table_name(*)
63
+ raise "#partitions_for_table_name is not implemented"
64
+ end
65
+
66
+ def add_index_on_all_partitions(*)
67
+ raise "#add_index_on_all_partitions is not implemented"
68
+ end
69
+
70
+ def table_partitioned?(*)
71
+ raise "#table_partitioned? is not implemented"
72
+ end
37
73
  end
38
74
  end
39
75
  end
@@ -14,6 +14,10 @@ module PgParty
14
14
  PgParty::AdapterDecorator.new(self).create_list_partition(*args, &blk)
15
15
  end
16
16
 
17
+ ruby2_keywords def create_hash_partition(*args, &blk)
18
+ PgParty::AdapterDecorator.new(self).create_hash_partition(*args, &blk)
19
+ end
20
+
17
21
  ruby2_keywords def create_range_partition_of(*args)
18
22
  PgParty::AdapterDecorator.new(self).create_range_partition_of(*args)
19
23
  end
@@ -22,6 +26,14 @@ module PgParty
22
26
  PgParty::AdapterDecorator.new(self).create_list_partition_of(*args)
23
27
  end
24
28
 
29
+ ruby2_keywords def create_hash_partition_of(*args)
30
+ PgParty::AdapterDecorator.new(self).create_hash_partition_of(*args)
31
+ end
32
+
33
+ ruby2_keywords def create_default_partition_of(*args)
34
+ PgParty::AdapterDecorator.new(self).create_default_partition_of(*args)
35
+ end
36
+
25
37
  ruby2_keywords def create_table_like(*args)
26
38
  PgParty::AdapterDecorator.new(self).create_table_like(*args)
27
39
  end
@@ -34,9 +46,33 @@ module PgParty
34
46
  PgParty::AdapterDecorator.new(self).attach_list_partition(*args)
35
47
  end
36
48
 
49
+ ruby2_keywords def attach_hash_partition(*args)
50
+ PgParty::AdapterDecorator.new(self).attach_hash_partition(*args)
51
+ end
52
+
53
+ ruby2_keywords def attach_default_partition(*args)
54
+ PgParty::AdapterDecorator.new(self).attach_default_partition(*args)
55
+ end
56
+
37
57
  ruby2_keywords def detach_partition(*args)
38
58
  PgParty::AdapterDecorator.new(self).detach_partition(*args)
39
59
  end
60
+
61
+ ruby2_keywords def partitions_for_table_name(*args)
62
+ PgParty::AdapterDecorator.new(self).partitions_for_table_name(*args)
63
+ end
64
+
65
+ ruby2_keywords def parent_for_table_name(*args)
66
+ PgParty::AdapterDecorator.new(self).parent_for_table_name(*args)
67
+ end
68
+
69
+ ruby2_keywords def add_index_on_all_partitions(*args)
70
+ PgParty::AdapterDecorator.new(self).add_index_on_all_partitions(*args)
71
+ end
72
+
73
+ ruby2_keywords def table_partitioned?(*args)
74
+ PgParty::AdapterDecorator.new(self).table_partitioned?(*args)
75
+ end
40
76
  end
41
77
  end
42
78
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest"
4
+ require 'parallel'
4
5
 
5
6
  module PgParty
6
7
  class AdapterDecorator < SimpleDelegator
8
+ SUPPORTED_PARTITION_TYPES = %i[range list hash].freeze
9
+
7
10
  def initialize(adapter)
8
11
  super(adapter)
9
12
 
@@ -18,6 +21,10 @@ module PgParty
18
21
  create_partition(table_name, :list, partition_key, **options, &blk)
19
22
  end
20
23
 
24
+ def create_hash_partition(table_name, partition_key:, **options, &blk)
25
+ create_partition(table_name, :hash, partition_key, **options, &blk)
26
+ end
27
+
21
28
  def create_range_partition_of(table_name, start_range:, end_range:, **options)
22
29
  create_partition_of(table_name, range_constraint_clause(start_range, end_range), **options)
23
30
  end
@@ -26,17 +33,42 @@ module PgParty
26
33
  create_partition_of(table_name, list_constraint_clause(values), **options)
27
34
  end
28
35
 
36
+ def create_hash_partition_of(table_name, modulus:, remainder:, **options)
37
+ create_partition_of(table_name, hash_constraint_clause(modulus, remainder), **options)
38
+ end
39
+
40
+ def create_default_partition_of(table_name, **options)
41
+ create_partition_of(table_name, nil, default_partition: true, **options)
42
+ end
43
+
29
44
  def create_table_like(table_name, new_table_name, **options)
30
- primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) }
45
+ primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) }
46
+ partition_key = options.fetch(:partition_key, nil)
47
+ partition_type = options.fetch(:partition_type, nil)
48
+ create_with_pks = options.fetch(
49
+ :create_with_primary_key,
50
+ PgParty.config.create_with_primary_key
51
+ )
52
+
53
+ validate_primary_key(primary_key) unless create_with_pks
54
+ if partition_type
55
+ validate_supported_partition_type!(partition_type)
56
+ raise ArgumentError, '`partition_key` is required when specifying a partition_type' unless partition_key
57
+ end
31
58
 
32
- validate_primary_key(primary_key)
59
+ like_option = if !partition_type || create_with_pks
60
+ 'INCLUDING ALL'
61
+ else
62
+ 'INCLUDING ALL EXCLUDING INDEXES'
63
+ end
33
64
 
34
65
  execute(<<-SQL)
35
66
  CREATE TABLE #{quote_table_name(new_table_name)} (
36
- LIKE #{quote_table_name(table_name)} INCLUDING ALL
37
- )
67
+ LIKE #{quote_table_name(table_name)} #{like_option}
68
+ ) #{partition_type ? partition_by_clause(partition_type, partition_key) : nil}
38
69
  SQL
39
70
 
71
+ return if partition_type
40
72
  return if !primary_key
41
73
  return if has_primary_key?(new_table_name)
42
74
 
@@ -54,6 +86,20 @@ module PgParty
54
86
  attach_partition(parent_table_name, child_table_name, list_constraint_clause(values))
55
87
  end
56
88
 
89
+ def attach_hash_partition(parent_table_name, child_table_name, modulus:, remainder:)
90
+ attach_partition(parent_table_name, child_table_name, hash_constraint_clause(modulus, remainder))
91
+ end
92
+
93
+ def attach_default_partition(parent_table_name, child_table_name)
94
+ execute(<<-SQL)
95
+ ALTER TABLE #{quote_table_name(parent_table_name)}
96
+ ATTACH PARTITION #{quote_table_name(child_table_name)}
97
+ DEFAULT
98
+ SQL
99
+
100
+ PgParty.cache.clear!
101
+ end
102
+
57
103
  def detach_partition(parent_table_name, child_table_name)
58
104
  execute(<<-SQL)
59
105
  ALTER TABLE #{quote_table_name(parent_table_name)}
@@ -63,23 +109,101 @@ module PgParty
63
109
  PgParty.cache.clear!
64
110
  end
65
111
 
66
- private
112
+ def partitions_for_table_name(table_name, include_subpartitions:, _accumulator: [])
113
+ select_values(%[
114
+ SELECT pg_inherits.inhrelid::regclass::text
115
+ FROM pg_tables
116
+ INNER JOIN pg_inherits
117
+ ON pg_tables.tablename::regclass = pg_inherits.inhparent::regclass
118
+ WHERE pg_tables.schemaname = current_schema() AND
119
+ pg_tables.tablename = #{quote(table_name)}
120
+ ]).each_with_object(_accumulator) do |partition, acc|
121
+ acc << partition
122
+ next unless include_subpartitions
123
+
124
+ partitions_for_table_name(partition, include_subpartitions: true, _accumulator: acc)
125
+ end
126
+ end
67
127
 
68
- def create_partition(table_name, type, partition_key, **options)
69
- modified_options = options.except(:id, :primary_key, :template)
70
- template = options.fetch(:template, true)
71
- id = options.fetch(:id, :bigserial)
72
- primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) }
128
+ def parent_for_table_name(table_name, traverse: false)
129
+ parent = select_values(%[
130
+ SELECT pg_inherits.inhparent::regclass::text
131
+ FROM pg_tables
132
+ INNER JOIN pg_inherits
133
+ ON pg_tables.tablename::regclass = pg_inherits.inhrelid::regclass
134
+ WHERE pg_tables.schemaname = current_schema() AND
135
+ pg_tables.tablename = #{quote(table_name)}
136
+ ]).first
137
+ return parent if parent.nil? || !traverse
138
+
139
+ while (parents_parent = parent_for_table_name(parent)) do
140
+ parent = parents_parent
141
+ end
73
142
 
74
- validate_primary_key(primary_key)
143
+ parent
144
+ end
145
+
146
+ def add_index_on_all_partitions(table_name, column_name, in_threads: nil, **options)
147
+ if in_threads && open_transactions > 0
148
+ raise ArgumentError, '`in_threads:` cannot be used within a transaction. If running in a migration, use '\
149
+ '`disable_ddl_transaction!` and break out this operation into its own migration.'
150
+ end
75
151
 
76
- modified_options[:id] = false
77
- modified_options[:options] = "PARTITION BY #{type.to_s.upcase} (#{quote_partition_key(partition_key)})"
152
+ index_name, index_type, index_columns, index_options, algorithm, using = add_index_options(
153
+ table_name, column_name, options
154
+ )
155
+
156
+ # Postgres limits index name to 63 bytes (characters). We will use 8 characters for a `_random_suffix`
157
+ # on partitions to ensure no conflicts, leaving 55 chars for the specified index name
158
+ raise ArgumentError 'index name is too long - must be 55 characters or fewer' if index_name.length > 55
159
+
160
+ recursive_add_index(
161
+ table_name: table_name,
162
+ index_name: index_name,
163
+ index_type: index_type,
164
+ index_columns: index_columns,
165
+ index_options: index_options,
166
+ algorithm: algorithm,
167
+ using: using,
168
+ in_threads: in_threads
169
+ )
170
+ end
171
+
172
+ def table_partitioned?(table_name)
173
+ select_values(%[
174
+ SELECT relkind FROM pg_catalog.pg_class AS c
175
+ JOIN pg_catalog.pg_namespace AS ns ON c.relnamespace = ns.oid
176
+ WHERE relname = #{quote(table_name)} AND nspname = current_schema()
177
+ ]).first == 'p'
178
+ end
179
+
180
+ private
181
+
182
+ def create_partition(table_name, type, partition_key, **options)
183
+ modified_options = options.except(:id, :primary_key, :template, :create_with_primary_key)
184
+ template = options.fetch(:template, PgParty.config.create_template_tables)
185
+ id = options.fetch(:id, :bigserial)
186
+ primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) }
187
+ create_with_pks = options.fetch(
188
+ :create_with_primary_key,
189
+ PgParty.config.create_with_primary_key
190
+ )
191
+
192
+ validate_supported_partition_type!(type)
193
+
194
+ if create_with_pks
195
+ modified_options[:primary_key] = primary_key
196
+ modified_options[:id] = id
197
+ else
198
+ validate_primary_key(primary_key)
199
+ modified_options[:id] = false
200
+ end
201
+ modified_options[:options] = partition_by_clause(type, partition_key)
78
202
 
79
203
  create_table(table_name, modified_options) do |td|
80
- if id == :uuid
204
+ if !modified_options[:id] && id == :uuid
81
205
  td.column(primary_key, id, null: false, default: uuid_function)
82
- elsif id
206
+ elsif !modified_options[:id] && id
83
207
  td.column(primary_key, id, null: false)
84
208
  end
85
209
 
@@ -87,11 +211,16 @@ module PgParty
87
211
  end
88
212
 
89
213
  # Rails 4 has a bug where uuid columns are always nullable
90
- change_column_null(table_name, primary_key, false) if id == :uuid
214
+ change_column_null(table_name, primary_key, false) if !modified_options[:id] && id == :uuid
91
215
 
92
216
  return unless template
93
217
 
94
- create_table_like(table_name, template_table_name(table_name), primary_key: id && primary_key)
218
+ create_table_like(
219
+ table_name,
220
+ template_table_name(table_name),
221
+ primary_key: id && primary_key,
222
+ create_with_primary_key: create_with_pks
223
+ )
95
224
  end
96
225
 
97
226
  def create_partition_of(table_name, constraint_clause, **options)
@@ -99,13 +228,21 @@ module PgParty
99
228
  primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) }
100
229
  template_table_name = template_table_name(table_name)
101
230
 
231
+ validate_default_partition_support! if options[:default_partition]
232
+
102
233
  if schema_cache.data_source_exists?(template_table_name)
103
- create_table_like(template_table_name, child_table_name, primary_key: false)
234
+ create_table_like(template_table_name, child_table_name, primary_key: false,
235
+ partition_type: options[:partition_type], partition_key: options[:partition_key])
104
236
  else
105
- create_table_like(table_name, child_table_name, primary_key: primary_key)
237
+ create_table_like(table_name, child_table_name, primary_key: primary_key,
238
+ partition_type: options[:partition_type], partition_key: options[:partition_key])
106
239
  end
107
240
 
108
- attach_partition(table_name, child_table_name, constraint_clause)
241
+ if options[:default_partition]
242
+ attach_default_partition(table_name, child_table_name)
243
+ else
244
+ attach_partition(table_name, child_table_name, constraint_clause)
245
+ end
109
246
 
110
247
  child_table_name
111
248
  end
@@ -120,6 +257,82 @@ module PgParty
120
257
  PgParty.cache.clear!
121
258
  end
122
259
 
260
+ def recursive_add_index(table_name:, index_name:, index_type:, index_columns:, index_options:, using:, algorithm:,
261
+ in_threads: nil, _parent_index_name: nil, _created_index_names: [])
262
+ partitions = partitions_for_table_name(table_name, include_subpartitions: false)
263
+ updated_name = _created_index_names.empty? ? index_name : generate_index_name(index_name, table_name)
264
+
265
+ # If this is a partitioned table, add index ONLY on this table.
266
+ if table_partitioned?(table_name)
267
+ add_index_only(table_name, type: index_type, name: updated_name, using: using, columns: index_columns,
268
+ options: index_options)
269
+ _created_index_names << updated_name
270
+
271
+ parallel_map(partitions, in_threads: in_threads) do |partition_name|
272
+ recursive_add_index(
273
+ table_name: partition_name,
274
+ index_name: index_name,
275
+ index_type: index_type,
276
+ index_columns: index_columns,
277
+ index_options: index_options,
278
+ using: using,
279
+ algorithm: algorithm,
280
+ _parent_index_name: updated_name,
281
+ _created_index_names: _created_index_names
282
+ )
283
+ end
284
+ else
285
+ _created_index_names << updated_name # Track as created before execution of concurrent index command
286
+ add_index_from_options(table_name, name: updated_name, type: index_type, algorithm: algorithm, using: using,
287
+ columns: index_columns, options: index_options)
288
+ end
289
+
290
+ attach_child_index(updated_name, _parent_index_name) if _parent_index_name
291
+
292
+ return true if index_valid?(updated_name)
293
+
294
+ raise 'index creation failed - an index was marked invalid'
295
+ rescue => e
296
+ # Clean up any indexes created so this command can be retried later
297
+ drop_indices_if_exist(_created_index_names)
298
+ raise e
299
+ end
300
+
301
+ def attach_child_index(child, parent)
302
+ return unless postgres_major_version >= 11
303
+
304
+ execute "ALTER INDEX #{quote_column_name(parent)} ATTACH PARTITION #{quote_column_name(child)}"
305
+ end
306
+
307
+ def add_index_only(table_name, type:, name:, using:, columns:, options:)
308
+ return unless postgres_major_version >= 11
309
+
310
+ execute "CREATE #{type} INDEX #{quote_column_name(name)} ON ONLY "\
311
+ "#{quote_table_name(table_name)} #{using} (#{columns})#{options}"
312
+ end
313
+
314
+ def add_index_from_options(table_name, name:, type:, algorithm:, using:, columns:, options:)
315
+ execute "CREATE #{type} INDEX #{algorithm} #{quote_column_name(name)} ON "\
316
+ "#{quote_table_name(table_name)} #{using} (#{columns})#{options}"
317
+ end
318
+
319
+ def drop_indices_if_exist(index_names)
320
+ index_names.uniq.each { |name| execute "DROP INDEX IF EXISTS #{quote_column_name(name)}" }
321
+ end
322
+
323
+ def parallel_map(arr, in_threads:)
324
+ return [] if arr.empty?
325
+ return arr.map { |item| yield(item) } unless in_threads && in_threads > 1
326
+
327
+ if ActiveRecord::Base.connection_pool.size <= in_threads
328
+ raise ArgumentError, 'in_threads: must be lower than your database connection pool size'
329
+ end
330
+
331
+ Parallel.map(arr, in_threads: in_threads) do |item|
332
+ ActiveRecord::Base.connection_pool.with_connection { yield(item) }
333
+ end
334
+ end
335
+
123
336
  # Rails 5.2 now returns boolean literals
124
337
  # This causes partition creation to fail when the constraint clause includes a boolean
125
338
  # Might be a PostgreSQL bug, but for now let's revert to the old quoting behavior
@@ -157,27 +370,69 @@ module PgParty
157
370
  end
158
371
 
159
372
  def template_table_name(table_name)
160
- "#{table_name}_template"
373
+ "#{parent_for_table_name(table_name, traverse: true) || table_name}_template"
161
374
  end
162
375
 
163
376
  def range_constraint_clause(start_range, end_range)
164
377
  "FROM (#{quote_collection(start_range)}) TO (#{quote_collection(end_range)})"
165
378
  end
166
379
 
380
+ def hash_constraint_clause(modulus, remainder)
381
+ "WITH (MODULUS #{modulus.to_i}, REMAINDER #{remainder.to_i})"
382
+ end
383
+
167
384
  def list_constraint_clause(values)
168
385
  "IN (#{quote_collection(values.try(:to_a) || values)})"
169
386
  end
170
387
 
388
+ def partition_by_clause(type, partition_key)
389
+ "PARTITION BY #{type.to_s.upcase} (#{quote_partition_key(partition_key)})"
390
+ end
391
+
171
392
  def uuid_function
172
393
  try(:supports_pgcrypto_uuid?) ? "gen_random_uuid()" : "uuid_generate_v4()"
173
394
  end
174
395
 
175
396
  def hashed_table_name(table_name, key)
176
- "#{table_name}_#{Digest::MD5.hexdigest(key)[0..6]}"
397
+ return "#{table_name}_#{Digest::MD5.hexdigest(key)[0..6]}" if key
398
+
399
+ # use _default suffix for default partitions (without a constraint clause)
400
+ "#{table_name}_default"
401
+ end
402
+
403
+ def index_valid?(index_name)
404
+ select_values(
405
+ "SELECT relname FROM pg_class, pg_index WHERE pg_index.indisvalid = false AND "\
406
+ "pg_index.indexrelid = pg_class.oid AND relname = #{quote(index_name)}"
407
+ ).empty?
408
+ end
409
+
410
+ def generate_index_name(index_name, table_name)
411
+ "#{index_name}_#{Digest::MD5.hexdigest(table_name)[0..6]}"
412
+ end
413
+
414
+ def validate_supported_partition_type!(partition_type)
415
+ if (sym = partition_type.to_s.downcase.to_sym) && sym.in?(SUPPORTED_PARTITION_TYPES)
416
+ return if sym != :hash || postgres_major_version >= 11
417
+
418
+ raise NotImplementedError, 'Hash partitions are only available in Postgres 11 or higher'
419
+ end
420
+
421
+ raise ArgumentError, "Supported partition types are #{SUPPORTED_PARTITION_TYPES.join(', ')}"
422
+ end
423
+
424
+ def validate_default_partition_support!
425
+ return if postgres_major_version >= 11
426
+
427
+ raise NotImplementedError, 'Default partitions are only available in Postgres 11 or higher'
177
428
  end
178
429
 
179
430
  def supports_partitions?
180
- __getobj__.send(:postgresql_version) >= 100000
431
+ postgres_major_version >= 10
432
+ end
433
+
434
+ def postgres_major_version
435
+ __getobj__.send(:postgresql_version)/10000
181
436
  end
182
437
  end
183
438
  end
@@ -9,7 +9,7 @@ module PgParty
9
9
  def initialize
10
10
  # automatically initialize a new hash when
11
11
  # accessing an object id that doesn't exist
12
- @store = Hash.new { |h, k| h[k] = { models: {}, partitions: nil } }
12
+ @store = Hash.new { |h, k| h[k] = { models: {}, partitions: nil, partitions_with_subpartitions: nil } }
13
13
  end
14
14
 
15
15
  def clear!
@@ -24,10 +24,11 @@ module PgParty
24
24
  LOCK.synchronize { fetch_value(@store[key][:models], child_table.to_sym, block) }
25
25
  end
26
26
 
27
- def fetch_partitions(key, &block)
27
+ def fetch_partitions(key, include_subpartitions, &block)
28
28
  return block.call unless caching_enabled?
29
+ sub_key = include_subpartitions ? :partitions_with_subpartitions : :partitions
29
30
 
30
- LOCK.synchronize { fetch_value(@store[key], :partitions, block) }
31
+ LOCK.synchronize { fetch_value(@store[key], sub_key, block) }
31
32
  end
32
33
 
33
34
  private
@@ -5,12 +5,18 @@ module PgParty
5
5
  attr_accessor \
6
6
  :caching,
7
7
  :caching_ttl,
8
- :schema_exclude_partitions
8
+ :schema_exclude_partitions,
9
+ :create_template_tables,
10
+ :create_with_primary_key,
11
+ :include_subpartitions_in_partition_list
9
12
 
10
13
  def initialize
11
14
  @caching = true
12
15
  @caching_ttl = -1
13
16
  @schema_exclude_partitions = true
17
+ @create_template_tables = true
18
+ @create_with_primary_key = false
19
+ @include_subpartitions_in_partition_list = false
14
20
  end
15
21
  end
16
22
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg_party/model_decorator"
4
+ require "ruby2_keywords"
5
+
6
+ module PgParty
7
+ module Model
8
+ module HashMethods
9
+ ruby2_keywords def create_partition(*args)
10
+ PgParty::ModelDecorator.new(self).create_hash_partition(*args)
11
+ end
12
+
13
+ ruby2_keywords def partition_key_in(*args)
14
+ PgParty::ModelDecorator.new(self).hash_partition_key_in(*args)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -10,6 +10,10 @@ module PgParty
10
10
  PgParty::ModelDecorator.new(self).create_list_partition(*args)
11
11
  end
12
12
 
13
+ ruby2_keywords def create_default_partition(*args)
14
+ PgParty::ModelDecorator.new(self).create_default_partition(*args)
15
+ end
16
+
13
17
  ruby2_keywords def partition_key_in(*args)
14
18
  PgParty::ModelDecorator.new(self).list_partition_key_in(*args)
15
19
  end
@@ -13,6 +13,10 @@ module PgParty
13
13
  PgParty::ModelInjector.new(self, *key, &blk).inject_list_methods
14
14
  end
15
15
 
16
+ def hash_partition_by(*key, &blk)
17
+ PgParty::ModelInjector.new(self, *key, &blk).inject_hash_methods
18
+ end
19
+
16
20
  def partitioned?
17
21
  try(:partition_key).present?
18
22
  end
@@ -10,6 +10,10 @@ module PgParty
10
10
  PgParty::ModelDecorator.new(self).create_range_partition(*args)
11
11
  end
12
12
 
13
+ ruby2_keywords def create_default_partition(*args)
14
+ PgParty::ModelDecorator.new(self).create_default_partition(*args)
15
+ end
16
+
13
17
  ruby2_keywords def partition_key_in(*args)
14
18
  PgParty::ModelDecorator.new(self).range_partition_key_in(*args)
15
19
  end
@@ -6,13 +6,15 @@ module PgParty
6
6
  module Model
7
7
  module SharedMethods
8
8
  def reset_primary_key
9
- if self != base_class
10
- base_class.primary_key
11
- elsif partition_name = partitions.first
12
- in_partition(partition_name).get_primary_key(base_class.name)
13
- else
14
- get_primary_key(base_class.name)
15
- end
9
+ return base_class.primary_key if self != base_class
10
+
11
+ partitions = partitions(include_subpartitions: true)
12
+ return get_primary_key(base_class.name) if partitions.empty?
13
+
14
+ first_partition = partitions.detect { |p| !connection.table_partitioned?(p) }
15
+ raise 'No leaf partitions exist for this model. Create a partition to contain your data' unless first_partition
16
+
17
+ in_partition(first_partition).get_primary_key(base_class.name)
16
18
  end
17
19
 
18
20
  def table_exists?
@@ -21,8 +23,8 @@ module PgParty
21
23
  connection.schema_cache.data_source_exists?(target_table)
22
24
  end
23
25
 
24
- def partitions
25
- PgParty::ModelDecorator.new(self).partitions
26
+ def partitions(*args)
27
+ PgParty::ModelDecorator.new(self).partitions(*args)
26
28
  end
27
29
 
28
30
  def in_partition(*args)
@@ -62,15 +62,11 @@ module PgParty
62
62
  end
63
63
  end
64
64
 
65
- def partitions
66
- PgParty.cache.fetch_partitions(cache_key) do
67
- connection.select_values(<<-SQL)
68
- SELECT pg_inherits.inhrelid::regclass::text
69
- FROM pg_tables
70
- INNER JOIN pg_inherits
71
- ON pg_tables.tablename::regclass = pg_inherits.inhparent::regclass
72
- WHERE pg_tables.tablename = #{connection.quote(table_name)}
73
- SQL
65
+ alias_method :hash_partition_key_in, :list_partition_key_in
66
+
67
+ def partitions(include_subpartitions: PgParty.config.include_subpartitions_in_partition_list)
68
+ PgParty.cache.fetch_partitions(cache_key, include_subpartitions) do
69
+ connection.partitions_for_table_name(table_name, include_subpartitions: include_subpartitions)
74
70
  end
75
71
  rescue
76
72
  []
@@ -95,6 +91,23 @@ module PgParty
95
91
  create_partition(:create_list_partition_of, table_name, **modified_options)
96
92
  end
97
93
 
94
+ def create_hash_partition(modulus:, remainder:, **options)
95
+ modified_options = options.merge(
96
+ modulus: modulus,
97
+ remainder: remainder,
98
+ primary_key: primary_key,
99
+ )
100
+
101
+ create_partition(:create_hash_partition_of, table_name, **modified_options)
102
+ end
103
+
104
+ def create_default_partition(**options)
105
+ modified_options = options.merge(
106
+ primary_key: primary_key,
107
+ )
108
+ create_partition(:create_default_partition_of, table_name, **modified_options)
109
+ end
110
+
98
111
  private
99
112
 
100
113
  def create_partition(migration_method, table_name, **options)
@@ -20,6 +20,12 @@ module PgParty
20
20
  inject_methods_for(PgParty::Model::ListMethods)
21
21
  end
22
22
 
23
+ def inject_hash_methods
24
+ require "pg_party/model/hash_methods"
25
+
26
+ inject_methods_for(PgParty::Model::HashMethods)
27
+ end
28
+
23
29
  private
24
30
 
25
31
  def inject_methods_for(mod)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgParty
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_party
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Krage
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-12 00:00:00.000000000 Z
11
+ date: 2020-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -44,6 +44,20 @@ dependencies:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: 0.0.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: parallel
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
47
61
  - !ruby/object:Gem::Dependency
48
62
  name: appraisal
49
63
  requirement: !ruby/object:Gem::Requirement
@@ -235,6 +249,7 @@ files:
235
249
  - lib/pg_party/cache.rb
236
250
  - lib/pg_party/config.rb
237
251
  - lib/pg_party/hacks/postgresql_database_tasks.rb
252
+ - lib/pg_party/model/hash_methods.rb
238
253
  - lib/pg_party/model/list_methods.rb
239
254
  - lib/pg_party/model/methods.rb
240
255
  - lib/pg_party/model/range_methods.rb