pg_party 1.1.0 → 1.5.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 +4 -4
- data/README.md +298 -9
- data/lib/pg_party.rb +24 -3
- data/lib/pg_party/adapter/abstract_methods.rb +36 -0
- data/lib/pg_party/adapter/postgresql_methods.rb +45 -8
- data/lib/pg_party/adapter_decorator.rb +303 -28
- data/lib/pg_party/cache.rb +51 -16
- data/lib/pg_party/config.rb +22 -0
- data/lib/pg_party/hacks/postgresql_database_tasks.rb +25 -0
- data/lib/pg_party/model/hash_methods.rb +18 -0
- data/lib/pg_party/model/list_methods.rb +7 -2
- data/lib/pg_party/model/methods.rb +8 -4
- data/lib/pg_party/model/range_methods.rb +7 -2
- data/lib/pg_party/model/shared_methods.rb +14 -4
- data/lib/pg_party/model_decorator.rb +53 -32
- data/lib/pg_party/model_injector.rb +17 -5
- data/lib/pg_party/version.rb +1 -1
- metadata +43 -13
- data/lib/pg_party/hacks/schema_cache.rb +0 -13
data/lib/pg_party/cache.rb
CHANGED
@@ -6,32 +6,67 @@ module PgParty
|
|
6
6
|
class Cache
|
7
7
|
LOCK = Mutex.new
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
def initialize
|
10
|
+
# automatically initialize a new hash when
|
11
|
+
# accessing an object id that doesn't exist
|
12
|
+
@store = Hash.new { |h, k| h[k] = { models: {}, partitions: nil, partitions_with_subpartitions: nil } }
|
13
|
+
end
|
14
|
+
|
15
|
+
def clear!
|
16
|
+
LOCK.synchronize { @store.clear }
|
17
|
+
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch_model(key, child_table, &block)
|
22
|
+
return block.call unless caching_enabled?
|
23
|
+
|
24
|
+
LOCK.synchronize { fetch_value(@store[key][:models], child_table.to_sym, block) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def fetch_partitions(key, include_subpartitions, &block)
|
28
|
+
return block.call unless caching_enabled?
|
29
|
+
sub_key = include_subpartitions ? :partitions_with_subpartitions : :partitions
|
30
|
+
|
31
|
+
LOCK.synchronize { fetch_value(@store[key], sub_key, block) }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def caching_enabled?
|
37
|
+
PgParty.config.caching
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch_value(subhash, key, block)
|
41
|
+
entry = subhash[key]
|
12
42
|
|
13
|
-
|
43
|
+
if entry.nil? || entry.expired?
|
44
|
+
entry = Entry.new(block.call)
|
45
|
+
subhash[key] = entry
|
14
46
|
end
|
15
47
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
48
|
+
entry.value
|
49
|
+
end
|
50
|
+
|
51
|
+
class Entry
|
52
|
+
attr_reader :value
|
53
|
+
|
54
|
+
def initialize(value)
|
55
|
+
@value = value
|
56
|
+
@timestamp = Time.now
|
20
57
|
end
|
21
58
|
|
22
|
-
def
|
23
|
-
|
24
|
-
store[key][:partitions] ||= block.call
|
25
|
-
end
|
59
|
+
def expired?
|
60
|
+
ttl.positive? && Time.now - @timestamp > ttl
|
26
61
|
end
|
27
62
|
|
28
63
|
private
|
29
64
|
|
30
|
-
def
|
31
|
-
|
32
|
-
# accessing an object id that doesn't exist
|
33
|
-
@store ||= Hash.new { |h, k| h[k] = { models: {}, partitions: nil } }
|
65
|
+
def ttl
|
66
|
+
PgParty.config.caching_ttl
|
34
67
|
end
|
35
68
|
end
|
69
|
+
|
70
|
+
private_constant :Entry
|
36
71
|
end
|
37
72
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgParty
|
4
|
+
class Config
|
5
|
+
attr_accessor \
|
6
|
+
:caching,
|
7
|
+
:caching_ttl,
|
8
|
+
:schema_exclude_partitions,
|
9
|
+
:create_template_tables,
|
10
|
+
:create_with_primary_key,
|
11
|
+
:include_subpartitions_in_partition_list
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@caching = true
|
15
|
+
@caching_ttl = -1
|
16
|
+
@schema_exclude_partitions = true
|
17
|
+
@create_template_tables = true
|
18
|
+
@create_with_primary_key = false
|
19
|
+
@include_subpartitions_in_partition_list = false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgParty
|
4
|
+
module Hacks
|
5
|
+
module PostgreSQLDatabaseTasks
|
6
|
+
def run_cmd(cmd, args, action)
|
7
|
+
if action != "dumping" || !PgParty.config.schema_exclude_partitions
|
8
|
+
return super
|
9
|
+
end
|
10
|
+
|
11
|
+
partitions = begin
|
12
|
+
ActiveRecord::Base.connection.select_values(
|
13
|
+
"SELECT DISTINCT inhrelid::regclass::text FROM pg_inherits"
|
14
|
+
)
|
15
|
+
rescue
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
|
19
|
+
excluded_tables = partitions.flat_map { |table| ["-T", "*.#{table}"] }
|
20
|
+
|
21
|
+
super(cmd, args + excluded_tables, action)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pg_party/model_decorator"
|
4
|
+
require "ruby2_keywords"
|
5
|
+
|
6
|
+
module PgParty
|
7
|
+
module Model
|
8
|
+
module HashMethods
|
9
|
+
ruby2_keywords def create_partition(*args)
|
10
|
+
PgParty::ModelDecorator.new(self).create_hash_partition(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
ruby2_keywords def partition_key_in(*args)
|
14
|
+
PgParty::ModelDecorator.new(self).hash_partition_key_in(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,15 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "pg_party/model_decorator"
|
4
|
+
require "ruby2_keywords"
|
4
5
|
|
5
6
|
module PgParty
|
6
7
|
module Model
|
7
8
|
module ListMethods
|
8
|
-
def create_partition(*args)
|
9
|
+
ruby2_keywords def create_partition(*args)
|
9
10
|
PgParty::ModelDecorator.new(self).create_list_partition(*args)
|
10
11
|
end
|
11
12
|
|
12
|
-
def
|
13
|
+
ruby2_keywords def create_default_partition(*args)
|
14
|
+
PgParty::ModelDecorator.new(self).create_default_partition(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
ruby2_keywords def partition_key_in(*args)
|
13
18
|
PgParty::ModelDecorator.new(self).list_partition_key_in(*args)
|
14
19
|
end
|
15
20
|
end
|
@@ -5,12 +5,16 @@ require "pg_party/model_injector"
|
|
5
5
|
module PgParty
|
6
6
|
module Model
|
7
7
|
module Methods
|
8
|
-
def range_partition_by(key
|
9
|
-
PgParty::ModelInjector.new(self, key
|
8
|
+
def range_partition_by(*key, &blk)
|
9
|
+
PgParty::ModelInjector.new(self, *key, &blk).inject_range_methods
|
10
10
|
end
|
11
11
|
|
12
|
-
def list_partition_by(key
|
13
|
-
PgParty::ModelInjector.new(self, key
|
12
|
+
def list_partition_by(*key, &blk)
|
13
|
+
PgParty::ModelInjector.new(self, *key, &blk).inject_list_methods
|
14
|
+
end
|
15
|
+
|
16
|
+
def hash_partition_by(*key, &blk)
|
17
|
+
PgParty::ModelInjector.new(self, *key, &blk).inject_hash_methods
|
14
18
|
end
|
15
19
|
|
16
20
|
def partitioned?
|
@@ -1,15 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "pg_party/model_decorator"
|
4
|
+
require "ruby2_keywords"
|
4
5
|
|
5
6
|
module PgParty
|
6
7
|
module Model
|
7
8
|
module RangeMethods
|
8
|
-
def create_partition(*args)
|
9
|
+
ruby2_keywords def create_partition(*args)
|
9
10
|
PgParty::ModelDecorator.new(self).create_range_partition(*args)
|
10
11
|
end
|
11
12
|
|
12
|
-
def
|
13
|
+
ruby2_keywords def create_default_partition(*args)
|
14
|
+
PgParty::ModelDecorator.new(self).create_default_partition(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
ruby2_keywords def partition_key_in(*args)
|
13
18
|
PgParty::ModelDecorator.new(self).range_partition_key_in(*args)
|
14
19
|
end
|
15
20
|
end
|
@@ -6,15 +6,25 @@ module PgParty
|
|
6
6
|
module Model
|
7
7
|
module SharedMethods
|
8
8
|
def reset_primary_key
|
9
|
-
|
9
|
+
return base_class.primary_key if self != base_class
|
10
|
+
|
11
|
+
partitions = partitions(include_subpartitions: PgParty.config.include_subpartitions_in_partition_list)
|
12
|
+
return get_primary_key(base_class.name) if partitions.empty?
|
13
|
+
|
14
|
+
first_partition = partitions.detect { |p| !connection.table_partitioned?(p) }
|
15
|
+
raise 'No leaf partitions exist for this model. Create a partition to contain your data' unless first_partition
|
16
|
+
|
17
|
+
in_partition(first_partition).get_primary_key(base_class.name)
|
10
18
|
end
|
11
19
|
|
12
20
|
def table_exists?
|
13
|
-
|
21
|
+
target_table = partitions.first || table_name
|
22
|
+
|
23
|
+
connection.schema_cache.data_source_exists?(target_table)
|
14
24
|
end
|
15
25
|
|
16
|
-
def partitions
|
17
|
-
PgParty::ModelDecorator.new(self).partitions
|
26
|
+
def partitions(**args)
|
27
|
+
PgParty::ModelDecorator.new(self).partitions(**args)
|
18
28
|
end
|
19
29
|
|
20
30
|
def in_partition(*args)
|
@@ -1,27 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "pg_party/cache"
|
4
|
-
|
5
3
|
module PgParty
|
6
4
|
class ModelDecorator < SimpleDelegator
|
7
|
-
def partition_primary_key
|
8
|
-
if self != base_class
|
9
|
-
base_class.primary_key
|
10
|
-
elsif partition_name = partitions.first
|
11
|
-
in_partition(partition_name).get_primary_key(base_class.name)
|
12
|
-
else
|
13
|
-
get_primary_key(base_class.name)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def partition_table_exists?
|
18
|
-
target_table = partitions.first || table_name
|
19
|
-
|
20
|
-
connection.schema_cache.data_source_exists?(target_table)
|
21
|
-
end
|
22
|
-
|
23
5
|
def in_partition(child_table_name)
|
24
|
-
PgParty
|
6
|
+
PgParty.cache.fetch_model(cache_key, child_table_name) do
|
25
7
|
Class.new(__getobj__) do
|
26
8
|
self.table_name = child_table_name
|
27
9
|
|
@@ -41,6 +23,11 @@ module PgParty
|
|
41
23
|
def self.new(*args, &blk)
|
42
24
|
superclass.new(*args, &blk)
|
43
25
|
end
|
26
|
+
|
27
|
+
# to avoid unnecessary db lookups
|
28
|
+
def self.partitions
|
29
|
+
[]
|
30
|
+
end
|
44
31
|
end
|
45
32
|
end
|
46
33
|
end
|
@@ -49,7 +36,7 @@ module PgParty
|
|
49
36
|
if complex_partition_key
|
50
37
|
complex_partition_key_query("(#{partition_key}) = (?)", value)
|
51
38
|
else
|
52
|
-
|
39
|
+
where_partition_key(:eq, value)
|
53
40
|
end
|
54
41
|
end
|
55
42
|
|
@@ -61,9 +48,9 @@ module PgParty
|
|
61
48
|
end_range
|
62
49
|
)
|
63
50
|
else
|
64
|
-
|
65
|
-
|
66
|
-
|
51
|
+
where_partition_key(:gteq, start_range).merge(
|
52
|
+
where_partition_key(:lt, end_range)
|
53
|
+
)
|
67
54
|
end
|
68
55
|
end
|
69
56
|
|
@@ -75,15 +62,11 @@ module PgParty
|
|
75
62
|
end
|
76
63
|
end
|
77
64
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
INNER JOIN pg_inherits
|
84
|
-
ON pg_tables.tablename::regclass = pg_inherits.inhparent::regclass
|
85
|
-
WHERE pg_tables.tablename = #{connection.quote(table_name)}
|
86
|
-
SQL
|
65
|
+
alias_method :hash_partition_key_in, :list_partition_key_in
|
66
|
+
|
67
|
+
def partitions(include_subpartitions: PgParty.config.include_subpartitions_in_partition_list)
|
68
|
+
PgParty.cache.fetch_partitions(cache_key, include_subpartitions) do
|
69
|
+
connection.partitions_for_table_name(table_name, include_subpartitions: include_subpartitions)
|
87
70
|
end
|
88
71
|
rescue
|
89
72
|
[]
|
@@ -108,6 +91,23 @@ module PgParty
|
|
108
91
|
create_partition(:create_list_partition_of, table_name, **modified_options)
|
109
92
|
end
|
110
93
|
|
94
|
+
def create_hash_partition(modulus:, remainder:, **options)
|
95
|
+
modified_options = options.merge(
|
96
|
+
modulus: modulus,
|
97
|
+
remainder: remainder,
|
98
|
+
primary_key: primary_key,
|
99
|
+
)
|
100
|
+
|
101
|
+
create_partition(:create_hash_partition_of, table_name, **modified_options)
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_default_partition(**options)
|
105
|
+
modified_options = options.merge(
|
106
|
+
primary_key: primary_key,
|
107
|
+
)
|
108
|
+
create_partition(:create_default_partition_of, table_name, **modified_options)
|
109
|
+
end
|
110
|
+
|
111
111
|
private
|
112
112
|
|
113
113
|
def create_partition(migration_method, table_name, **options)
|
@@ -145,5 +145,26 @@ module PgParty
|
|
145
145
|
|
146
146
|
from(subquery, current_alias)
|
147
147
|
end
|
148
|
+
|
149
|
+
def where_partition_key(meth, values)
|
150
|
+
partition_key_array = Array.wrap(partition_key)
|
151
|
+
values = Array.wrap(values)
|
152
|
+
|
153
|
+
if partition_key_array.size != values.size
|
154
|
+
raise "number of provided values does not match the number of partition key columns"
|
155
|
+
end
|
156
|
+
|
157
|
+
arel_query = partition_key_array.zip(values).inject(nil) do |obj, (column, value)|
|
158
|
+
node = current_arel_table[column].send(meth, value)
|
159
|
+
|
160
|
+
if obj.nil?
|
161
|
+
node
|
162
|
+
else
|
163
|
+
obj.and(node)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
where(arel_query)
|
168
|
+
end
|
148
169
|
end
|
149
170
|
end
|
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
module PgParty
|
4
4
|
class ModelInjector
|
5
|
-
def initialize(model, key)
|
5
|
+
def initialize(model, *key, &blk)
|
6
6
|
@model = model
|
7
|
-
@key = key
|
7
|
+
@key = key.flatten.compact
|
8
|
+
@key_blk = blk
|
8
9
|
end
|
9
10
|
|
10
11
|
def inject_range_methods
|
@@ -19,6 +20,12 @@ module PgParty
|
|
19
20
|
inject_methods_for(PgParty::Model::ListMethods)
|
20
21
|
end
|
21
22
|
|
23
|
+
def inject_hash_methods
|
24
|
+
require "pg_party/model/hash_methods"
|
25
|
+
|
26
|
+
inject_methods_for(PgParty::Model::HashMethods)
|
27
|
+
end
|
28
|
+
|
22
29
|
private
|
23
30
|
|
24
31
|
def inject_methods_for(mod)
|
@@ -38,11 +45,16 @@ module PgParty
|
|
38
45
|
instance_predicate: false
|
39
46
|
)
|
40
47
|
|
41
|
-
if @
|
42
|
-
@model.partition_key = @
|
48
|
+
if @key_blk
|
49
|
+
@model.partition_key = @key_blk.call
|
43
50
|
@model.complex_partition_key = true
|
44
51
|
else
|
45
|
-
@
|
52
|
+
if @key.size == 1
|
53
|
+
@model.partition_key = @key.first
|
54
|
+
else
|
55
|
+
@model.partition_key = @key
|
56
|
+
end
|
57
|
+
|
46
58
|
@model.complex_partition_key = false
|
47
59
|
end
|
48
60
|
end
|
data/lib/pg_party/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_party
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Krage
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,20 +16,48 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5.0'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '6.
|
22
|
+
version: '6.2'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
29
|
+
version: '5.0'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '6.
|
32
|
+
version: '6.2'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: ruby2_keywords
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.0.2
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.0.2
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: parallel
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.0'
|
33
61
|
- !ruby/object:Gem::Dependency
|
34
62
|
name: appraisal
|
35
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -64,14 +92,14 @@ dependencies:
|
|
64
92
|
requirements:
|
65
93
|
- - "~>"
|
66
94
|
- !ruby/object:Gem::Version
|
67
|
-
version: '1.
|
95
|
+
version: '1.3'
|
68
96
|
type: :development
|
69
97
|
prerelease: false
|
70
98
|
version_requirements: !ruby/object:Gem::Requirement
|
71
99
|
requirements:
|
72
100
|
- - "~>"
|
73
101
|
- !ruby/object:Gem::Version
|
74
|
-
version: '1.
|
102
|
+
version: '1.3'
|
75
103
|
- !ruby/object:Gem::Dependency
|
76
104
|
name: database_cleaner
|
77
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -182,14 +210,14 @@ dependencies:
|
|
182
210
|
requirements:
|
183
211
|
- - "~>"
|
184
212
|
- !ruby/object:Gem::Version
|
185
|
-
version:
|
213
|
+
version: 0.17.0
|
186
214
|
type: :development
|
187
215
|
prerelease: false
|
188
216
|
version_requirements: !ruby/object:Gem::Requirement
|
189
217
|
requirements:
|
190
218
|
- - "~>"
|
191
219
|
- !ruby/object:Gem::Version
|
192
|
-
version:
|
220
|
+
version: 0.17.0
|
193
221
|
- !ruby/object:Gem::Dependency
|
194
222
|
name: timecop
|
195
223
|
requirement: !ruby/object:Gem::Requirement
|
@@ -219,7 +247,9 @@ files:
|
|
219
247
|
- lib/pg_party/adapter/postgresql_methods.rb
|
220
248
|
- lib/pg_party/adapter_decorator.rb
|
221
249
|
- lib/pg_party/cache.rb
|
222
|
-
- lib/pg_party/
|
250
|
+
- lib/pg_party/config.rb
|
251
|
+
- lib/pg_party/hacks/postgresql_database_tasks.rb
|
252
|
+
- lib/pg_party/model/hash_methods.rb
|
223
253
|
- lib/pg_party/model/list_methods.rb
|
224
254
|
- lib/pg_party/model/methods.rb
|
225
255
|
- lib/pg_party/model/range_methods.rb
|
@@ -239,14 +269,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
239
269
|
requirements:
|
240
270
|
- - ">="
|
241
271
|
- !ruby/object:Gem::Version
|
242
|
-
version: 2.
|
272
|
+
version: 2.5.0
|
243
273
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
244
274
|
requirements:
|
245
275
|
- - ">="
|
246
276
|
- !ruby/object:Gem::Version
|
247
277
|
version: '0'
|
248
278
|
requirements: []
|
249
|
-
rubygems_version: 3.
|
279
|
+
rubygems_version: 3.1.4
|
250
280
|
signing_key:
|
251
281
|
specification_version: 4
|
252
282
|
summary: ActiveRecord PostgreSQL Partitioning
|