pg_party 1.1.0 → 1.5.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: 11489588c1a9cccdbb8f39a73ebd3b91367c45640677211df0ff73afcb267598
4
- data.tar.gz: 0e2e6184b88e80afa186173c291ae3044976e1bd909f8d3501d35f780c024c09
3
+ metadata.gz: b833474e06e9a20278f5ad629e4c26f290baeeac56a0aa9c4ec6dc1104023393
4
+ data.tar.gz: 881f312c5714dc37760ef49537a5a200b46e72bace05a944890fe78dcc110f71
5
5
  SHA512:
6
- metadata.gz: 1dddde1408aeeee28b7bd0474c39483b3df2ccba68de294eeacc77421e8f8aae0508685f2c5926c695ab3c129d9ae0ffec45fca8d99be7ec8f15e4a7d7e5ead5
7
- data.tar.gz: dc26fa61e8c0d026e7c102c0100d275c9b1d1f5baf2880b6bca82fde7f3fa700d48ed30394e15d3d85fdf786d008148e8b90c23662c43914d4e852a80e976657
6
+ metadata.gz: 6b9a253b6fe399aa6908bc4d7a4e8625622fadf4877af642d22c52f1e52c211a89b23e4cf3d4bec94b461fc9daf4db5de23384091255a33d055872cd337f97ef
7
+ data.tar.gz: 8425ced0e015ceada85d6fedf1b553358a774ff39b85a0d4700c058a8747a24049e4f6d52a7d138722cb7cca3e04f75d883eed51f10df8f8ddbe88de629ead6a
data/README.md CHANGED
@@ -48,6 +48,54 @@ $ gem install pg_party
48
48
  Note that the gemspec does not require `pg`, as some model methods _may_ work for other databases.
49
49
  Migration methods will be unavailable unless `pg` is installed.
50
50
 
51
+ ## Configuration
52
+
53
+ These values can be accessed and set via `PgParty.config` and `PgParty.configure`.
54
+
55
+ - `caching`
56
+ - Whether to cache currently attached partitions and anonymous model classes
57
+ - Default: `true`
58
+ - `caching_ttl`
59
+ - Length of time (in seconds) that cache entries are considered valid
60
+ - Default: `-1` (never expire cache entries)
61
+ - `schema_exclude_partitions`
62
+ - Whether to exclude child partitions in `rake db:structure:dump`
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 behavior is disabled by default as this configuration usually requires composite primary keys to be specified
70
+ and ActiveRecord does not natively support composite primary keys. There are workarounds such as the
71
+ [composite_primary_keys gem](https://github.com/composite-primary-keys/composite_primary_keys).
72
+ * This is not supported for Postgres 10 (requires Postgres 11+)
73
+ * Primary key constraints must include all partition keys, for example: `primary_key: [:id, :created_at], partition_key: :created_at`
74
+ * Partition keys cannot use expressions
75
+ * Can be overridden via the `create_with_primary_key:` option when creating partitioned tables
76
+ - Default: `false`
77
+ - `include_subpartitions_in_partition_list`
78
+ - Whether to include nested subpartitions in the result of `YourModelClass.partiton_list` mby default.
79
+ You can always pass the `include_subpartitions:` option to override this.
80
+ - Default: `false` (for backward compatibility)
81
+
82
+ 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.
83
+
84
+ ### Example
85
+
86
+ ```ruby
87
+ # in a Rails initializer
88
+ PgParty.configure do |c|
89
+ c.caching_ttl = 60
90
+ c.schema_exclude_partitions = false
91
+ c.include_subpartitions_in_partition_list = true
92
+ # Postgres 11+ users starting fresh may consider the below options to rely on Postgres' native features instead of
93
+ # this gem's template tables feature.
94
+ c.create_template_tables = false
95
+ c.create_with_primary_key = true
96
+ end
97
+ ```
98
+
51
99
  ## Usage
52
100
 
53
101
  ### Migrations
@@ -62,25 +110,63 @@ These methods are available in migrations as well as `ActiveRecord::Base#connect
62
110
  - `create_list_partition`
63
111
  - Create partitioned table using the _list_ partitioning method
64
112
  - Required args: `table_name`, `partition_key:`
113
+ - `create_hash_partition` (Postgres 11+)
114
+ - Create partitioned table using the _hash_ partitioning method
115
+ - Required args: `table_name`, `partition_key:`
65
116
  - `create_range_partition_of`
66
117
  - Create partition in _range_ partitioned table with partition key between _range_ of values
67
118
  - Required args: `table_name`, `start_range:`, `end_range:`
119
+ - Create a subpartition by specifying a `partition_type:` of `:range`, `:list`, or `:hash` and a `partition_key:`
68
120
  - `create_list_partition_of`
69
121
  - Create partition in _list_ partitioned table with partition key in _list_ of values
70
122
  - Required args: `table_name`, `values:`
123
+ - Create a subpartition by specifying a `partition_type:` of `:range`, `:list`, or `:hash` and a `partition_key:`
124
+ - `create_hash_partition_of` (Postgres 11+)
125
+ - Create partition in _hash_ partitioned table for partition keys with hashed values having a specific remainder
126
+ - Required args: `table_name`, `modulus:`, `remainder`
127
+ - Create a subpartition by specifying a `partition_type:` of `:range`, `:list`, or `:hash` and a `partition_key:`
128
+ - Note that all partitions in a _hash_ partitioned table should have the same modulus. See [Examples](#examples) for more info.
129
+ - `create_default_partition_of` (Postgres 11+)
130
+ - Create a default partition for values not falling in the range or list constraints of any other partitions
131
+ - Required args: `table_name`
71
132
  - `attach_range_partition`
72
133
  - Attach existing table to _range_ partitioned table with partition key between _range_ of values
73
134
  - Required args: `parent_table_name`, `child_table_name`, `start_range:`, `end_range:`
74
135
  - `attach_list_partition`
75
136
  - Attach existing table to _list_ partitioned table with partition key in _list_ of values
76
137
  - Required args: `parent_table_name`, `child_table_name`, `values:`
138
+ - `attach_hash_partition` (Postgres 11+)
139
+ - Attach existing table to _hash_ partitioned table with partition key hashed values having a specific remainder
140
+ - Required args: `parent_table_name`, `child_table_name`, `modulus:`, `remainder`
141
+ - `attach_default_partition` (Postgres 11+)
142
+ - Attach existing table as the _default_ partition
143
+ - Required args: `parent_table_name`, `child_table_name`
77
144
  - `detach_partition`
78
145
  - Detach partition from both _range and list_ partitioned tables
79
146
  - Required args: `parent_table_name`, `child_table_name`
80
147
  - `create_table_like`
81
148
  - Clone _any_ existing table
82
149
  - Required args: `table_name`, `new_table_name`
83
-
150
+ - `partitions_for_table_name`
151
+ - List all attached partitions for a given table
152
+ - Required args: `table_name`, `include_subpartitions:` (true or false)
153
+ - `parent_for_table_name`
154
+ - Fetch the parent table for a partition
155
+ - Required args: `table_name`
156
+ - Pass optional `traverse: true` to return the top-level table in the hierarchy (for subpartitions)
157
+ - Returns `nil` if the table is not a partition / has no parent
158
+ - `table_partitioned?`
159
+ - Returns true if the table is partitioned (false for non-partitioned tables and partitions themselves)
160
+ - Required args: `table_name`
161
+ - `add_index_on_all_partitions`
162
+ - Recursively add an index to all partitions and subpartitions of `table_name` using Postgres's ADD INDEX CONCURRENTLY
163
+ algorithm which adds the index in a non-blocking manner.
164
+ - Required args: `table_name`, `column_name` (all `add_index` arguments are supported)
165
+ - Use the `in_threads:` option to add indexes in parallel threads when there are many partitions. A value of 2 to 4
166
+ may be reasonable for tables with many large partitions and hosts with 4+ CPUs/cores.
167
+ - Use `disable_ddl_transaction!` in your migration to disable transactions when using this command with `in_threads:`
168
+ or `algorithm: :concurrently`.
169
+
84
170
  #### Examples
85
171
 
86
172
  Create _range_ partitioned table on `created_at::date` with two partitions:
@@ -131,20 +217,127 @@ class CreateSomeListRecord < ActiveRecord::Migration[5.1]
131
217
  create_list_partition_of \
132
218
  :some_list_records,
133
219
  values: 101..200
220
+
221
+ # default partition support is available in Postgres 11 or higher
222
+ create_default_partition_of \
223
+ :some_list_records
224
+ end
225
+ end
226
+ ```
227
+
228
+ Create _hash_ partitioned table on `account_id` with two partitions (Postgres 11+ required):
229
+ * A hash partition can be used to spread keys evenly(ish) across partitions
230
+ * `modulus:` should always equal the total number of partitions planned for the table
231
+ * `remainder:` is an integer which should be in the range of 0 to modulus-1
232
+
233
+ ```ruby
234
+ class CreateSomeHashRecord < ActiveRecord::Migration[5.1]
235
+ def up
236
+ # symbol is used for partition keys referring to individual columns
237
+ # create_with_primary_key: true, template: false on Postgres 11 will rely on PostgreSQL's native partition schema
238
+ # management vs this gem's template tables
239
+ # Note composite primary keys will require a workaround in ActiveRecord, such as through the use of the composite_primary_keys gem
240
+ create_hash_partition :some_hash_records, partition_key: :account_id, primary_key: [:id, :account_id],
241
+ create_with_primary_key: true, template: false do |t|
242
+ t.bigserial :id, null: false
243
+ t.bigint :account_id, null: false
244
+ t.text :some_value
245
+ t.timestamps
246
+ end
247
+
248
+ # without name argument, child partition created as "some_list_records_<hash>"
249
+ create_hash_partition_of \
250
+ :some_hash_records,
251
+ modulus: 2,
252
+ remainder: 0
253
+
254
+ # without name argument, child partition created as "some_list_records_<hash>"
255
+ create_hash_partition_of \
256
+ :some_hash_records,
257
+ modulus: 2,
258
+ remainder: 1
134
259
  end
135
260
  end
136
261
  ```
137
262
 
263
+ Advanced example with subpartitioning: Create _list_ partitioned table on `account_id` subpartitioned by _range_ on `created_at`
264
+ with default partitions. This example is for a table with no primary key... perhaps for some analytics use case.
265
+ * Default partitions are only supported in Postgres 11+
266
+
267
+ ```ruby
268
+ class CreateSomeListSubpartitionedRecord < ActiveRecord::Migration[5.1]
269
+ def up
270
+ create_list_partition :some_list_subpartitioned_records, partition_key: :account_id, id: false,
271
+ template: false do |t|
272
+ t.bigint :account_id, null: false
273
+ t.text :some_value
274
+ t.created_at
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
138
328
  Unfortunately, PostgreSQL 10 doesn't support indexes on partitioned tables.
139
329
  However, individual _partitions_ can have indexes.
140
330
  To avoid explicit index creation for _every_ new partition, we've introduced the idea of template tables.
141
331
  For every call to `create_list_partition` and `create_range_partition`, a clone `<table_name>_template` is created.
142
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)\
335
+ but this may result in you using composite primary keys, which is not natively supported by ActiveRecord.
143
336
 
144
337
  ```ruby
145
338
  class CreateSomeListRecord < ActiveRecord::Migration[5.1]
146
339
  def up
147
- # template table creation is enabled by default - use "template: false" to opt-out
340
+ # template table creation is enabled by default - use "template: false" or the config option to opt-out
148
341
  create_list_partition :some_list_records, partition_key: :id do |t|
149
342
  t.integer :some_foreign_id
150
343
  t.text :some_value
@@ -167,6 +360,8 @@ class CreateSomeListRecord < ActiveRecord::Migration[5.1]
167
360
  end
168
361
  ```
169
362
 
363
+ #### Attaching Existing Tables as Partitions
364
+
170
365
  Attach an existing table to a _range_ partitioned table:
171
366
 
172
367
  ```ruby
@@ -194,6 +389,20 @@ class AttachListPartition < ActiveRecord::Migration[5.1]
194
389
  end
195
390
  ```
196
391
 
392
+ Attach an existing table to a _hash_ partitioned table:
393
+
394
+ ```ruby
395
+ class AttachHashPartition < ActiveRecord::Migration[5.1]
396
+ def up
397
+ attach_hash_partition \
398
+ :some_hash_records,
399
+ :some_existing_table,
400
+ modulus: 2,
401
+ remainder: 1
402
+ end
403
+ end
404
+ ```
405
+
197
406
  Detach a partition from any partitioned table:
198
407
 
199
408
  ```ruby
@@ -204,6 +413,31 @@ class DetachPartition < ActiveRecord::Migration[5.1]
204
413
  end
205
414
  ```
206
415
 
416
+ #### Safely cascading `add_index` commands
417
+ Postgres 11+ will automatically cascade CREATE INDEX operations to partitions and subpartitions, however
418
+ CREATE INDEX CONCURRENTLY is not supported, meaning table locks will be taken on each table while the new index is built.
419
+ Postgres 10 provides no way to cascade index creation natively.
420
+ * The `add_index_on_all_partitions` method solves for these limitations by recursively creating the specified
421
+ index on all partitions and subpartitions. Index names on individual partitions will include a hash suffix to avoid conflicts.
422
+ * On Postgres 11+, the created indices are correctly attached to an index on the parent table
423
+ * 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.
424
+ * This command can also be used on subpartitions to cascade targeted indices starting at one level of the table hierarchy
425
+
426
+ ```ruby
427
+ class AddSomeValueIndexToSomeListRecord < ActiveRecord::Migration[5.1]
428
+ # add_index_on_all_partitions with in_threads option may not be used within a transaction
429
+ # (also, algorithm: :concurrently cannot be used within a transaction)
430
+ disable_ddl_transaction!
431
+
432
+ def up
433
+ add_index :some_records_template, :some_value # Only if using Postgres 10 with template tables
434
+
435
+ # Pass the `in_threads:` option to create indices in parallel across multiple Postgres connections
436
+ add_index_on_all_partitions :some_records, :some_value, algorithm: :concurrently, in_threads: 4
437
+ end
438
+ end
439
+ ```
440
+
207
441
  For more examples, take a look at the Combustion schema definition and integration specs:
208
442
 
209
443
  - https://github.com/rkrage/pg_party/blob/master/spec/dummy/db/schema.rb
@@ -224,12 +458,15 @@ Class methods available to _all_ ActiveRecord models:
224
458
  - `list_partition_by`
225
459
  - Configure a model backed by a _list_ partitioned table
226
460
  - Required arg: `key` (partition key column) or block returning partition key expression
461
+ - `hash_partition_by`
462
+ - Configure a model backed by a _hash_ partitioned table
463
+ - Required arg: `key` (partition key column) or block returning partition key expression
227
464
 
228
465
  Class methods available to both _range and list_ partitioned models:
229
466
 
230
467
  - `partitions`
231
468
  - Retrieve a list of currently attached partitions
232
- - No arguments
469
+ - Optional `include_subpartitions:` argument to include all subpartitions in the returned list
233
470
  - `in_partition`
234
471
  - Retrieve an ActiveRecord model scoped to an individual partition
235
472
  - Required arg: `child_table_name`
@@ -254,6 +491,16 @@ Class methods available to _list_ partitioned models:
254
491
  - `partition_key_in`
255
492
  - Query for records where partition key in _list_ of values
256
493
  - Required arg: list of `values`
494
+
495
+
496
+ Class methods available to _hash_ partitioned models:
497
+
498
+ - `create_partition`
499
+ - Dynamically create new partition with hashed partition key divided by _modulus_ equals _remainder_
500
+ - Required arg: `modulus:`, `remainder:`
501
+ - `partition_key_in`
502
+ - Query for records where partition key in _list_ of values (method operates the same as for _list_ partitions above)
503
+ - Required arg: list of `values`
257
504
 
258
505
  #### Examples
259
506
 
@@ -275,6 +522,15 @@ class SomeListRecord < ApplicationRecord
275
522
  end
276
523
  ```
277
524
 
525
+ Configure model backed by a _hash_ partitioned table to get access to the methods described above:
526
+
527
+ ```ruby
528
+ class SomeHashRecord < ApplicationRecord
529
+ # symbol is used for partition keys referring to individual columns
530
+ hash_partition_by :id
531
+ end
532
+ ```
533
+
278
534
  Dynamically create new partition from _range_ partitioned model:
279
535
 
280
536
  ```ruby
@@ -289,19 +545,26 @@ Dynamically create new partition from _list_ partitioned model:
289
545
  SomeListRecord.create_partition(values: 200..300)
290
546
  ```
291
547
 
548
+ Dynamically create new partition from _hash_ partitioned model:
549
+
550
+ ```ruby
551
+ # additional options include: "name:" and "primary_key:"
552
+ SomeHashRecord.create_partition(modulus: 2, remainder: 1)
553
+ ```
554
+
292
555
  For _range_ partitioned model, query for records where partition key in _range_ of values:
293
556
 
294
557
  ```ruby
295
558
  SomeRangeRecord.partition_key_in("2019-06-08", "2019-06-10")
296
559
  ```
297
560
 
298
- For _list_ partitioned model, query for records where partition key in _list_ of values:
561
+ For _list_ and _hash_ partitioned models, query for records where partition key in _list_ of values:
299
562
 
300
563
  ```ruby
301
564
  SomeListRecord.partition_key_in(1, 2, 3, 4)
302
565
  ```
303
566
 
304
- For both _range and list_ partitioned models, query for records matching partition key:
567
+ For all partitioned models, query for records matching partition key:
305
568
 
306
569
  ```ruby
307
570
  SomeRangeRecord.partition_key_eq(Date.current)
@@ -309,15 +572,15 @@ SomeRangeRecord.partition_key_eq(Date.current)
309
572
  SomeListRecord.partition_key_eq(100)
310
573
  ```
311
574
 
312
- For both _range and list_ partitioned models, retrieve currently attached partitions:
575
+ For all partitioned models, retrieve currently attached partitions:
313
576
 
314
577
  ```ruby
315
578
  SomeRangeRecord.partitions
316
579
 
317
- SomeListRecord.partitions
580
+ SomeListRecord.partitions(include_subpartitions: true) # Include nested subpartitions
318
581
  ```
319
582
 
320
- For both _range and list_ partitioned models, retrieve ActiveRecord model scoped to individual partition:
583
+ For both all partitioned models, retrieve ActiveRecord model scoped to individual partition:
321
584
 
322
585
  ```ruby
323
586
  SomeRangeRecord.in_partition(:some_range_records_partition_name)
@@ -325,9 +588,35 @@ SomeRangeRecord.in_partition(:some_range_records_partition_name)
325
588
  SomeListRecord.in_partition(:some_list_records_partition_name)
326
589
  ```
327
590
 
591
+ To create _range_ partitions by month for previous, current and next months it's possible to use this example. To automate creation of partitions, run `Log.maintenance` every day with cron:
592
+
593
+ ```ruby
594
+ class Log < ApplicationRecord
595
+ range_partition_by { '(created_at::date)' }
596
+
597
+ def self.maintenance
598
+ partitions = [Date.today.prev_month, Date.today, Date.today.next_month]
599
+
600
+ partitions.each do |day|
601
+ name = Log.partition_name_for(day)
602
+ next if ActiveRecord::Base.connection.table_exists?(name)
603
+ Log.create_partition(
604
+ name: name,
605
+ start_range: day.beginning_of_month,
606
+ end_range: day.next_month.beginning_of_month
607
+ )
608
+ end
609
+ end
610
+
611
+ def self.partition_name_for(day)
612
+ "logs_y#{day.year}_m#{day.month}"
613
+ end
614
+ end
615
+ ```
616
+
328
617
  For more examples, take a look at the model integration specs:
329
618
 
330
- - https://github.com/rkrage/pg_party/tree/documentation/spec/integration/model
619
+ - https://github.com/rkrage/pg_party/tree/master/spec/integration/model
331
620
 
332
621
  ## Development
333
622