pg_partitions 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5fc6179ef26414c9abbf9d233068064d2c309f30
4
+ data.tar.gz: e3ea73ed26170bfa49a7343633b1720beeeadcab
5
+ SHA512:
6
+ metadata.gz: 46dfeb813305c00f1a07e05f7073c85ffcd092954193b2a9744b7570c2c18df23b8fb1d9bc7df73b74cb051da41bd0bf924583c53f60d648c25ac6803db3f3f2
7
+ data.tar.gz: 86c772af301a20e45e9b7b2a4cf14a65bff62d3ccf4fb5a60b314f24c9ddc3a79c72eda275128cc809fc483aabb1bb487ea3f4289ad23e2a4b271d18e60596e4
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ before_install: gem install bundler -v 1.14.4
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pg_partitions.gemspec
4
+ gemspec
5
+ gem 'pry'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Ray Zane
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # PgPartitions
2
+
3
+ Partitioning postgres takes some doing. PgPartitions adds methods to your migrations to help you manage them.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'pg_partitions'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ ## Usage
18
+
19
+ Imagine you have a comments table with millions of rows and your queries are starting to be a bit slow. Postgres partitioning allows yo to divide your comments table into smaller tables.
20
+
21
+ In a migration, you'll first need to include `PgPartitions`.
22
+
23
+ ```ruby
24
+ class PartitionComments < ActiveRecord::Migration[5.1]
25
+ include PgPartitions
26
+
27
+ def change
28
+ # ...
29
+ end
30
+ end
31
+ ```
32
+
33
+ Let's assume we have a column called year that stores the year the comment was created. We can partition our table based on the value of that column:
34
+
35
+ ```ruby
36
+ add_partition :comments, :comments_2016, check: 'year = 2016'
37
+ add_partition :comments, :comments_2017, check: 'year = 2017'
38
+ ```
39
+
40
+ After we create our partitions, the query plan is going to change a little bit:
41
+
42
+ ```ruby
43
+ Comment.all.explain
44
+ => EXPLAIN for: SELECT "comments".* FROM "comments"
45
+ QUERY PLAN
46
+ ------------------------------------------------------------------------
47
+ Append (cost=0.00..60.80 rows=4081 width=12)
48
+ -> Seq Scan on comments (cost=0.00..0.00 rows=1 width=12)
49
+ -> Seq Scan on comments_2016 (cost=0.00..30.40 rows=2040 width=12)
50
+ -> Seq Scan on comments_2017 (cost=0.00..30.40 rows=2040 width=12)
51
+ ```
52
+
53
+ See how it's querying our partitions in addition to the parent table? Now, watch what happens when we put a WHERE condition on the `year` column:
54
+
55
+ ```ruby
56
+ Comment.where(year: 2016).explain
57
+ => EXPLAIN for: SELECT "comments".* FROM "comments" WHERE "comments"."year" = $1 [["year", 2016]]
58
+ QUERY PLAN
59
+ ----------------------------------------------------------------------
60
+ Append (cost=0.00..35.50 rows=11 width=12)
61
+ -> Seq Scan on comments (cost=0.00..0.00 rows=1 width=12)
62
+ Filter: (year = 2016)
63
+ -> Seq Scan on comments_2016 (cost=0.00..35.50 rows=10 width=12)
64
+ Filter: (year = 2016)
65
+ ```
66
+
67
+ Notice how it never looked at the `comments_2017` table? That's the magic of partitions.
68
+
69
+ Now, there's one remaining issue. When we insert data into the `comments` table, we need it to route to be inserted into a partition instead of the actual table. For that, we can create a trigger:
70
+
71
+ ```ruby
72
+ add_partition_trigger :comments, :comments_by_year, [
73
+ { if: 'NEW.year = 2016', insert: :comments_2016 },
74
+ { elsif: 'NEW.year = 2017', insert: :comments_2017 },
75
+ { else: "RAISE EXECEPTION 'comments_by_year recieived an unexpected value: %', NEW.year;" }
76
+ ]
77
+ ```
78
+
79
+ If the new record has a `year` of 2016, it'll be inserted into the `comments_2016` table. If the `year` is 2017, it'll be inserted into the `comments_2017` table. Otherwise, the trigger will throw an error.
80
+
81
+ Now, imagine a year goes by and you need to add another partition for `2018`. You'll need to add the partition and update the trigger:
82
+
83
+ ```ruby
84
+ add_partition :comments, :comments_2018, check: 'NEW.year = 2018'
85
+
86
+ update_partition_trigger :comments, :comments_by_year, [
87
+ { if: 'NEW.year = 2016', insert: :comments_2016 },
88
+ { elsif: 'NEW.year = 2017', insert: :comments_2017 },
89
+ { elsif: 'NEW.year = 2018', insert: :comments_2018 },
90
+ { else: "RAISE EXECEPTION 'comments_by_year recieived an unexpected value: %', NEW.year;" }
91
+ ]
92
+ ```
93
+
94
+ ## Caveats
95
+
96
+ * You'll have to set `config.active_record.schema_format = :sql`. PgPartition doesn't support the use of `schema.rb`.
97
+
98
+ ## Development
99
+
100
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
101
+
102
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rzane/pg_partitions.
107
+
108
+
109
+ ## License
110
+
111
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
112
+
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ namespace :db do
11
+ task :setup do
12
+ system 'createdb pg_partitions_dev'
13
+ system 'createdb pg_partitions_test'
14
+ end
15
+
16
+ task :reset do
17
+ system 'dropdb pg_partitions_dev'
18
+ system 'dropdb pg_partitions_test'
19
+ Rake::Task['db:setup'].invoke
20
+ end
21
+ end
22
+
23
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'pg_partitions'
5
+ require 'active_record'
6
+ require 'pry'
7
+
8
+ ActiveRecord::Base.establish_connection(
9
+ adapter: 'postgresql',
10
+ database: 'pg_partitions_dev'
11
+ )
12
+
13
+ class Migration < ActiveRecord::Migration[5.1]
14
+ include PgPartitions
15
+
16
+ def change
17
+ create_table :comments do |t|
18
+ t.integer :year
19
+ end
20
+
21
+ add_partition :comments, :comments_2016, check: 'year = 2016'
22
+ add_partition :comments, :comments_2017, check: 'year = 2017'
23
+ end
24
+ end
25
+
26
+ ActiveRecord::Migration.run Migration
27
+
28
+ class Comment < ActiveRecord::Base
29
+ end
30
+
31
+ binding.pry
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+ rake db:setup
@@ -0,0 +1,49 @@
1
+ require 'active_support/core_ext/string'
2
+
3
+ require 'pg_partitions/version'
4
+ require 'pg_partitions/sql'
5
+
6
+ module PgPartitions
7
+ def add_partition(table, name, check:)
8
+ statement = SQL::Partition.new(table, check)
9
+ create_table(name, id: false, options: statement.to_sql)
10
+ end
11
+
12
+ def add_partition_trigger(table, name, conditions)
13
+ insert_trigger = SQL::Trigger.new(table, "#{name}_insert", 'BEFORE INSERT')
14
+ delete_function = SQL::DeleteFunction.new(table, "#{name}_delete")
15
+ delete_trigger = SQL::Trigger.new(table, "#{name}_delete", 'AFTER INSERT')
16
+
17
+ reversible do |dir|
18
+ dir.up do
19
+ update_partition_trigger(table, name, conditions)
20
+ execute insert_trigger.to_sql
21
+
22
+ execute delete_function.to_sql
23
+ execute delete_trigger.to_sql
24
+ end
25
+
26
+ dir.down do
27
+ drop_partition_trigger(table, name)
28
+ end
29
+ end
30
+ end
31
+
32
+ def update_partition_trigger(table, name, conditions)
33
+ insert_conditions = SQL::If.new(conditions)
34
+ insert_function = SQL::InsertFunction.new(
35
+ table,
36
+ "#{name}_insert",
37
+ insert_conditions.to_sql
38
+ )
39
+
40
+ execute insert_function.to_sql
41
+ end
42
+
43
+ def drop_partition_trigger(table, name)
44
+ execute "DROP TRIGGER #{name}_insert ON #{table}"
45
+ execute "DROP FUNCTION #{name}_insert()"
46
+ execute "DROP TRIGGER #{name}_delete ON #{table}"
47
+ execute "DROP FUNCTION #{name}_delete()"
48
+ end
49
+ end
@@ -0,0 +1,82 @@
1
+ module PgPartitions
2
+ module SQL
3
+ class Partition < Struct.new(:table, :check)
4
+ def to_sql
5
+ "(LIKE #{table} INCLUDING ALL, CHECK (#{check})) INHERITS (#{table})"
6
+ end
7
+ end
8
+
9
+ class If < Struct.new(:conditions)
10
+ def to_sql
11
+ if conditions.empty?
12
+ raise ArgumentError, 'You must provide at least one condition'
13
+ end
14
+
15
+ lines = conditions.map do |opts|
16
+ if opts.key? :if
17
+ build_condition :if, opts
18
+ elsif opts.key? :elsif
19
+ build_condition :elsif, opts
20
+ else opts.key? :else
21
+ "ELSE\n #{opts[:else]}"
22
+ end
23
+ end
24
+
25
+ lines << 'END IF;'
26
+ lines.join("\n") << "\n"
27
+ end
28
+
29
+ private
30
+
31
+ def build_condition(key, opts)
32
+ then_sql = opts.fetch :then do
33
+ "INSERT INTO #{opts.fetch(:insert)} VALUES(NEW.*) RETURNING * INTO result;"
34
+ end
35
+
36
+ "#{key.to_s.upcase} (#{opts[key]}) THEN\n #{then_sql}"
37
+ end
38
+ end
39
+
40
+ class Function < Struct.new(:table, :name, :body)
41
+ def to_sql
42
+ <<~SQL
43
+ CREATE OR REPLACE FUNCTION #{name}()
44
+ RETURNS TRIGGER AS $$
45
+ DECLARE
46
+ result #{table}%rowtype;
47
+ BEGIN
48
+ #{body.indent(2)}
49
+ RETURN result;
50
+ END;
51
+ $$
52
+ LANGUAGE plpgsql;
53
+ SQL
54
+ end
55
+ end
56
+
57
+ class InsertFunction < Function
58
+ end
59
+
60
+ class DeleteFunction < Function
61
+ def initialize(table, name)
62
+ super(table, name, delete_statement(table))
63
+ end
64
+
65
+ private
66
+
67
+ def delete_statement(table)
68
+ "DELETE FROM ONLY #{table} WHERE id = NEW.id RETURNING * INTO result;"
69
+ end
70
+ end
71
+
72
+ class Trigger < Struct.new(:table, :name, :event)
73
+ def to_sql
74
+ <<~SQL
75
+ CREATE TRIGGER #{name}
76
+ #{event} ON #{table}
77
+ FOR EACH ROW EXECUTE PROCEDURE #{name}();
78
+ SQL
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,3 @@
1
+ module PgPartitions
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pg_partitions/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pg_partitions"
8
+ spec.version = PgPartitions::VERSION
9
+ spec.authors = ["Ray Zane"]
10
+ spec.email = ["ray@promptworks.com"]
11
+
12
+ spec.summary = %q{ActiveRecord::Migration utility for creating partitions in PostgreSQL.}
13
+ spec.homepage = "https://github.com/rzane/pg_partitions"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.14"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "minitest", "~> 5.0"
26
+ spec.add_development_dependency "pg"
27
+ spec.add_development_dependency "activerecord"
28
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_partitions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ray Zane
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activerecord
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - ray@promptworks.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".travis.yml"
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - bin/console
97
+ - bin/setup
98
+ - lib/pg_partitions.rb
99
+ - lib/pg_partitions/sql.rb
100
+ - lib/pg_partitions/version.rb
101
+ - pg_partitions.gemspec
102
+ homepage: https://github.com/rzane/pg_partitions
103
+ licenses:
104
+ - MIT
105
+ metadata: {}
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.6.10
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: ActiveRecord::Migration utility for creating partitions in PostgreSQL.
126
+ test_files: []