partitionable 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/pacuna/partitionable.svg?branch=master)](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: []
|