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 +4 -4
- data/README.md +244 -8
- data/lib/pg_party/adapter/abstract_methods.rb +36 -0
- data/lib/pg_party/adapter/postgresql_methods.rb +36 -0
- data/lib/pg_party/adapter_decorator.rb +278 -23
- data/lib/pg_party/cache.rb +4 -3
- data/lib/pg_party/config.rb +7 -1
- data/lib/pg_party/model/hash_methods.rb +18 -0
- data/lib/pg_party/model/list_methods.rb +4 -0
- data/lib/pg_party/model/methods.rb +4 -0
- data/lib/pg_party/model/range_methods.rb +4 -0
- data/lib/pg_party/model/shared_methods.rb +11 -9
- data/lib/pg_party/model_decorator.rb +22 -9
- data/lib/pg_party/model_injector.rb +6 -0
- data/lib/pg_party/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0edf666f9d4e7d3a7af82990be55d58b970949c5a549c1d8f1e1aeb2c066b09
|
4
|
+
data.tar.gz: 8c1164c72ffec0f00a892f9f4cf7028969a7aa39d056f4aae5e05674651a6142
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
-
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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)}
|
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
|
-
|
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
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
77
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
data/lib/pg_party/cache.rb
CHANGED
@@ -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],
|
31
|
+
LOCK.synchronize { fetch_value(@store[key], sub_key, block) }
|
31
32
|
end
|
32
33
|
|
33
34
|
private
|
data/lib/pg_party/config.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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)
|
data/lib/pg_party/version.rb
CHANGED
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.
|
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-
|
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
|