pg_party 0.7.3 → 1.0.0

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