partitionable 0.2.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +68 -0
- data/Rakefile +34 -0
- data/lib/partitionable.rb +5 -0
- data/lib/partitionable/acts_as_partitionable.rb +141 -0
- data/lib/partitionable/version.rb +3 -0
- data/lib/tasks/partitionable_tasks.rake +4 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 45ffa6ac52a4391ae901589f8ee0b341b85760d1
|
4
|
+
data.tar.gz: b6d738345cb33c37c6d425ba8042bf24a76b91d0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 24ad2fe29382796d7052deee0406373bd988b5f5a5e77fd6855beb95859ef746815130e4bd01751846613b21f5c2908c5207154ed90067352d68945cb6dd33b1
|
7
|
+
data.tar.gz: 31c56321fc983331220e08fb8f00f9774db0606c453bc1c3a2bcd88694e492b0afd2fe1ef433804f1056671baab844eb7163b971fe142f44153b6eeaad7d734e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 Pablo Acuña
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
[](https://travis-ci.org/pacuna/partitionable)
|
2
|
+
|
3
|
+
# Partitionable
|
4
|
+
|
5
|
+
This gem adds support for using the PostgreSQL partitioning mechanism
|
6
|
+
describe [here](https://www.postgresql.org/docs/9.1/static/ddl-partitioning.html).
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
Partitionable assumes the model you want to partition has a date attribute which will be
|
11
|
+
used for checking the partitions constraints and triggers.
|
12
|
+
|
13
|
+
### Example
|
14
|
+
|
15
|
+
Let's say you have a model named `ArticleStat` and its respective table named `article_stats`.
|
16
|
+
Suppose this model also has a `logdate` attribute of type date. We want to partition
|
17
|
+
the data by year and month using this attribute.
|
18
|
+
|
19
|
+
Add the `acts_as_partitionable` module and method to the model. The `index_fields` and `logdate_attr` are mandatory
|
20
|
+
options. The first one adds an index for those attributes when creating the partitions and the latter
|
21
|
+
is the date attribute used for routing the records to the correct partitions:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class ArticleStat < ApplicationRecord
|
25
|
+
include Partitionable::ActsAsPartitionable
|
26
|
+
|
27
|
+
acts_as_partitionable index_fields: ['id', 'site'], logdate_attr: 'logdate'
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
And that's it. Now every time you create a new record, the gem will create
|
32
|
+
the correspondent partition table if doesn't exists. It also will update the trigger
|
33
|
+
function so every other new record that should go into this partition gets correctly
|
34
|
+
routed.
|
35
|
+
|
36
|
+
## Installation
|
37
|
+
Add this line to your application's Gemfile:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
gem "partitionable", '~> 0.1.0'
|
41
|
+
```
|
42
|
+
|
43
|
+
And then execute:
|
44
|
+
```bash
|
45
|
+
$ bundle
|
46
|
+
```
|
47
|
+
|
48
|
+
## Tests
|
49
|
+
|
50
|
+
First, create the database for the dummy embedded application:
|
51
|
+
|
52
|
+
```bash
|
53
|
+
cd test/dummy
|
54
|
+
bin/rails db:setup
|
55
|
+
cd ../..
|
56
|
+
```
|
57
|
+
Then you can run the tests with:
|
58
|
+
|
59
|
+
```bash
|
60
|
+
bin/test
|
61
|
+
```
|
62
|
+
|
63
|
+
## Contributing
|
64
|
+
|
65
|
+
PR
|
66
|
+
|
67
|
+
## License
|
68
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Partitionable'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
task default: :test
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Partitionable
|
2
|
+
module ActsAsPartitionable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
before_save :create_partition_from_record
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def acts_as_partitionable(options = {})
|
11
|
+
cattr_accessor :index_fields
|
12
|
+
cattr_accessor :logdate_attr
|
13
|
+
self.index_fields = options[:index_fields]
|
14
|
+
self.logdate_attr = options[:logdate_attr]
|
15
|
+
|
16
|
+
def partition_name(month, year)
|
17
|
+
formatted_month = sprintf('%02d', month.to_i)
|
18
|
+
"#{self.table_name}_y#{year}m#{formatted_month}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_partition(month, year)
|
22
|
+
ActiveRecord::Base.connection.execute create_table_statement(month, year)
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_table_statement(month, year)
|
26
|
+
table = partition_name(month, year)
|
27
|
+
index_name = "#{table}_#{index_fields.join('_')}"
|
28
|
+
first_day_of_month = Date.civil(year, month, 1)
|
29
|
+
first_day_next_month = (first_day_of_month + 1.month)
|
30
|
+
<<-SQL
|
31
|
+
CREATE TABLE #{table} (
|
32
|
+
CHECK ( #{self.logdate_attr} >= DATE '#{first_day_of_month.to_s}' AND #{self.logdate_attr} < DATE '#{first_day_next_month.to_s}' )
|
33
|
+
) INHERITS (#{self.table_name});
|
34
|
+
CREATE INDEX #{index_name} ON #{table} (#{index_fields.join(',')});
|
35
|
+
SQL
|
36
|
+
end
|
37
|
+
|
38
|
+
def drop_partition(month, year)
|
39
|
+
name = partition_name(month, year)
|
40
|
+
index_name = "#{name}_#{index_fields.join('_')}"
|
41
|
+
function_name = "#{name}_insert_trigger_function()"
|
42
|
+
trigger_name = "#{name}_trigger"
|
43
|
+
ActiveRecord::Base.connection.execute(
|
44
|
+
<<-SQL
|
45
|
+
DROP TABLE IF EXISTS #{name};
|
46
|
+
DROP INDEX IF EXISTS #{index_name};
|
47
|
+
DROP FUNCTION IF EXISTS #{function_name} CASCADE;
|
48
|
+
DROP TRIGGER IF EXISTS #{trigger_name} ON #{self.table_name} CASCADE;
|
49
|
+
SQL
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def trigger_statement months_and_years
|
54
|
+
trigger_body = get_trigger_body(months_and_years)
|
55
|
+
statement = ""
|
56
|
+
statement += <<-SQL
|
57
|
+
CREATE OR REPLACE FUNCTION #{self.table_name}_insert_trigger()
|
58
|
+
RETURNS TRIGGER AS $$
|
59
|
+
SQL
|
60
|
+
|
61
|
+
statement += trigger_body
|
62
|
+
statement += <<-SQL
|
63
|
+
$$
|
64
|
+
LANGUAGE plpgsql;
|
65
|
+
|
66
|
+
DROP TRIGGER IF EXISTS insert_#{self.table_name}_trigger ON #{self.table_name};
|
67
|
+
CREATE TRIGGER insert_#{self.table_name}_trigger
|
68
|
+
BEFORE INSERT ON #{self.table_name}
|
69
|
+
FOR EACH ROW EXECUTE PROCEDURE #{self.table_name}_insert_trigger();
|
70
|
+
SQL
|
71
|
+
statement
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_trigger_body months_and_years
|
75
|
+
|
76
|
+
statement = ""
|
77
|
+
statement += <<-eos
|
78
|
+
BEGIN
|
79
|
+
eos
|
80
|
+
|
81
|
+
months_and_years.each_with_index do |data, index|
|
82
|
+
|
83
|
+
first_day_of_month = Date.civil(data[1].to_i, data[0].to_i, 1)
|
84
|
+
first_day_next_month = (first_day_of_month + 1.month)
|
85
|
+
if index == 0
|
86
|
+
statement += <<-eos
|
87
|
+
IF ( NEW.#{self.logdate_attr} >= DATE '#{first_day_of_month}' AND
|
88
|
+
NEW.#{self.logdate_attr} < DATE '#{first_day_next_month}' ) THEN
|
89
|
+
INSERT INTO #{partition_name(data[0], data[1])} VALUES (NEW.*);
|
90
|
+
eos
|
91
|
+
else
|
92
|
+
statement += <<-eos
|
93
|
+
ELSIF ( NEW.#{self.logdate_attr} >= DATE '#{first_day_of_month}' AND
|
94
|
+
NEW.#{self.logdate_attr} < DATE '#{first_day_next_month}' ) THEN
|
95
|
+
INSERT INTO #{partition_name(data[0], data[1])} VALUES (NEW.*);
|
96
|
+
eos
|
97
|
+
end
|
98
|
+
end
|
99
|
+
statement += <<-eos
|
100
|
+
END IF;
|
101
|
+
RETURN NULL;
|
102
|
+
END;
|
103
|
+
eos
|
104
|
+
end
|
105
|
+
|
106
|
+
def partition_exists?(month, year)
|
107
|
+
ActiveRecord::Base.connection.data_source_exists? partition_name(month, year)
|
108
|
+
end
|
109
|
+
|
110
|
+
def update_trigger
|
111
|
+
ActiveRecord::Base.connection.execute updated_trigger_statement
|
112
|
+
end
|
113
|
+
|
114
|
+
def updated_trigger_statement
|
115
|
+
tables = ActiveRecord::Base.connection.tables.select{|t| t =~ /#{self.table_name}_y[0-9]{4}m[0-9]{2}/}
|
116
|
+
months_and_years = tables.map {|t| [t.match(/m\K[0-9]{2}/)[0], t.match(/y\K[0-9]{4}/)[0]]}
|
117
|
+
trigger_statement months_and_years.sort_by{|month, year| [year.to_i, month.to_i]}
|
118
|
+
end
|
119
|
+
|
120
|
+
include Partitionable::ActsAsPartitionable::LocalInstanceMethods
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
module LocalInstanceMethods
|
125
|
+
def has_partition?
|
126
|
+
month = self.send(self.class.logdate_attr.to_sym).month
|
127
|
+
year = self.send(self.class.logdate_attr.to_sym).year
|
128
|
+
self.class.partition_exists? month,year
|
129
|
+
end
|
130
|
+
|
131
|
+
def create_partition_from_record
|
132
|
+
return if has_partition?
|
133
|
+
|
134
|
+
month = self.send(self.class.logdate_attr.to_sym).month
|
135
|
+
year = self.send(self.class.logdate_attr.to_sym).year
|
136
|
+
self.class.create_partition(month,year)
|
137
|
+
self.class.update_trigger
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: partitionable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pablo Acuña
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-09-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.0
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 5.0.0.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 5.0.0
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 5.0.0.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: pg
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
description:
|
48
|
+
email:
|
49
|
+
- pabloacuna88@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- MIT-LICENSE
|
55
|
+
- README.md
|
56
|
+
- Rakefile
|
57
|
+
- lib/partitionable.rb
|
58
|
+
- lib/partitionable/acts_as_partitionable.rb
|
59
|
+
- lib/partitionable/version.rb
|
60
|
+
- lib/tasks/partitionable_tasks.rake
|
61
|
+
homepage: http://www.archdaily.com
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 2.5.1
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: Seamless PostgreSQL date partitioning for your Rails models.
|
85
|
+
test_files: []
|