partitionable 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -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).
@@ -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,5 @@
1
+ require 'partitionable/acts_as_partitionable'
2
+
3
+ module Partitionable
4
+ # Your code goes here...
5
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Partitionable
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :partitionable do
3
+ # # Task goes here
4
+ # 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: []