pg_party 1.0.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,32 +6,67 @@ module PgParty
6
6
  class Cache
7
7
  LOCK = Mutex.new
8
8
 
9
- class << self
10
- def clear!
11
- LOCK.synchronize { store.clear }
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
- nil
43
+ if entry.nil? || entry.expired?
44
+ entry = Entry.new(block.call)
45
+ subhash[key] = entry
14
46
  end
15
47
 
16
- def fetch_model(key, child_table, &block)
17
- LOCK.synchronize do
18
- store[key][:models][child_table.to_sym] ||= block.call
19
- end
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 fetch_partitions(key, &block)
23
- LOCK.synchronize do
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 store
31
- # automatically initialize a new hash when
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 partition_key_in(*args)
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=nil, &blk)
9
- PgParty::ModelInjector.new(self, key || blk).inject_range_methods
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=nil, &blk)
13
- PgParty::ModelInjector.new(self, key || blk).inject_list_methods
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 partition_key_in(*args)
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
- PgParty::ModelDecorator.new(self).partition_primary_key
9
+ return base_class.primary_key if self != base_class
10
+
11
+ partitions = partitions(include_subpartitions: true)
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
- PgParty::ModelDecorator.new(self).partition_table_exists?
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::Cache.fetch_model(cache_key, child_table_name) do
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
- where(current_arel_table[partition_key].eq(value))
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
- node = current_arel_table[partition_key]
65
-
66
- where(node.gteq(start_range).and(node.lt(end_range)))
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
- def partitions
79
- PgParty::Cache.fetch_partitions(cache_key) do
80
- connection.select_values(<<-SQL)
81
- SELECT pg_inherits.inhrelid::regclass::text
82
- FROM pg_tables
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)
@@ -138,21 +138,33 @@ module PgParty
138
138
  end
139
139
  end
140
140
 
141
- def model_class
142
- if respond_to?(:klass)
143
- klass
144
- else
145
- self
146
- end
147
- end
148
-
149
141
  def complex_partition_key_query(clause, *interpolated_values)
150
- subquery = model_class
151
- .unscoped
142
+ subquery = unscoped
152
143
  .select("*")
153
144
  .where(clause, *interpolated_values)
154
145
 
155
146
  from(subquery, current_alias)
156
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
157
169
  end
158
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 @key.is_a?(Proc)
42
- @model.partition_key = @key.call
48
+ if @key_blk
49
+ @model.partition_key = @key_blk.call
43
50
  @model.complex_partition_key = true
44
51
  else
45
- @model.partition_key = @key
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgParty
4
- VERSION = "1.0.1"
4
+ VERSION = "1.4.0"
5
5
  end