pg_partitions 0.1.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/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +112 -0
- data/Rakefile +23 -0
- data/bin/console +31 -0
- data/bin/setup +7 -0
- data/lib/pg_partitions.rb +49 -0
- data/lib/pg_partitions/sql.rb +82 -0
- data/lib/pg_partitions/version.rb +3 -0
- data/pg_partitions.gemspec +28 -0
- metadata +126 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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,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,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: []
|