pg_party 0.2.1 → 0.3.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
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