pg_party 0.7.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 4b3cbb99316596bf151d53ef3d550d449e7f7853
4
- data.tar.gz: 2f0185c8e5ae5b8928ae0c196eafe0b72dbca215
2
+ SHA256:
3
+ metadata.gz: b3eb42aaee8d1f94ffd962760c6f4596330cbdc899a314738848861bfc01e29c
4
+ data.tar.gz: '049172b0bb51a0d8664ad54b4599a588c448b8558861c3f50688fb938aaf7680'
5
5
  SHA512:
6
- metadata.gz: 4a6a84178f454a81549f02ddd3e2871de48269a8ac86e6f1e68a6df8f8bd40cc8b85945e8914879a8a5543fc71257792d44cf0de0d20dda0c7b67fe0afcceac6
7
- data.tar.gz: 1cb16eaf85e97774734eff7e316a404c3d6980e1d95932d6f145cef74f17db83f79e48cc264812c32ff696a135dc82c4f815f0f4624a4d176d416e85a5338e4c
6
+ metadata.gz: 0e00484e8d7142cdff81b64301f5cf2207318e07dd9f918730472ece7840e2025e71e15a827987042c3ede9c66ad41633af88cc4ec7e0f83d7a4e28c57e822a8
7
+ data.tar.gz: c3fa8f87087bd47e87910fcb2707cd58d83c7e6bf91207dfde299b550c9aace44cc905a7781d9ddc5b4281a8de654bf096813420aa8937442bf09a053f13f549
data/README.md CHANGED
@@ -2,26 +2,28 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/pg_party.svg)][rubygems]
4
4
  [![Build Status](https://circleci.com/gh/rkrage/pg_party.svg?&style=shield)][circle]
5
- [![Dependency Status](https://gemnasium.com/badges/github.com/rkrage/pg_party.svg)][gemnasium]
6
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/c409453d2283dd440227/maintainability)][cc_maintainability]
7
6
  [![Test Coverage](https://api.codeclimate.com/v1/badges/c409453d2283dd440227/test_coverage)][cc_coverage]
8
7
 
9
8
  [rubygems]: https://rubygems.org/gems/pg_party
10
9
  [circle]: https://circleci.com/gh/rkrage/pg_party/tree/master
11
- [gemnasium]: https://gemnasium.com/github.com/rkrage/pg_party
12
10
  [cc_maintainability]: https://codeclimate.com/github/rkrage/pg_party/maintainability
13
11
  [cc_coverage]: https://codeclimate.com/github/rkrage/pg_party/test_coverage
14
12
 
15
- [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html) migrations and model helpers for creating and managing [PostgreSQL 10 partitions](https://www.postgresql.org/docs/10/static/ddl-partitioning.html)!
13
+ [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html) migrations and model helpers for creating and managing [PostgreSQL 10+ partitions](https://www.postgresql.org/docs/10/static/ddl-partitioning.html)!
16
14
 
17
- Features:
18
- - Migration methods for partition specific database operations
19
- - Model methods for querying partitioned data
20
- - Model methods for creating adhoc partitions
15
+ ## Features
21
16
 
22
- Limitations:
23
- - Partition tables are not represented correctly in `db/schema.rb` please use the `:sql` schema format
24
- - Only single column partition keys supported (e.g., `column`, `column::cast`)
17
+ - Migration methods for partition specific database operations
18
+ - Model methods for querying partitioned data, creating adhoc partitions, and retreiving partition metadata
19
+
20
+ ## Limitations
21
+
22
+ - Partition tables are not represented correctly in `db/schema.rb` — please use the `:sql` schema format
23
+
24
+ ## Future Work
25
+
26
+ - Automatic partition creation (via cron or some other means)
25
27
 
26
28
  ## Installation
27
29
 
@@ -33,89 +35,152 @@ gem 'pg_party'
33
35
 
34
36
  And then execute:
35
37
 
36
- $ bundle
38
+ ```
39
+ $ bundle
40
+ ```
37
41
 
38
42
  Or install it yourself as:
39
43
 
40
- $ gem install pg_party
41
-
42
- ## Usage
43
-
44
- Full API documentation is in progress.
44
+ ```
45
+ $ gem install pg_party
46
+ ```
45
47
 
46
- In the meantime, take a look at the [Combustion](https://github.com/pat/combustion) schema definition and integration specs:
47
- - https://github.com/rkrage/pg_party/blob/master/spec/internal/db/schema.rb
48
- - https://github.com/rkrage/pg_party/tree/master/spec/integration
48
+ Note that the gemspec does not require `pg`, as some model methods _may_ work for other databases.
49
+ Migration methods will be unavailable unless `pg` is installed.
49
50
 
50
- ### Migration Examples
51
+ ## Usage
51
52
 
52
- Create range partition on `created_at::date` with two child partitions:
53
+ ### Migrations
54
+
55
+ #### Methods
56
+
57
+ These methods are available in migrations as well as `ActiveRecord::Base#connection` objects.
58
+
59
+ - `create_range_partition`
60
+ - Create partitioned table using the _range_ partitioning method
61
+ - Required args: `table_name`, `partitition_key:`
62
+ - `create_list_partition`
63
+ - Create partitioned table using the _list_ partitioning method
64
+ - Required args: `table_name`, `partition_key:`
65
+ - `create_range_partition_of`
66
+ - Create partition in _range_ partitioned table with partition key between _range_ of values
67
+ - Required args: `table_name`, `start_range:`, `end_range:`
68
+ - `create_list_partition_of`
69
+ - Create partition in _list_ partitioned table with partition key in _list_ of values
70
+ - Required args: `table_name`, `values:`
71
+ - `attach_range_partition`
72
+ - Attach existing table to _range_ partitioned table with partition key between _range_ of values
73
+ - Required args: `parent_table_name`, `child_table_name`, `start_range:`, `end_range:`
74
+ - `attach_list_partition`
75
+ - Attach existing table to _list_ partitioned table with partition key in _list_ of values
76
+ - Required args: `parent_table_name`, `child_table_name`, `values:`
77
+ - `detach_partition`
78
+ - Detach partition from both _range and list_ partitioned tables
79
+ - Required args: `parent_table_name`, `child_table_name`
80
+ - `create_table_like`
81
+ - Clone _any_ existing table
82
+ - Required args: `table_name`, `new_table_name`
83
+
84
+ #### Examples
85
+
86
+ Create _range_ partitioned table on `created_at::date` with two partitions:
53
87
 
54
88
  ```ruby
55
89
  class CreateSomeRangeRecord < ActiveRecord::Migration[5.1]
56
90
  def up
57
- current_date = Date.current
58
-
59
- create_range_partition :some_range_records, partition_key: "created_at::date" do |t|
91
+ # proc is used for partition keys containing expressions
92
+ create_range_partition :some_range_records, partition_key: ->{ "(created_at::date)" } do |t|
60
93
  t.text :some_value
61
94
  t.timestamps
62
95
  end
63
96
 
97
+ # optional name argument is used to specify child table name
64
98
  create_range_partition_of \
65
99
  :some_range_records,
66
- partition_key: "created_at::date",
67
- start_range: current_date,
68
- end_range: current_date + 1.day
100
+ name: :some_range_records_a,
101
+ start_range: "2019-06-07",
102
+ end_range: "2019-06-08"
69
103
 
104
+ # optional name argument is used to specify child table name
70
105
  create_range_partition_of \
71
106
  :some_range_records,
72
- partition_key: "created_at::date",
73
- start_range: current_date + 1.day,
74
- end_range: current_date + 2.days
107
+ name: :some_range_records_b,
108
+ start_range: "2019-06-08",
109
+ end_range: "2019-06-09"
75
110
  end
76
111
  end
77
112
  ```
78
113
 
79
- Create list partition on `id` with two child partitions:
114
+ Create _list_ partitioned table on `id` with two partitions:
80
115
 
81
116
  ```ruby
82
117
  class CreateSomeListRecord < ActiveRecord::Migration[5.1]
83
118
  def up
119
+ # symbol is used for partition keys referring to individual columns
84
120
  create_list_partition :some_list_records, partition_key: :id do |t|
85
121
  t.text :some_value
86
122
  t.timestamps
87
123
  end
88
124
 
125
+ # without name argument, child partition created as "some_list_records_<hash>"
89
126
  create_list_partition_of \
90
127
  :some_list_records,
91
- partition_key: :id,
92
- values: (1..100).to_a
128
+ values: 1..100
93
129
 
130
+ # without name argument, child partition created as "some_list_records_<hash>"
94
131
  create_list_partition_of \
95
132
  :some_list_records,
96
- partition_key: :id,
97
- values: (100..200).to_a
133
+ values: 101..200
98
134
  end
99
135
  end
100
136
  ```
101
137
 
102
- Attach an existing table to a range partition:
138
+ Unfortunately, PostgreSQL 10 doesn't support indexes on partitioned tables.
139
+ However, individual _partitions_ can have indexes.
140
+ To avoid explicit index creation for _every_ new partition, we've introduced the idea of template tables.
141
+ For every call to `create_list_partition` and `create_range_partition`, a clone `<table_name>_template` is created.
142
+ 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`:
103
143
 
104
144
  ```ruby
105
- class AttachRangePartition < ActiveRecord::Migration[5.1]
145
+ class CreateSomeListRecord < ActiveRecord::Migration[5.1]
106
146
  def up
107
- current_date = Date.current
147
+ # template table creation is enabled by default - use "template: false" to opt-out
148
+ create_list_partition :some_list_records, partition_key: :id do |t|
149
+ t.integer :some_foreign_id
150
+ t.text :some_value
151
+ t.timestamps
152
+ end
153
+
154
+ # create index on the template table
155
+ add_index :some_list_records_template, :some_foreign_id
156
+
157
+ # create partition with an index on "some_foreign_id"
158
+ create_list_partition_of \
159
+ :some_list_records,
160
+ values: 1..100
161
+
162
+ # create partition with an index on "some_foreign_id"
163
+ create_list_partition_of \
164
+ :some_list_records,
165
+ values: 101..200
166
+ end
167
+ end
168
+ ```
169
+
170
+ Attach an existing table to a _range_ partitioned table:
108
171
 
172
+ ```ruby
173
+ def up
109
174
  attach_range_partition \
110
175
  :some_range_records,
111
176
  :some_existing_table,
112
- start_range: current_date,
113
- end_range: current_date + 1.day
177
+ start_range: "2019-06-09",
178
+ end_range: "2019-06-10"
114
179
  end
115
180
  end
116
181
  ```
117
182
 
118
- Attach an existing table to a list partition:
183
+ Attach an existing table to a _list_ partitioned table:
119
184
 
120
185
  ```ruby
121
186
  class AttachListPartition < ActiveRecord::Migration[5.1]
@@ -123,12 +188,12 @@ class AttachListPartition < ActiveRecord::Migration[5.1]
123
188
  attach_list_partition \
124
189
  :some_list_records,
125
190
  :some_existing_table,
126
- values: (200..300).to_a
191
+ values: 200..300
127
192
  end
128
193
  end
129
194
  ```
130
195
 
131
- Detach a child table from any partition:
196
+ Detach a partition from any partitioned table:
132
197
 
133
198
  ```ruby
134
199
  class DetachPartition < ActiveRecord::Migration[5.1]
@@ -138,51 +203,104 @@ class DetachPartition < ActiveRecord::Migration[5.1]
138
203
  end
139
204
  ```
140
205
 
141
- ### Model Examples
206
+ For more examples, take a look at the Combustion schema definition and integration specs:
207
+
208
+ - https://github.com/rkrage/pg_party/blob/master/spec/dummy/db/schema.rb
209
+ - https://github.com/rkrage/pg_party/blob/master/spec/integration/migration_spec.rb
210
+
211
+ ### Models
212
+
213
+ #### Methods
214
+
215
+ Class methods available to _all_ ActiveRecord models:
216
+
217
+ - `partitioned?`
218
+ - Check if a model is backed by either a _list or range_ partitioned table
219
+ - No arguments
220
+ - `range_partition_by`
221
+ - Configure a model backed by a _range_ partitioned table
222
+ - Required arg: `key` (partition key column) or block returning partition key expression
223
+ - `list_partition_by`
224
+ - Configure a model backed by a _list_ partitioned table
225
+ - Required arg: `key` (partition key column) or block returning partition key expression
226
+
227
+ Class methods available to both _range and list_ partitioned models:
228
+
229
+ - `partitions`
230
+ - Retrieve a list of currently attached partitions
231
+ - No arguments
232
+ - `in_partition`
233
+ - Retrieve an ActiveRecord model scoped to an individual partition
234
+ - Required arg: `child_table_name`
235
+ - `partition_key_eq`
236
+ - Query for records where partition key matches a value
237
+ - Required arg: `value`
238
+
239
+ Class methods available to _range_ partitioned models:
240
+
241
+ - `create_partition`
242
+ - Dynamically create new partition with partition key in _range_ of values
243
+ - Required args: `start_range:`, `end_range:`
244
+ - `partition_key_in`
245
+ - Query for records where partition key in _range_ of values
246
+ - Required args: `start_range`, `end_range`
247
+
248
+ Class methods available to _list_ partitioned models:
142
249
 
143
- Define model that is backed by a range partition:
250
+ - `create_partition`
251
+ - Dynamically create new partition with partition key in _list_ of values
252
+ - Required arg: `values:`
253
+ - `partition_key_in`
254
+ - Query for records where partition key in _list_ of values
255
+ - Required arg: list of `values`
256
+
257
+ #### Examples
258
+
259
+ Configure model backed by a _range_ partitioned table to get access to the methods described above:
144
260
 
145
261
  ```ruby
146
262
  class SomeRangeRecord < ApplicationRecord
147
- range_partition_by "created_at::date"
263
+ # block is used for partition keys containing expressions
264
+ range_partition_by { "(created_at::date)" }
148
265
  end
149
266
  ```
150
267
 
151
- Define model that is backed by a list partition:
268
+ Configure model backed by a _list_ partitioned table to get access to the methods described above:
152
269
 
153
270
  ```ruby
154
271
  class SomeListRecord < ApplicationRecord
272
+ # symbol is used for partition keys referring to individual columns
155
273
  list_partition_by :id
156
274
  end
157
275
  ```
158
276
 
159
- Create child partition from range partition model:
277
+ Dynamically create new partition from _range_ partitioned model:
160
278
 
161
279
  ```ruby
162
- current_date = Date.current
163
-
164
- SomeRangeRecord.create_partition(start_range: current_date + 1.day, end_range: current_date + 2.days)
280
+ # additional options include: "name:" and "primary_key:"
281
+ SomeRangeRecord.create_partition(start_range: "2019-06-09", end_range: "2019-06-10")
165
282
  ```
166
283
 
167
- Create child partition from list partition model:
284
+ Dynamically create new partition from _list_ partitioned model:
168
285
 
169
286
  ```ruby
170
- SomeListRecord.create_partition(values: (200..300).to_a)
287
+ # additional options include: "name:" and "primary_key:"
288
+ SomeListRecord.create_partition(values: 200..300)
171
289
  ```
172
290
 
173
- Query for records within partition range:
291
+ For _range_ partitioned model, query for records where partition key in _range_ of values:
174
292
 
175
293
  ```ruby
176
- SomeRangeRecord.partition_key_in("2017-01-01".to_date, "2017-02-01".to_date)
294
+ SomeRangeRecord.partition_key_in("2019-06-08", "2019-06-10")
177
295
  ```
178
296
 
179
- Query for records in partition list:
297
+ For _list_ partitioned model, query for records where partition key in _list_ of values:
180
298
 
181
299
  ```ruby
182
300
  SomeListRecord.partition_key_in(1, 2, 3, 4)
183
301
  ```
184
302
 
185
- Query for records matching partition key:
303
+ For both _range and list_ partitioned models, query for records matching partition key:
186
304
 
187
305
  ```ruby
188
306
  SomeRangeRecord.partition_key_eq(Date.current)
@@ -190,7 +308,7 @@ SomeRangeRecord.partition_key_eq(Date.current)
190
308
  SomeListRecord.partition_key_eq(100)
191
309
  ```
192
310
 
193
- List currently attached partitions:
311
+ For both _range and list_ partitioned models, retrieve currently attached partitions:
194
312
 
195
313
  ```ruby
196
314
  SomeRangeRecord.partitions
@@ -198,7 +316,7 @@ SomeRangeRecord.partitions
198
316
  SomeListRecord.partitions
199
317
  ```
200
318
 
201
- Retrieve ActiveRecord model class scoped to a child partition:
319
+ For both _range and list_ partitioned models, retrieve ActiveRecord model scoped to individual partition:
202
320
 
203
321
  ```ruby
204
322
  SomeRangeRecord.in_partition(:some_range_records_partition_name)
@@ -206,26 +324,38 @@ SomeRangeRecord.in_partition(:some_range_records_partition_name)
206
324
  SomeListRecord.in_partition(:some_list_records_partition_name)
207
325
  ```
208
326
 
327
+ For more examples, take a look at the model integration specs:
328
+
329
+ - https://github.com/rkrage/pg_party/tree/documentation/spec/integration/model
330
+
209
331
  ## Development
210
332
 
211
333
  The development / test environment relies heavily on [Docker](https://docs.docker.com).
212
334
 
213
335
  Start the containers in the background:
214
336
 
215
- $ docker-compose up -d
337
+ ```
338
+ $ docker-compose up -d
339
+ ```
216
340
 
217
341
  Install dependencies:
218
342
 
219
- $ bin/de bundle
220
- $ bin/de appraisal
343
+ ```
344
+ $ bin/de bundle
345
+ $ bin/de appraisal
346
+ ```
221
347
 
222
348
  Run the tests:
223
349
 
224
- $ bin/de appraisal rake
350
+ ```
351
+ $ bin/de appraisal rake
352
+ ```
225
353
 
226
354
  Open a Pry console to play around with the sample Rails app:
227
355
 
228
- $ bin/de console
356
+ ```
357
+ $ bin/de console
358
+ ```
229
359
 
230
360
  ## Contributing
231
361
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_party/version"
2
4
  require "active_support"
3
5
 
@@ -8,17 +10,23 @@ ActiveSupport.on_load(:active_record) do
8
10
 
9
11
  require "pg_party/adapter/abstract_methods"
10
12
 
11
- ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
12
- include PgParty::Adapter::AbstractMethods
13
- end
14
-
13
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.include(
14
+ PgParty::Adapter::AbstractMethods
15
+ )
16
+
17
+ require "pg_party/hacks/schema_cache"
18
+
19
+ ActiveRecord::ConnectionAdapters::SchemaCache.include(
20
+ PgParty::Hacks::SchemaCache
21
+ )
22
+
15
23
  begin
16
24
  require "active_record/connection_adapters/postgresql_adapter"
17
25
  require "pg_party/adapter/postgresql_methods"
18
26
 
19
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
20
- include PgParty::Adapter::PostgreSQLMethods
21
- end
27
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(
28
+ PgParty::Adapter::PostgreSQLMethods
29
+ )
22
30
  rescue LoadError
23
31
  # migration methods will not be available
24
32
  end
@@ -1,32 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PgParty
2
4
  module Adapter
3
5
  module AbstractMethods
4
6
  def create_range_partition(*)
5
- raise NotImplementedError, "#create_range_partition is not implemented"
7
+ raise "#create_range_partition is not implemented"
6
8
  end
7
9
 
8
10
  def create_list_partition(*)
9
- raise NotImplementedError, "#create_list_partition is not implemented"
11
+ raise "#create_list_partition is not implemented"
10
12
  end
11
13
 
12
14
  def create_range_partition_of(*)
13
- raise NotImplementedError, "#create_range_partition_of is not implemented"
15
+ raise "#create_range_partition_of is not implemented"
14
16
  end
15
17
 
16
18
  def create_list_partition_of(*)
17
- raise NotImplementedError, "#create_list_partition_of is not implemented"
19
+ raise "#create_list_partition_of is not implemented"
20
+ end
21
+
22
+ def create_table_like(*)
23
+ raise "#create_table_like is not implemented"
18
24
  end
19
25
 
20
26
  def attach_range_partition(*)
21
- raise NotImplementedError, "#attach_range_partition is not implemented"
27
+ raise "#attach_range_partition is not implemented"
22
28
  end
23
29
 
24
30
  def attach_list_partition(*)
25
- raise NotImplementedError, "#attach_list_partition is not implemented"
31
+ raise "#attach_list_partition is not implemented"
26
32
  end
27
33
 
28
34
  def detach_partition(*)
29
- raise NotImplementedError, "#detach_partition is not implemented"
35
+ raise "#detach_partition is not implemented"
30
36
  end
31
37
  end
32
38
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_party/adapter_decorator"
2
4
 
3
5
  module PgParty
@@ -19,6 +21,10 @@ module PgParty
19
21
  PgParty::AdapterDecorator.new(self).create_list_partition_of(*args)
20
22
  end
21
23
 
24
+ def create_table_like(*args)
25
+ PgParty::AdapterDecorator.new(self).create_table_like(*args)
26
+ end
27
+
22
28
  def attach_range_partition(*args)
23
29
  PgParty::AdapterDecorator.new(self).attach_range_partition(*args)
24
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "digest"
2
4
  require "pg_party/cache"
3
5
 
@@ -18,47 +20,39 @@ module PgParty
18
20
  end
19
21
 
20
22
  def create_range_partition_of(table_name, start_range:, end_range:, **options)
21
- if options[:name]
22
- child_table_name = options[:name]
23
- else
24
- child_table_name = hashed_table_name(table_name, "#{start_range}#{end_range}")
25
- end
26
-
27
- constraint_clause = "FROM (#{quote(start_range)}) TO (#{quote(end_range)})"
28
-
29
- create_partition_of(table_name, child_table_name, constraint_clause, **options)
23
+ create_partition_of(table_name, range_constraint_clause(start_range, end_range), **options)
30
24
  end
31
25
 
32
26
  def create_list_partition_of(table_name, values:, **options)
33
- if options[:name]
34
- child_table_name = options[:name]
35
- else
36
- child_table_name = hashed_table_name(table_name, values.to_s)
37
- end
27
+ create_partition_of(table_name, list_constraint_clause(values), **options)
28
+ end
38
29
 
39
- constraint_clause = "IN (#{Array.wrap(values).map(&method(:quote)).join(",")})"
30
+ def create_table_like(table_name, new_table_name, **options)
31
+ primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) }
40
32
 
41
- create_partition_of(table_name, child_table_name, constraint_clause, **options)
42
- end
33
+ validate_primary_key(primary_key)
43
34
 
44
- def attach_range_partition(parent_table_name, child_table_name, start_range:, end_range:)
45
35
  execute(<<-SQL)
46
- ALTER TABLE #{quote_table_name(parent_table_name)}
47
- ATTACH PARTITION #{quote_table_name(child_table_name)}
48
- FOR VALUES FROM (#{quote(start_range)}) TO (#{quote(end_range)})
36
+ CREATE TABLE #{quote_table_name(new_table_name)} (
37
+ LIKE #{quote_table_name(table_name)} INCLUDING ALL
38
+ )
49
39
  SQL
50
40
 
51
- PgParty::Cache.clear!
52
- end
41
+ return if !primary_key
42
+ return if has_primary_key?(new_table_name)
53
43
 
54
- def attach_list_partition(parent_table_name, child_table_name, values:)
55
44
  execute(<<-SQL)
56
- ALTER TABLE #{quote_table_name(parent_table_name)}
57
- ATTACH PARTITION #{quote_table_name(child_table_name)}
58
- FOR VALUES IN (#{Array.wrap(values).map(&method(:quote)).join(",")})
45
+ ALTER TABLE #{quote_table_name(new_table_name)}
46
+ ADD PRIMARY KEY (#{quote_column_name(primary_key)})
59
47
  SQL
48
+ end
60
49
 
61
- PgParty::Cache.clear!
50
+ def attach_range_partition(parent_table_name, child_table_name, start_range:, end_range:)
51
+ attach_partition(parent_table_name, child_table_name, range_constraint_clause(start_range, end_range))
52
+ end
53
+
54
+ def attach_list_partition(parent_table_name, child_table_name, values:)
55
+ attach_partition(parent_table_name, child_table_name, list_constraint_clause(values))
62
56
  end
63
57
 
64
58
  def detach_partition(parent_table_name, child_table_name)
@@ -73,16 +67,17 @@ module PgParty
73
67
  private
74
68
 
75
69
  def create_partition(table_name, type, partition_key, **options)
76
- modified_options = options.except(:id, :primary_key)
70
+ modified_options = options.except(:id, :primary_key, :template)
71
+ template = options.fetch(:template, true)
77
72
  id = options.fetch(:id, :bigserial)
78
- primary_key = options.fetch(:primary_key, :id)
73
+ primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) }
79
74
 
80
- raise ArgumentError, "composite primary key not supported" if primary_key.is_a?(Array)
75
+ validate_primary_key(primary_key)
81
76
 
82
77
  modified_options[:id] = false
83
- modified_options[:options] = "PARTITION BY #{type.to_s.upcase} ((#{quote_partition_key(partition_key)}))"
78
+ modified_options[:options] = "PARTITION BY #{type.to_s.upcase} (#{quote_partition_key(partition_key)})"
84
79
 
85
- result = create_table(table_name, modified_options) do |td|
80
+ create_table(table_name, modified_options) do |td|
86
81
  if id == :uuid
87
82
  td.column(primary_key, id, null: false, default: uuid_function)
88
83
  elsif id
@@ -95,42 +90,35 @@ module PgParty
95
90
  # Rails 4 has a bug where uuid columns are always nullable
96
91
  change_column_null(table_name, primary_key, false) if id == :uuid
97
92
 
98
- result
93
+ return unless template
94
+
95
+ create_table_like(table_name, template_table_name(table_name), primary_key: id && primary_key)
99
96
  end
100
97
 
101
- def create_partition_of(table_name, child_table_name, constraint_clause, **options)
102
- primary_key = options.fetch(:primary_key, :id)
103
- index = options.fetch(:index, true)
104
- partition_key = options[:partition_key]
98
+ def create_partition_of(table_name, constraint_clause, **options)
99
+ child_table_name = options.fetch(:name) { hashed_table_name(table_name, constraint_clause) }
100
+ primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) }
101
+ template_table_name = template_table_name(table_name)
105
102
 
106
- raise ArgumentError, "composite primary key not supported" if primary_key.is_a?(Array)
103
+ if schema_cache.data_source_exists?(template_table_name)
104
+ create_table_like(template_table_name, child_table_name, primary_key: false)
105
+ else
106
+ create_table_like(table_name, child_table_name, primary_key: primary_key)
107
+ end
107
108
 
109
+ attach_partition(table_name, child_table_name, constraint_clause)
110
+
111
+ child_table_name
112
+ end
113
+
114
+ def attach_partition(parent_table_name, child_table_name, constraint_clause)
108
115
  execute(<<-SQL)
109
- CREATE TABLE #{quote_table_name(child_table_name)}
110
- PARTITION OF #{quote_table_name(table_name)}
116
+ ALTER TABLE #{quote_table_name(parent_table_name)}
117
+ ATTACH PARTITION #{quote_table_name(child_table_name)}
111
118
  FOR VALUES #{constraint_clause}
112
119
  SQL
113
120
 
114
- if primary_key
115
- execute(<<-SQL)
116
- ALTER TABLE #{quote_table_name(child_table_name)}
117
- ADD PRIMARY KEY (#{quote_column_name(primary_key)})
118
- SQL
119
- end
120
-
121
- if index && partition_key && primary_key != partition_key
122
- index_name = index_name(child_table_name, partition_key)
123
-
124
- execute(<<-SQL)
125
- CREATE INDEX #{quote_table_name(index_name)}
126
- ON #{quote_table_name(child_table_name)}
127
- USING btree ((#{quote_partition_key(partition_key)}))
128
- SQL
129
- end
130
-
131
121
  PgParty::Cache.clear!
132
-
133
- child_table_name
134
122
  end
135
123
 
136
124
  # Rails 5.2 now returns boolean literals
@@ -145,8 +133,40 @@ module PgParty
145
133
  end
146
134
  end
147
135
 
136
+ def has_primary_key?(table_name)
137
+ primary_key(table_name).present?
138
+ end
139
+
140
+ def calculate_primary_key(table_name)
141
+ ActiveRecord::Base.get_primary_key(table_name.to_s.singularize).to_sym
142
+ end
143
+
144
+ def validate_primary_key(key)
145
+ raise ArgumentError, "composite primary key not supported" if key.is_a?(Array)
146
+ end
147
+
148
148
  def quote_partition_key(key)
149
- key.to_s.split("::").map(&method(:quote_column_name)).join("::")
149
+ if key.is_a?(Proc)
150
+ key.call.to_s # very difficult to determine how to sanitize a complex expression
151
+ else
152
+ quote_column_name(key)
153
+ end
154
+ end
155
+
156
+ def quote_collection(values)
157
+ Array.wrap(values).map(&method(:quote)).join(",")
158
+ end
159
+
160
+ def template_table_name(table_name)
161
+ "#{table_name}_template"
162
+ end
163
+
164
+ def range_constraint_clause(start_range, end_range)
165
+ "FROM (#{quote_collection(start_range)}) TO (#{quote_collection(end_range)})"
166
+ end
167
+
168
+ def list_constraint_clause(values)
169
+ "IN (#{quote_collection(values.try(:to_a) || values)})"
150
170
  end
151
171
 
152
172
  def uuid_function
@@ -160,9 +180,5 @@ module PgParty
160
180
  def supports_partitions?
161
181
  __getobj__.send(:postgresql_version) >= 100000
162
182
  end
163
-
164
- def index_name(table_name, key)
165
- "index_#{table_name}_on_#{key.to_s.split("::").join("_")}"
166
- end
167
183
  end
168
184
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "thread"
2
4
 
3
5
  module PgParty
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgParty
4
+ module Hacks
5
+ module SchemaCache
6
+ def self.included(base)
7
+ return if base.method_defined?(:data_source_exists?)
8
+
9
+ base.send(:alias_method, :data_source_exists?, :table_exists?)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_party/model_decorator"
2
4
 
3
5
  module PgParty
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_party/model_injector"
2
4
 
3
5
  module PgParty
4
6
  module Model
5
7
  module Methods
6
- def range_partition_by(key)
7
- PgParty::ModelInjector.new(self, key).inject_range_methods
8
+ def range_partition_by(key=nil, &blk)
9
+ PgParty::ModelInjector.new(self, key || blk).inject_range_methods
8
10
  end
9
11
 
10
- def list_partition_by(key)
11
- PgParty::ModelInjector.new(self, key).inject_list_methods
12
+ def list_partition_by(key=nil, &blk)
13
+ PgParty::ModelInjector.new(self, key || blk).inject_list_methods
12
14
  end
13
15
 
14
16
  def partitioned?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_party/model_decorator"
2
4
 
3
5
  module PgParty
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_party/model_decorator"
2
4
 
3
5
  module PgParty
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_party/cache"
2
4
 
3
5
  module PgParty
@@ -15,7 +17,7 @@ module PgParty
15
17
  def partition_table_exists?
16
18
  target_table = partitions.first || table_name
17
19
 
18
- connection.schema_cache.send(table_exists_method, target_table)
20
+ connection.schema_cache.data_source_exists?(target_table)
19
21
  end
20
22
 
21
23
  def in_partition(child_table_name)
@@ -44,17 +46,33 @@ module PgParty
44
46
  end
45
47
 
46
48
  def partition_key_eq(value)
47
- where(partition_key_as_arel.eq(value))
49
+ if complex_partition_key
50
+ complex_partition_key_query("(#{partition_key}) = (?)", value)
51
+ else
52
+ where(current_arel_table[partition_key].eq(value))
53
+ end
48
54
  end
49
55
 
50
56
  def range_partition_key_in(start_range, end_range)
51
- node = partition_key_as_arel
57
+ if complex_partition_key
58
+ complex_partition_key_query(
59
+ "(#{partition_key}) >= (?) AND (#{partition_key}) < (?)",
60
+ start_range,
61
+ end_range
62
+ )
63
+ else
64
+ node = current_arel_table[partition_key]
52
65
 
53
- where(node.gteq(start_range).and(node.lt(end_range)))
66
+ where(node.gteq(start_range).and(node.lt(end_range)))
67
+ end
54
68
  end
55
69
 
56
70
  def list_partition_key_in(*values)
57
- where(partition_key_as_arel.in(values.flatten))
71
+ if complex_partition_key
72
+ complex_partition_key_query("(#{partition_key}) IN (?)", values.flatten)
73
+ else
74
+ where(current_arel_table[partition_key].in(values.flatten))
75
+ end
58
76
  end
59
77
 
60
78
  def partitions
@@ -67,6 +85,8 @@ module PgParty
67
85
  WHERE pg_tables.tablename = #{connection.quote(table_name)}
68
86
  SQL
69
87
  end
88
+ rescue
89
+ []
70
90
  end
71
91
 
72
92
  def create_range_partition(start_range:, end_range:, **options)
@@ -74,44 +94,57 @@ module PgParty
74
94
  start_range: start_range,
75
95
  end_range: end_range,
76
96
  primary_key: primary_key,
77
- partition_key: partition_key
78
97
  )
79
98
 
80
- connection.create_range_partition_of(table_name, **modified_options)
99
+ create_partition(:create_range_partition_of, table_name, **modified_options)
81
100
  end
82
101
 
83
102
  def create_list_partition(values:, **options)
84
103
  modified_options = options.merge(
85
104
  values: values,
86
105
  primary_key: primary_key,
87
- partition_key: partition_key
88
106
  )
89
107
 
90
- connection.create_list_partition_of(table_name, **modified_options)
108
+ create_partition(:create_list_partition_of, table_name, **modified_options)
91
109
  end
92
110
 
93
111
  private
94
112
 
113
+ def create_partition(migration_method, table_name, **options)
114
+ transaction { connection.send(migration_method, table_name, **options) }
115
+ end
116
+
95
117
  def cache_key
96
118
  __getobj__.object_id
97
119
  end
98
120
 
99
- def table_exists_method
100
- [:data_source_exists?, :table_exists?].detect do |meth|
101
- connection.schema_cache.respond_to?(meth)
121
+ # https://stackoverflow.com/questions/28685149/activerecord-query-with-aliasd-table-names
122
+ def current_arel_table
123
+ none.arel.source.left.tap do |node|
124
+ if [Arel::Table, Arel::Nodes::TableAlias].exclude?(node.class)
125
+ raise "could not find arel table in current scope"
126
+ end
102
127
  end
103
128
  end
104
129
 
105
- def partition_key_as_arel
106
- arel_column = arel_table[partition_column]
130
+ def current_alias
131
+ arel_node = current_arel_table
107
132
 
108
- if partition_cast
109
- quoted_cast = connection.quote_column_name(partition_cast)
110
-
111
- Arel::Nodes::NamedFunction.new("CAST", [arel_column.as(quoted_cast)])
112
- else
113
- arel_column
133
+ case arel_node
134
+ when Arel::Table
135
+ arel_node.name
136
+ when Arel::Nodes::TableAlias
137
+ arel_node.right
114
138
  end
115
139
  end
140
+
141
+ def complex_partition_key_query(clause, *interpolated_values)
142
+ subquery = base_class
143
+ .unscoped
144
+ .select("*")
145
+ .where(clause, *interpolated_values)
146
+
147
+ from(subquery, current_alias)
148
+ end
116
149
  end
117
150
  end
@@ -1,10 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PgParty
2
4
  class ModelInjector
3
5
  def initialize(model, key)
4
6
  @model = model
5
7
  @key = key
6
-
7
- @column, @cast = key.to_s.split("::")
8
8
  end
9
9
 
10
10
  def inject_range_methods
@@ -33,15 +33,18 @@ module PgParty
33
33
  def create_class_attributes
34
34
  @model.class_attribute(
35
35
  :partition_key,
36
- :partition_column,
37
- :partition_cast,
36
+ :complex_partition_key,
38
37
  instance_accessor: false,
39
38
  instance_predicate: false
40
39
  )
41
40
 
42
- @model.partition_key = @key
43
- @model.partition_column = @column
44
- @model.partition_cast = @cast
41
+ if @key.is_a?(Proc)
42
+ @model.partition_key = @key.call
43
+ @model.complex_partition_key = true
44
+ else
45
+ @model.partition_key = @key
46
+ @model.complex_partition_key = false
47
+ end
45
48
  end
46
49
  end
47
50
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PgParty
2
- VERSION = "0.7.3"
4
+ VERSION = "1.0.0"
3
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_party
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 1.0.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: 2018-02-16 00:00:00.000000000 Z
11
+ date: 2019-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '4.2'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6'
22
+ version: '6.1'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,21 +29,21 @@ dependencies:
29
29
  version: '4.2'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6'
32
+ version: '6.1'
33
33
  - !ruby/object:Gem::Dependency
34
- name: pg
34
+ name: appraisal
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.20'
39
+ version: '2.2'
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0.20'
46
+ version: '2.2'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -59,131 +59,131 @@ dependencies:
59
59
  - !ruby/object:Gem::Version
60
60
  version: '1.15'
61
61
  - !ruby/object:Gem::Dependency
62
- name: rake
62
+ name: byebug
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '12.0'
67
+ version: '10.0'
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '12.0'
74
+ version: '10.0'
75
75
  - !ruby/object:Gem::Dependency
76
- name: rspec-rails
76
+ name: combustion
77
77
  requirement: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '3.6'
81
+ version: '1.1'
82
82
  type: :development
83
83
  prerelease: false
84
84
  version_requirements: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: '3.6'
88
+ version: '1.1'
89
89
  - !ruby/object:Gem::Dependency
90
- name: rspec-its
90
+ name: database_cleaner
91
91
  requirement: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: '1.2'
95
+ version: '1.6'
96
96
  type: :development
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '1.2'
102
+ version: '1.6'
103
103
  - !ruby/object:Gem::Dependency
104
- name: pry-byebug
104
+ name: nokogiri
105
105
  requirement: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '3.4'
109
+ version: 1.9.1
110
110
  type: :development
111
111
  prerelease: false
112
112
  version_requirements: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '3.4'
116
+ version: 1.9.1
117
117
  - !ruby/object:Gem::Dependency
118
- name: rspec_junit_formatter
118
+ name: pry-byebug
119
119
  requirement: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: '0.3'
123
+ version: '3.4'
124
124
  type: :development
125
125
  prerelease: false
126
126
  version_requirements: !ruby/object:Gem::Requirement
127
127
  requirements:
128
128
  - - "~>"
129
129
  - !ruby/object:Gem::Version
130
- version: '0.3'
130
+ version: '3.4'
131
131
  - !ruby/object:Gem::Dependency
132
- name: appraisal
132
+ name: rake
133
133
  requirement: !ruby/object:Gem::Requirement
134
134
  requirements:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
- version: '2.2'
137
+ version: '12.0'
138
138
  type: :development
139
139
  prerelease: false
140
140
  version_requirements: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - "~>"
143
143
  - !ruby/object:Gem::Version
144
- version: '2.2'
144
+ version: '12.0'
145
145
  - !ruby/object:Gem::Dependency
146
- name: combustion
146
+ name: rspec-its
147
147
  requirement: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - "~>"
150
150
  - !ruby/object:Gem::Version
151
- version: '0.7'
151
+ version: '1.2'
152
152
  type: :development
153
153
  prerelease: false
154
154
  version_requirements: !ruby/object:Gem::Requirement
155
155
  requirements:
156
156
  - - "~>"
157
157
  - !ruby/object:Gem::Version
158
- version: '0.7'
158
+ version: '1.2'
159
159
  - !ruby/object:Gem::Dependency
160
- name: database_cleaner
160
+ name: rspec-rails
161
161
  requirement: !ruby/object:Gem::Requirement
162
162
  requirements:
163
163
  - - "~>"
164
164
  - !ruby/object:Gem::Version
165
- version: '1.6'
165
+ version: '3.6'
166
166
  type: :development
167
167
  prerelease: false
168
168
  version_requirements: !ruby/object:Gem::Requirement
169
169
  requirements:
170
170
  - - "~>"
171
171
  - !ruby/object:Gem::Version
172
- version: '1.6'
172
+ version: '3.6'
173
173
  - !ruby/object:Gem::Dependency
174
- name: timecop
174
+ name: rspec_junit_formatter
175
175
  requirement: !ruby/object:Gem::Requirement
176
176
  requirements:
177
177
  - - "~>"
178
178
  - !ruby/object:Gem::Version
179
- version: '0.9'
179
+ version: '0.3'
180
180
  type: :development
181
181
  prerelease: false
182
182
  version_requirements: !ruby/object:Gem::Requirement
183
183
  requirements:
184
184
  - - "~>"
185
185
  - !ruby/object:Gem::Version
186
- version: '0.9'
186
+ version: '0.3'
187
187
  - !ruby/object:Gem::Dependency
188
188
  name: simplecov
189
189
  requirement: !ruby/object:Gem::Requirement
@@ -198,6 +198,20 @@ dependencies:
198
198
  - - "~>"
199
199
  - !ruby/object:Gem::Version
200
200
  version: '0.15'
201
+ - !ruby/object:Gem::Dependency
202
+ name: timecop
203
+ requirement: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - "~>"
206
+ - !ruby/object:Gem::Version
207
+ version: '0.9'
208
+ type: :development
209
+ prerelease: false
210
+ version_requirements: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - "~>"
213
+ - !ruby/object:Gem::Version
214
+ version: '0.9'
201
215
  description: Migrations and model helpers for creating and managing PostgreSQL 10
202
216
  partitions
203
217
  email:
@@ -213,6 +227,7 @@ files:
213
227
  - lib/pg_party/adapter/postgresql_methods.rb
214
228
  - lib/pg_party/adapter_decorator.rb
215
229
  - lib/pg_party/cache.rb
230
+ - lib/pg_party/hacks/schema_cache.rb
216
231
  - lib/pg_party/model/list_methods.rb
217
232
  - lib/pg_party/model/methods.rb
218
233
  - lib/pg_party/model/range_methods.rb
@@ -239,8 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
239
254
  - !ruby/object:Gem::Version
240
255
  version: 1.8.11
241
256
  requirements: []
242
- rubyforge_project:
243
- rubygems_version: 2.6.11
257
+ rubygems_version: 3.0.3
244
258
  signing_key:
245
259
  specification_version: 4
246
260
  summary: ActiveRecord PostgreSQL Partitioning