pg_party 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|