pg_party 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ceb308a34accd65263dc633ad43e2effabf750e8
4
- data.tar.gz: 88fbc43ccc1e6228c3dbfa4b00fcb45bebcc4d12
3
+ metadata.gz: 820e7b5d4a74330f2871d8e8d5e278974001dd1f
4
+ data.tar.gz: 80a59da979fa45c339aa3abe0201c154809a4eb4
5
5
  SHA512:
6
- metadata.gz: 27cec43797b297e82dc378d465cf1586fe9c74d91538c2ea9f9d5d93efb8a01f4c5ab38e56edd5ef4dabed25265f4d1f5d94fe3a4c0717a1ae1696a0b06d4773
7
- data.tar.gz: 77ef5c1668d4cafe1101f859445ed49faa0e3053e30b62413a6c3c0db3c2dc8c95ddafb9ca92c30d6f6f1cae1956bb0822c243616edd24db98ef15da269392ec
6
+ metadata.gz: 4e14b35c529df6ea91d093e444962be64d65a112ab706b67bdce500904fb74eb88c31fd944aa06c9386f8cd365286841723ea71ecb883086604912ce6484d6db
7
+ data.tar.gz: 622f8c66614c01ef3a2a649f6e0c9123b07a7d73d8adbd74ef2428215ff70d78a70e554f5bd4b98e246beb2327d174b5badce0131d6127c19b59234f9bf10022
data/README.md CHANGED
@@ -2,11 +2,20 @@
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
+ [![Code Climate](https://codeclimate.com/github/rkrage/pg_party/badges/gpa.svg)][climate]
5
6
 
6
7
  [rubygems]: https://rubygems.org/gems/pg_party
7
8
  [circle]: https://circleci.com/gh/rkrage/pg_party
9
+ [climate]: https://codeclimate.com/github/rkrage/pg_party
8
10
 
9
- Active Record migrations and model helpers for creating and managing PostgreSQL 10 partitions!
11
+ [Active Record](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)!
12
+
13
+ Features:
14
+ - migration methods for partition specific database operations
15
+ - model methods for querying partitioned data
16
+ - model methods for creating adhoc partitions
17
+
18
+ Note: PostgreSQL 10 is currently in beta. This gem should not be used in production environments (yet).
10
19
 
11
20
  ## Installation
12
21
 
@@ -26,11 +35,176 @@ Or install it yourself as:
26
35
 
27
36
  ## Usage
28
37
 
29
- TODO: Write usage instructions here
38
+ Full API documentation is in progress.
39
+
40
+ In the meantime, take a look at the [Combustion](https://github.com/pat/combustion) schema definition and integration specs:
41
+ - https://github.com/rkrage/pg_party/blob/master/spec/internal/db/schema.rb
42
+ - https://github.com/rkrage/pg_party/tree/master/spec/integration
43
+
44
+ ### Migration Examples
45
+
46
+ Create range partition on `created_at::date` with two child partitions:
47
+
48
+ ```ruby
49
+ class CreateSomeRangeRecord < ActiveRecord::Migration[5.1]
50
+ def up
51
+ current_date = Date.current
52
+
53
+ create_range_partition :some_range_records, partition_key: "created_at::date" do |t|
54
+ t.text :some_value
55
+ t.timestamps
56
+ end
57
+
58
+ create_range_partition_of \
59
+ :some_range_records,
60
+ partition_key: "created_at::date",
61
+ start_range: current_date,
62
+ end_range: current_date + 1.day
63
+
64
+ create_range_partition_of \
65
+ :some_range_records,
66
+ partition_key: "created_at::date",
67
+ start_range: current_date + 1.day,
68
+ end_range: current_date + 2.days
69
+ end
70
+ end
71
+ ```
72
+
73
+ Create list partition on `id` with two child partitions:
74
+
75
+ ```ruby
76
+ class CreateSomeListRecord < ActiveRecord::Migration[5.1]
77
+ def up
78
+ create_list_partition :some_list_records, partition_key: :id do |t|
79
+ t.text :some_value
80
+ t.timestamps
81
+ end
82
+
83
+ create_list_partition_of \
84
+ :some_list_records,
85
+ partition_key: :id,
86
+ values: (1..100).to_a
87
+
88
+ create_list_partition_of \
89
+ :some_list_records,
90
+ partition_key: :id,
91
+ values: (100..200).to_a
92
+ end
93
+ end
94
+ ```
95
+
96
+ Attach an existing table to a range partition:
97
+
98
+ ```ruby
99
+ class AttachRangePartition < ActiveRecord::Migration[5.1]
100
+ def up
101
+ attach_range_partition("parent_table", "child_table")
102
+ end
103
+ end
104
+ ```
105
+
106
+ Attach an existing table to a list partition:
107
+
108
+ ```ruby
109
+ class AttachListPartition < ActiveRecord::Migration[5.1]
110
+ def up
111
+ attach_list_partition("parent_table", "child_table")
112
+ end
113
+ end
114
+ ```
115
+
116
+ Detach a child table from any partition:
117
+
118
+ ```ruby
119
+ class DetachPartition < ActiveRecord::Migration[5.1]
120
+ def up
121
+ detach_partition("parent_table", "child_table")
122
+ end
123
+ end
124
+ ```
125
+
126
+ ### Model Examples
127
+
128
+ Define model that is backed by a range partition:
129
+
130
+ ```ruby
131
+ class SomeRangeRecord < ApplicationRecord
132
+ range_partition_by "created_at::date"
133
+ end
134
+ ```
135
+
136
+ Define model that is backed by a list partition:
137
+
138
+ ```ruby
139
+ class SomeListRecord < ApplicationRecord
140
+ list_partition_by :id
141
+ end
142
+ ```
143
+
144
+ Create child partition from range partition model:
145
+
146
+ ```ruby
147
+ current_date = Date.current
148
+
149
+ SomeRangeRecord.create_partition(start_range: current_date + 1.day, end_range: current_date + 2.days)
150
+ ```
151
+
152
+ Create child partition from list partition model:
153
+
154
+ ```ruby
155
+ SomeListRecord.create_partition(values: (200..300).to_a)
156
+ ```
157
+
158
+ Query for records within partition range:
159
+
160
+ ```ruby
161
+ SomeRangeRecord.partition_key_in("2017-01-01".to_date, "2017-02-01".to_date)
162
+ ```
163
+
164
+ Query for records in partition list:
165
+
166
+ ```ruby
167
+ SomeListRecord.partition_key_in(1, 2, 3, 4)
168
+ ```
169
+
170
+ Query for records matching partition key:
171
+
172
+ ```ruby
173
+ SomeRangeRecord.partition_key_eq(Date.current)
174
+
175
+ SomeListRecord.partition_key_eq(100)
176
+ ```
177
+
178
+ Query for records by partition name:
179
+
180
+ ```ruby
181
+ # returns a collection of anonymous ActiveRecord::Base subclassed instances
182
+ SomeRangeRecord.in_partition("some_range_records_partition_name")
183
+
184
+ # returns a collection of anonymous ActiveRecord::Base subclassed instances
185
+ SomeListRecord.in_partition("some_list_records_partition_name")
186
+ ```
30
187
 
31
188
  ## Development
32
189
 
33
- TODO: Write development instructions here
190
+ The development / test environment relies heavily on [Docker](https://docs.docker.com).
191
+
192
+ Start the containers in the background:
193
+
194
+ $ docker-compose up -d
195
+
196
+ Install dependencies:
197
+
198
+ $ bin/de bundle
199
+ $ bin/de appraisal
200
+
201
+ Run the tests:
202
+
203
+ $ bin/de rake
204
+
205
+ Open a Pry console to play around with the sample Rails app:
206
+
207
+ $ bin/de console
34
208
 
35
209
  ## Contributing
36
210
 
@@ -1,31 +1,31 @@
1
1
  module PgParty
2
- module ConnectionAdapters
3
- module AbstractAdapter
4
- def create_range_partition(*args)
2
+ module Adapter
3
+ module AbstractMethods
4
+ def create_range_partition(*)
5
5
  raise NotImplementedError, "#create_range_partition is not implemented"
6
6
  end
7
7
 
8
- def create_list_partition(*args)
8
+ def create_list_partition(*)
9
9
  raise NotImplementedError, "#create_list_partition is not implemented"
10
10
  end
11
11
 
12
- def create_range_partition_of(*args)
12
+ def create_range_partition_of(*)
13
13
  raise NotImplementedError, "#create_range_partition_of is not implemented"
14
14
  end
15
15
 
16
- def create_list_partition_of(*args)
16
+ def create_list_partition_of(*)
17
17
  raise NotImplementedError, "#create_list_partition_of is not implemented"
18
18
  end
19
19
 
20
- def attach_range_partition(*args)
20
+ def attach_range_partition(*)
21
21
  raise NotImplementedError, "#attach_range_partition is not implemented"
22
22
  end
23
23
 
24
- def attach_list_partition(*args)
24
+ def attach_list_partition(*)
25
25
  raise NotImplementedError, "#attach_list_partition is not implemented"
26
26
  end
27
27
 
28
- def detach_partition(*args)
28
+ def detach_partition(*)
29
29
  raise NotImplementedError, "#detach_partition is not implemented"
30
30
  end
31
31
  end
@@ -0,0 +1,35 @@
1
+ require "pg_party/adapter_decorator"
2
+
3
+ module PgParty
4
+ module Adapter
5
+ module PostgreSQLMethods
6
+ def create_range_partition(*args, &blk)
7
+ PgParty::AdapterDecorator.new(self).create_range_partition(*args, &blk)
8
+ end
9
+
10
+ def create_list_partition(*args, &blk)
11
+ PgParty::AdapterDecorator.new(self).create_list_partition(*args, &blk)
12
+ end
13
+
14
+ def create_range_partition_of(*args)
15
+ PgParty::AdapterDecorator.new(self).create_range_partition_of(*args)
16
+ end
17
+
18
+ def create_list_partition_of(*args)
19
+ PgParty::AdapterDecorator.new(self).create_list_partition_of(*args)
20
+ end
21
+
22
+ def attach_range_partition(*args)
23
+ PgParty::AdapterDecorator.new(self).attach_range_partition(*args)
24
+ end
25
+
26
+ def attach_list_partition(*args)
27
+ PgParty::AdapterDecorator.new(self).attach_list_partition(*args)
28
+ end
29
+
30
+ def detach_partition(*args)
31
+ PgParty::AdapterDecorator.new(self).detach_partition(*args)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,133 @@
1
+ require "digest"
2
+
3
+ module PgParty
4
+ class AdapterDecorator < SimpleDelegator
5
+ def initialize(adapter)
6
+ super(adapter)
7
+
8
+ raise "Partitioning only supported in PostgreSQL >= 10.0" unless supports_partitions?
9
+ end
10
+
11
+ def create_range_partition(table_name, partition_key:, **options, &blk)
12
+ create_partition(table_name, :range, partition_key, **options, &blk)
13
+ end
14
+
15
+ def create_list_partition(table_name, partition_key:, **options, &blk)
16
+ create_partition(table_name, :list, partition_key, **options, &blk)
17
+ end
18
+
19
+ def create_range_partition_of(table_name, start_range:, end_range:, **options)
20
+ if options[:name]
21
+ child_table_name = options[:name]
22
+ else
23
+ child_table_name = hashed_table_name(table_name, "#{start_range}#{end_range}")
24
+ end
25
+
26
+ constraint_clause = "FROM (#{quote(start_range)}) TO (#{quote(end_range)})"
27
+
28
+ create_partition_of(table_name, child_table_name, constraint_clause, **options)
29
+ end
30
+
31
+ def create_list_partition_of(table_name, values:, **options)
32
+ if options[:name]
33
+ child_table_name = options[:name]
34
+ else
35
+ child_table_name = hashed_table_name(table_name, values.to_s)
36
+ end
37
+
38
+ constraint_clause = "IN (#{Array.wrap(values).map(&method(:quote)).join(",")})"
39
+
40
+ create_partition_of(table_name, child_table_name, constraint_clause, **options)
41
+ end
42
+
43
+ def attach_range_partition(parent_table_name, child_table_name, start_range:, end_range:)
44
+ execute(<<-SQL)
45
+ ALTER TABLE #{quote_table_name(parent_table_name)}
46
+ ATTACH PARTITION #{quote_table_name(child_table_name)}
47
+ FOR VALUES FROM (#{quote(start_range)}) TO (#{quote(end_range)})
48
+ SQL
49
+ end
50
+
51
+ def attach_list_partition(parent_table_name, child_table_name, values:)
52
+ execute(<<-SQL)
53
+ ALTER TABLE #{quote_table_name(parent_table_name)}
54
+ ATTACH PARTITION #{quote_table_name(child_table_name)}
55
+ FOR VALUES IN (#{Array.wrap(values).map(&method(:quote)).join(",")})
56
+ SQL
57
+ end
58
+
59
+ def detach_partition(parent_table_name, child_table_name)
60
+ execute(<<-SQL)
61
+ ALTER TABLE #{quote_table_name(parent_table_name)}
62
+ DETACH PARTITION #{quote_table_name(child_table_name)}
63
+ SQL
64
+ end
65
+
66
+ private
67
+
68
+ def create_partition(table_name, type, partition_key, **options)
69
+ modified_options = options.except(:id, :primary_key)
70
+ id = options.fetch(:id, :bigserial)
71
+ primary_key = options.fetch(:primary_key, :id)
72
+
73
+ raise ArgumentError, "composite primary key not supported" if primary_key.is_a?(Array)
74
+
75
+ modified_options[:id] = false
76
+ modified_options[:options] = "PARTITION BY #{type.to_s.upcase} ((#{quote_partition_key(partition_key)}))"
77
+
78
+ create_table(table_name, modified_options) do |td|
79
+ if id == :uuid
80
+ td.send(id, primary_key, null: false, default: uuid_function)
81
+ elsif id
82
+ td.send(id, primary_key, null: false)
83
+ end
84
+
85
+ yield(td) if block_given?
86
+ end
87
+ end
88
+
89
+ def create_partition_of(table_name, child_table_name, constraint_clause, **options)
90
+ primary_key = options.fetch(:primary_key, :id)
91
+ index = options.fetch(:index, true)
92
+ partition_key = options[:partition_key]
93
+
94
+ raise ArgumentError, "composite primary key not supported" if primary_key.is_a?(Array)
95
+
96
+ partition_clause = <<-SQL
97
+ PARTITION OF #{quote_table_name(table_name)}
98
+ FOR VALUES #{constraint_clause}
99
+ SQL
100
+
101
+ create_table(child_table_name, id: false, options: partition_clause)
102
+
103
+ if primary_key
104
+ execute(<<-SQL)
105
+ ALTER TABLE #{quote_table_name(child_table_name)}
106
+ ADD PRIMARY KEY (#{quote_column_name(primary_key)})
107
+ SQL
108
+ end
109
+
110
+ if index && partition_key && primary_key != partition_key
111
+ add_index(child_table_name, "((#{quote_partition_key(partition_key)}))")
112
+ end
113
+
114
+ child_table_name
115
+ end
116
+
117
+ def quote_partition_key(key)
118
+ key.to_s.split("::").map(&method(:quote_column_name)).join("::")
119
+ end
120
+
121
+ def uuid_function
122
+ try(:supports_pgcrypto_uuid?) ? "gen_random_uuid()" : "uuid_generate_v4()"
123
+ end
124
+
125
+ def hashed_table_name(table_name, key)
126
+ "#{table_name}_#{Digest::MD5.hexdigest(key)[0..6]}"
127
+ end
128
+
129
+ def supports_partitions?
130
+ postgresql_version >= 100000
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,23 @@
1
+ require "pg_party/model_decorator"
2
+
3
+ module PgParty
4
+ module Model
5
+ module ListMethods
6
+ def create_partition(*args)
7
+ PgParty::ModelDecorator.new(self).create_list_partition(*args)
8
+ end
9
+
10
+ def in_partition(*args)
11
+ PgParty::ModelDecorator.new(self).in_partition(*args)
12
+ end
13
+
14
+ def partition_key_in(*args)
15
+ PgParty::ModelDecorator.new(self).list_partition_key_in(*args)
16
+ end
17
+
18
+ def partition_key_eq(*args)
19
+ PgParty::ModelDecorator.new(self).partition_key_eq(*args)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ require "pg_party/model_injector"
2
+
3
+ module PgParty
4
+ module Model
5
+ module Methods
6
+ def range_partition_by(key)
7
+ PgParty::ModelInjector.new(self, key).inject_range_methods
8
+ end
9
+
10
+ def list_partition_by(key)
11
+ PgParty::ModelInjector.new(self, key).inject_list_methods
12
+ end
13
+
14
+ def partitioned?
15
+ try(:partition_key).present?
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ require "pg_party/model_decorator"
2
+
3
+ module PgParty
4
+ module Model
5
+ module RangeMethods
6
+ def create_partition(*args)
7
+ PgParty::ModelDecorator.new(self).create_range_partition(*args)
8
+ end
9
+
10
+ def in_partition(*args)
11
+ PgParty::ModelDecorator.new(self).in_partition(*args)
12
+ end
13
+
14
+ def partition_key_in(*args)
15
+ PgParty::ModelDecorator.new(self).range_partition_key_in(*args)
16
+ end
17
+
18
+ def partition_key_eq(*args)
19
+ PgParty::ModelDecorator.new(self).partition_key_eq(*args)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,62 @@
1
+ module PgParty
2
+ class ModelDecorator < SimpleDelegator
3
+ def in_partition(table_name)
4
+ child_class(table_name).all
5
+ end
6
+
7
+ def partition_key_eq(value)
8
+ where(partition_key_as_arel.eq(value))
9
+ end
10
+
11
+ def range_partition_key_in(start_range, end_range)
12
+ node = partition_key_as_arel
13
+
14
+ where(node.gteq(start_range).and(node.lt(end_range)))
15
+ end
16
+
17
+ def list_partition_key_in(*values)
18
+ where(partition_key_as_arel.in(values.flatten))
19
+ end
20
+
21
+ def create_range_partition(start_range:, end_range:, **options)
22
+ modified_options = options.merge(
23
+ start_range: start_range,
24
+ end_range: end_range,
25
+ primary_key: primary_key,
26
+ partition_key: partition_key
27
+ )
28
+
29
+ connection.create_range_partition_of(table_name, **modified_options)
30
+ end
31
+
32
+ def create_list_partition(values:, **options)
33
+ modified_options = options.merge(
34
+ values: values,
35
+ primary_key: primary_key,
36
+ partition_key: partition_key
37
+ )
38
+
39
+ connection.create_list_partition_of(table_name, **modified_options)
40
+ end
41
+
42
+ private
43
+
44
+ def child_class(table_name)
45
+ Class.new(__getobj__) do
46
+ self.table_name = table_name
47
+ end
48
+ end
49
+
50
+ def partition_key_as_arel
51
+ arel_column = arel_table[partition_column]
52
+
53
+ if partition_cast
54
+ quoted_cast = connection.quote_column_name(partition_cast)
55
+
56
+ Arel::Nodes::NamedFunction.new("CAST", [arel_column.as(quoted_cast)])
57
+ else
58
+ arel_column
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ module PgParty
2
+ class ModelInjector
3
+ def initialize(model, key)
4
+ @model = model
5
+ @key = key
6
+
7
+ @column, @cast = key.to_s.split("::")
8
+ end
9
+
10
+ def inject_range_methods
11
+ create_class_attributes
12
+
13
+ require "pg_party/model/range_methods"
14
+ @model.extend(PgParty::Model::RangeMethods)
15
+ end
16
+
17
+ def inject_list_methods
18
+ create_class_attributes
19
+
20
+ require "pg_party/model/list_methods"
21
+ @model.extend(PgParty::Model::ListMethods)
22
+ end
23
+
24
+ private
25
+
26
+ def create_class_attributes
27
+ @model.class_attribute(
28
+ :partition_key,
29
+ :partition_column,
30
+ :partition_cast,
31
+ instance_accessor: false
32
+ )
33
+
34
+ @model.partition_key = @key
35
+ @model.partition_column = @column
36
+ @model.partition_cast = @cast
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,3 @@
1
1
  module PgParty
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/pg_party.rb CHANGED
@@ -2,15 +2,20 @@ require "pg_party/version"
2
2
  require "active_support"
3
3
 
4
4
  ActiveSupport.on_load(:active_record) do
5
- require "pg_party/connection_handling"
6
- require "pg_party/model_methods"
5
+ require "pg_party/model/methods"
7
6
 
8
- extend PgParty::ConnectionHandling
9
- extend PgParty::ModelMethods
7
+ extend PgParty::Model::Methods
10
8
 
11
- require "pg_party/connection_adapters/abstract_adapter"
9
+ require "pg_party/adapter/abstract_methods"
12
10
 
13
11
  ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
14
- include PgParty::ConnectionAdapters::AbstractAdapter
12
+ include PgParty::Adapter::AbstractMethods
13
+ end
14
+
15
+ require "pg_party/adapter/postgresql_methods"
16
+ require "active_record/connection_adapters/postgresql_adapter"
17
+
18
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
19
+ include PgParty::Adapter::PostgreSQLMethods
15
20
  end
16
21
  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.2.1
4
+ version: 0.3.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: 2017-09-16 00:00:00.000000000 Z
11
+ date: 2017-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -189,13 +189,14 @@ files:
189
189
  - LICENSE.txt
190
190
  - README.md
191
191
  - lib/pg_party.rb
192
- - lib/pg_party/connection_adapters/abstract_adapter.rb
193
- - lib/pg_party/connection_adapters/postgresql_adapter.rb
194
- - lib/pg_party/connection_handling.rb
195
- - lib/pg_party/injected_list_model_methods.rb
196
- - lib/pg_party/injected_model_methods.rb
197
- - lib/pg_party/injected_range_model_methods.rb
198
- - lib/pg_party/model_methods.rb
192
+ - lib/pg_party/adapter/abstract_methods.rb
193
+ - lib/pg_party/adapter/postgresql_methods.rb
194
+ - lib/pg_party/adapter_decorator.rb
195
+ - lib/pg_party/model/list_methods.rb
196
+ - lib/pg_party/model/methods.rb
197
+ - lib/pg_party/model/range_methods.rb
198
+ - lib/pg_party/model_decorator.rb
199
+ - lib/pg_party/model_injector.rb
199
200
  - lib/pg_party/version.rb
200
201
  homepage: https://github.com/rkrage/pg_party
201
202
  licenses:
@@ -217,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
218
  version: 1.8.11
218
219
  requirements: []
219
220
  rubyforge_project:
220
- rubygems_version: 2.4.5
221
+ rubygems_version: 2.6.11
221
222
  signing_key:
222
223
  specification_version: 4
223
224
  summary: ActiveRecord PostgreSQL Partitioning
@@ -1,125 +0,0 @@
1
- require "digest"
2
-
3
- module PgParty
4
- module ConnectionAdapters
5
- module PostgreSQLAdapter
6
- def create_range_partition(table_name, partition_key:, **options, &blk)
7
- create_partition(table_name, :range, partition_key, **options, &blk)
8
- end
9
-
10
- def create_list_partition(table_name, partition_key:, **options, &blk)
11
- create_partition(table_name, :list, partition_key, **options, &blk)
12
- end
13
-
14
- def create_range_partition_of(table_name, start_range:, end_range:, **options)
15
- if options[:name]
16
- child_table_name = options[:name]
17
- else
18
- child_table_name = hashed_table_name(table_name, "#{start_range}#{end_range}")
19
- end
20
-
21
- constraint_clause = "FROM (#{quote(start_range)}) TO (#{quote(end_range)})"
22
-
23
- create_partition_of(table_name, child_table_name, constraint_clause, **options)
24
- end
25
-
26
- def create_list_partition_of(table_name, values:, **options)
27
- if options[:name]
28
- child_table_name = options[:name]
29
- else
30
- child_table_name = hashed_table_name(table_name, values.to_s)
31
- end
32
-
33
- constraint_clause = "IN (#{Array.wrap(values).map(&method(:quote)).join(",")})"
34
-
35
- create_partition_of(table_name, child_table_name, constraint_clause, **options)
36
- end
37
-
38
- def attach_range_partition(parent_table_name, child_table_name, start_range:, end_range:)
39
- execute(<<-SQL)
40
- ALTER TABLE #{quote_table_name(parent_table_name)}
41
- ATTACH PARTITION #{quote_table_name(child_table_name)}
42
- FOR VALUES FROM (#{quote(start_range)}) TO (#{quote(end_range)})
43
- SQL
44
- end
45
-
46
- def attach_list_partition(parent_table_name, child_table_name, values:)
47
- execute(<<-SQL)
48
- ALTER TABLE #{quote_table_name(parent_table_name)}
49
- ATTACH PARTITION #{quote_table_name(child_table_name)}
50
- FOR VALUES IN (#{Array.wrap(values).map(&method(:quote)).join(",")})
51
- SQL
52
- end
53
-
54
- def detach_partition(parent_table_name, child_table_name)
55
- execute(<<-SQL)
56
- ALTER TABLE #{quote_table_name(parent_table_name)}
57
- DETACH PARTITION #{quote_table_name(child_table_name)}
58
- SQL
59
- end
60
-
61
- private
62
-
63
- def create_partition(table_name, type, partition_key, **options)
64
- modified_options = options.except(:id, :primary_key)
65
- id = options.fetch(:id, :bigserial)
66
- primary_key = options.fetch(:primary_key, :id)
67
-
68
- raise ArgumentError, "composite primary key not supported" if primary_key.is_a?(Array)
69
-
70
- modified_options[:id] = false
71
- modified_options[:options] = "PARTITION BY #{type.to_s.upcase} ((#{quote_partition_key(partition_key)}))"
72
-
73
- create_table(table_name, modified_options) do |td|
74
- if id == :uuid
75
- td.send(id, primary_key, null: false, default: uuid_function)
76
- elsif id
77
- td.send(id, primary_key, null: false)
78
- end
79
-
80
- yield td if block_given?
81
- end
82
- end
83
-
84
- def create_partition_of(table_name, child_table_name, constraint_clause, **options)
85
- primary_key = options.fetch(:primary_key, :id)
86
- index = options.fetch(:index, true)
87
- partition_key = options[:partition_key]
88
-
89
- raise ArgumentError, "composite primary key not supported" if primary_key.is_a?(Array)
90
-
91
- partition_clause = <<-SQL
92
- PARTITION OF #{quote_table_name(table_name)}
93
- FOR VALUES #{constraint_clause}
94
- SQL
95
-
96
- create_table(child_table_name, id: false, options: partition_clause)
97
-
98
- if primary_key
99
- execute(<<-SQL)
100
- ALTER TABLE #{quote_table_name(child_table_name)}
101
- ADD PRIMARY KEY (#{quote_column_name(primary_key)})
102
- SQL
103
- end
104
-
105
- if index && partition_key && primary_key != partition_key
106
- add_index(child_table_name, "((#{quote_partition_key(partition_key)}))")
107
- end
108
-
109
- child_table_name
110
- end
111
-
112
- def quote_partition_key(key)
113
- key.to_s.split("::").map(&method(:quote_column_name)).join("::")
114
- end
115
-
116
- def uuid_function
117
- try(:supports_pgcrypto_uuid?) ? "gen_random_uuid()" : "uuid_generate_v4()"
118
- end
119
-
120
- def hashed_table_name(table_name, key)
121
- "#{table_name}_#{Digest::MD5.hexdigest(key)[0..6]}"
122
- end
123
- end
124
- end
125
- end
@@ -1,15 +0,0 @@
1
- module PgParty
2
- module ConnectionHandling
3
- def establish_connection(*args)
4
- super.tap do
5
- if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
6
- require "pg_party/connection_adapters/postgresql_adapter"
7
-
8
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
9
- include PgParty::ConnectionAdapters::PostgreSQLAdapter
10
- end
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,21 +0,0 @@
1
- require "pg_party/injected_model_methods"
2
-
3
- module PgParty
4
- module InjectedListModelMethods
5
- include InjectedModelMethods
6
-
7
- def create_partition(values:, **options)
8
- modified_options = options.merge(
9
- values: values,
10
- primary_key: primary_key,
11
- partition_key: partition_key
12
- )
13
-
14
- connection.create_list_partition_of(table_name, **modified_options)
15
- end
16
-
17
- def partition_key_in(*values)
18
- where(partition_key_as_arel.in(values.flatten))
19
- end
20
- end
21
- end
@@ -1,23 +0,0 @@
1
- module PgParty
2
- module InjectedModelMethods
3
- attr_reader :partition_key, :partition_column, :partition_cast
4
-
5
- def partition_key_matching(value)
6
- where(partition_key_as_arel.eq(value))
7
- end
8
-
9
- private
10
-
11
- def partition_key_as_arel
12
- arel_column = arel_table[partition_column]
13
-
14
- if partition_cast
15
- quoted_cast = connection.quote_column_name(partition_cast)
16
-
17
- Arel::Nodes::NamedFunction.new("CAST", [arel_column.as(quoted_cast)])
18
- else
19
- arel_column
20
- end
21
- end
22
- end
23
- end
@@ -1,24 +0,0 @@
1
- require "pg_party/injected_model_methods"
2
-
3
- module PgParty
4
- module InjectedRangeModelMethods
5
- include InjectedModelMethods
6
-
7
- def create_partition(start_range:, end_range:, **options)
8
- modified_options = options.merge(
9
- start_range: start_range,
10
- end_range: end_range,
11
- primary_key: primary_key,
12
- partition_key: partition_key
13
- )
14
-
15
- connection.create_range_partition_of(table_name, **modified_options)
16
- end
17
-
18
- def partition_key_in(start_range, end_range)
19
- node = partition_key_as_arel
20
-
21
- where(node.gteq(start_range).and(node.lt(end_range)))
22
- end
23
- end
24
- end
@@ -1,23 +0,0 @@
1
- module PgParty
2
- module ModelMethods
3
- def range_partition_by(key)
4
- @partition_key = key
5
- @partition_column, @partition_cast = key.to_s.split("::")
6
-
7
- require "pg_party/injected_range_model_methods"
8
- extend InjectedRangeModelMethods
9
- end
10
-
11
- def list_partition_by(key)
12
- @partition_key = key
13
- @partition_column, @partition_cast = key.to_s.split("::")
14
-
15
- require "pg_party/injected_list_model_methods"
16
- extend InjectedListModelMethods
17
- end
18
-
19
- def partitioned?
20
- @partition_key.present?
21
- end
22
- end
23
- end