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.
@@ -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: []