acts_as_partitioned 0.0.2
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 +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/acts_as_partitioned.gemspec +23 -0
- data/lib/active_record/acts/partitioned/.key.rb.swp +0 -0
- data/lib/active_record/acts/partitioned/cache/partition_cache.rb +36 -0
- data/lib/active_record/acts/partitioned/cache/partition_cache_entry.rb +35 -0
- data/lib/active_record/acts/partitioned/cache/partition_cache_entry_factory.rb +24 -0
- data/lib/active_record/acts/partitioned/copy_proxy.rb +108 -0
- data/lib/active_record/acts/partitioned/factory.rb +126 -0
- data/lib/active_record/acts/partitioned/key.rb +69 -0
- data/lib/active_record/acts/partitioned/keys.rb +31 -0
- data/lib/active_record/acts/partitioned/partition.rb +121 -0
- data/lib/active_record/acts/partitioned/structure.rb +30 -0
- data/lib/active_record/acts/partitioned.rb +57 -0
- data/lib/acts_as_partitioned/version.rb +3 -0
- data/lib/acts_as_partitioned.rb +8 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ff28314db1efa3069fbdb9097112672045cd2b2a
|
4
|
+
data.tar.gz: 3a7dcebc0758cc66aeedd53711e502f934c94d15
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 59b8231df8d42e6e7f86020afc2da307282c3b918fd7126a2b7c5f495a91d66e2e81c5b5ee9a81b5528d84e8f1887433c15cbde4e25b146b289ecf3029bf8dfb
|
7
|
+
data.tar.gz: ed96751cc941bda70c2b766c25f21da0f6cd7fa81dbc1be6263725c9052c3408c0fb0580dd26321856aba92471391ba5da79a0a6d7dc77fa24ccf07c4baced21
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Dan Draper
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# ActsAsPartitioned
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'acts_as_partitioned'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install acts_as_partitioned
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/[my-github-username]/acts_as_partitioned/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'acts_as_partitioned/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "acts_as_partitioned"
|
8
|
+
spec.version = ActsAsPartitioned::VERSION
|
9
|
+
spec.authors = ["Dan Draper"]
|
10
|
+
spec.email = ["daniel@codefire.com"]
|
11
|
+
spec.summary = %q{A plugin for active record}
|
12
|
+
spec.description = %q{Handle postgres style partitions in ActiveRecord}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
Binary file
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Partitioned
|
4
|
+
module Cache
|
5
|
+
class PartitionCache
|
6
|
+
def initialize(keys)
|
7
|
+
@pce_fact = PartitionCacheEntryFactory.new(keys)
|
8
|
+
@cache = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO: Add cache expiry
|
12
|
+
def add(partition)
|
13
|
+
@entry = @pce_fact.create
|
14
|
+
@entry.partition = partition
|
15
|
+
partition.key.each_pair do |key, value|
|
16
|
+
@entry.send("#{key}=", value)
|
17
|
+
end
|
18
|
+
found = self.find(partition.key)
|
19
|
+
@cache << @entry unless found
|
20
|
+
end
|
21
|
+
|
22
|
+
def find(hash)
|
23
|
+
res = @cache.find do |entry|
|
24
|
+
entry == hash
|
25
|
+
end
|
26
|
+
res ? res.partition : nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def size
|
30
|
+
@cache.size
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Partitioned
|
4
|
+
module Cache
|
5
|
+
class PartitionCacheEntry
|
6
|
+
attr_accessor :partition
|
7
|
+
|
8
|
+
def initialize(keys)
|
9
|
+
@keys = keys
|
10
|
+
@columns = keys.columns
|
11
|
+
end
|
12
|
+
|
13
|
+
def match(h)
|
14
|
+
hash = HashWithIndifferentAccess.new(h)
|
15
|
+
@columns.each do |column|
|
16
|
+
value = hash[column.to_sym]
|
17
|
+
raise "No value provided for #{column} (#{column.class})" unless value
|
18
|
+
unless match_instance(column, value)
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
true
|
23
|
+
end
|
24
|
+
alias :== :match
|
25
|
+
|
26
|
+
private
|
27
|
+
def match_instance(key, value)
|
28
|
+
compare_to = instance_variable_get("@#{key}")
|
29
|
+
compare_to === value || compare_to == value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Partitioned
|
4
|
+
module Cache
|
5
|
+
class PartitionCacheEntryFactory
|
6
|
+
def initialize(keys)
|
7
|
+
@keys = keys
|
8
|
+
end
|
9
|
+
|
10
|
+
def create
|
11
|
+
entry = PartitionCacheEntry.new(@keys)
|
12
|
+
sing = class << entry; self ; end
|
13
|
+
@keys.each do |key|
|
14
|
+
sing.send(:define_method, "#{key.column}=") { |arg|
|
15
|
+
instance_variable_set("@#{key.column}".to_sym, arg)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
entry
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'active_record/acts/partitioned/cache/partition_cache'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Acts
|
5
|
+
module Partitioned
|
6
|
+
class CopyProxy
|
7
|
+
def initialize(keys, factory)
|
8
|
+
@keys = keys
|
9
|
+
@factory = factory
|
10
|
+
@cache = Cache::PartitionCache.new(@keys)
|
11
|
+
end
|
12
|
+
|
13
|
+
# determine partition
|
14
|
+
# grab the partition keys from the hash (raise if missing)
|
15
|
+
# try to find an open copy file
|
16
|
+
# A copy file is linked to a partition - if we don't have one then should we fail or build a new part?
|
17
|
+
# if not create one
|
18
|
+
# expire old copy files
|
19
|
+
def <<(hash)
|
20
|
+
values = find_key_values(hash)
|
21
|
+
partition = @cache.find(values)
|
22
|
+
unless partition
|
23
|
+
# TODO: If there is no partition for then we need to create one
|
24
|
+
# We should provide a creation function - specifically how to create a partition with the desired key range
|
25
|
+
partition = @factory.find_for(hash)
|
26
|
+
raise "No partition for hash (#{hash.inspect})" unless partition
|
27
|
+
@cache.add(partition)
|
28
|
+
end
|
29
|
+
partition.copy_into << hash
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def find_key_values(hash)
|
34
|
+
values = {}
|
35
|
+
hash.each_pair do |key, value|
|
36
|
+
if @keys.columns.include?(key)
|
37
|
+
values[key] = value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
if values.keys.size < @keys.size
|
41
|
+
raise "Not all keys provided to copy data into partition: #{@keys.columns.join(',')} needed"
|
42
|
+
end
|
43
|
+
values
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
class CopyFile
|
49
|
+
# TODO: Make this a configurable option
|
50
|
+
::COPY_FILE_DIRECTORY = "/tmp/dumps/"
|
51
|
+
|
52
|
+
def initialize(table_name, options = {})
|
53
|
+
@table_name = table_name
|
54
|
+
@options = options
|
55
|
+
@header_written = false
|
56
|
+
@filename = generate_filename
|
57
|
+
@file = File.open(::COPY_FILE_DIRECTORY + @filename, "w")
|
58
|
+
#write_meta
|
59
|
+
end
|
60
|
+
|
61
|
+
def <<(hash)
|
62
|
+
unless @header_written
|
63
|
+
write_header(hash.keys)
|
64
|
+
end
|
65
|
+
# TODO: Write values
|
66
|
+
@file << hash.values.map { |v| quote_and_escape(v) }.join(',') << "\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
def close
|
70
|
+
@file.close
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def quote_and_escape(arg)
|
75
|
+
# TODO: Escape - see Adam's cortex code
|
76
|
+
"\"#{arg}\""
|
77
|
+
end
|
78
|
+
|
79
|
+
def write_meta
|
80
|
+
# TODO
|
81
|
+
end
|
82
|
+
|
83
|
+
def write_header(keys)
|
84
|
+
# TODO
|
85
|
+
@file << "COPY #{@table_name} (#{keys.join(',')}) FROM stdin with csv;\n"
|
86
|
+
@header_written = true
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_filename
|
90
|
+
str = "copy_"
|
91
|
+
str << @table_name
|
92
|
+
if @options.has_key?(:key)
|
93
|
+
str << "_#{@options[:key]}"
|
94
|
+
end
|
95
|
+
str << tmpstr
|
96
|
+
end
|
97
|
+
|
98
|
+
def tmpstr
|
99
|
+
str = ""
|
100
|
+
8.times do
|
101
|
+
str << ((rand * 25).to_i + 97).chr
|
102
|
+
end
|
103
|
+
str
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module Partitioned #:nodoc:
|
4
|
+
|
5
|
+
# TODO: Rename to Proxy
|
6
|
+
# TODO: If we were clever we would merge this with the Partiton AR model - can't merge as you need a proxy instance but we can move lots of methods over
|
7
|
+
class Factory
|
8
|
+
attr_reader :model, :partition_class
|
9
|
+
attr_reader :keys
|
10
|
+
delegate :find, :to => :partition_class
|
11
|
+
delegate :count, :to => :partition_class
|
12
|
+
delegate :with_key, :to => :partition_class
|
13
|
+
|
14
|
+
def initialize(model, partition_class, options = {})
|
15
|
+
@model = model
|
16
|
+
@keys = Keys.new
|
17
|
+
# TODO: Should we raise if we never add any keys?
|
18
|
+
@partition_class = partition_class
|
19
|
+
partition_class.factory = self
|
20
|
+
# TODO: Raise if model does not have key column(s)
|
21
|
+
@options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def partition_by(column, options = {})
|
25
|
+
# TODO: Raise if caller tries to partition on primary key
|
26
|
+
@keys << Key.new(column, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO: Prevent overlapping ranges
|
30
|
+
# TODO: Private?
|
31
|
+
def set_validations
|
32
|
+
# TODO: Move below this line to the partition class itself
|
33
|
+
@keys.each do |key|
|
34
|
+
case key.type
|
35
|
+
when :continuous
|
36
|
+
partition_class.validates_uniqueness_of("#{key.column}_begin", :scope => @keys.remaining_columns("#{key.column}_begin"))
|
37
|
+
partition_class.validates_uniqueness_of("#{key.column}_end", :scope => @keys.remaining_columns("#{key.column}_end"))
|
38
|
+
when :discrete
|
39
|
+
partition_class.validates_uniqueness_of(key.column, :scope => @keys.remaining_columns(key.column))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def init(options = {:force => false})
|
45
|
+
Structure.init_partition_catalog(model, @keys, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
def copy(filename, db_name = nil)
|
49
|
+
port, host, user, db = if db_name
|
50
|
+
config = ActiveRecord::Base.configurations[db_name.to_s]
|
51
|
+
raise "No such DB configuration: #{db_name}" unless config
|
52
|
+
[config['port'], config['host'], config['username'], config['database']]
|
53
|
+
else
|
54
|
+
conn = @model.connection.raw_connection
|
55
|
+
[conn.port, conn.host, conn.user, conn.db]
|
56
|
+
end
|
57
|
+
"psql --set ON_ERROR_STOP=1 --single-transaction -p #{port} -h #{host} -U #{user} #{db} < #{filename}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Arguments are the keys specified in creation as a hash
|
61
|
+
# eg: create(:date => Date.today, :domain => domain)
|
62
|
+
def create(key_hash)
|
63
|
+
# TODO: Raise if a key missing
|
64
|
+
@model.transaction do
|
65
|
+
partition = partition_class.create!(key_hash)
|
66
|
+
@keys.create_partition_tables(@model, :key_hash => key_hash)
|
67
|
+
# TODO: Indexes
|
68
|
+
partition
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def clear
|
73
|
+
partition_class.find(:all).each do |partition|
|
74
|
+
partition.drop!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Finds a partition to which these keys belong
|
79
|
+
# Not by keys used to create the partition
|
80
|
+
# This is the same thing for discrete keys
|
81
|
+
# but for continuous (ie; ranged keys)
|
82
|
+
# the end points of a range may not equal the values
|
83
|
+
# stored in the part
|
84
|
+
# Here we see if a value fits within the range
|
85
|
+
# Use this method if you want to know which partition
|
86
|
+
# to write data to
|
87
|
+
def find_for(_hash)
|
88
|
+
hash = _hash.symbolize_keys
|
89
|
+
conditions = {}
|
90
|
+
@keys.each do |key|
|
91
|
+
puts "key = #{key.inspect}"
|
92
|
+
value = hash[key.column.to_sym]
|
93
|
+
raise "No value provided for #{key.column}" unless value
|
94
|
+
case key.type
|
95
|
+
when :discrete
|
96
|
+
conditions[key.column.to_sym] = value
|
97
|
+
when :continuous
|
98
|
+
conditions[:"#{key.column}_begin"] = value.begin
|
99
|
+
conditions[:"#{key.column}_end"] = value.end
|
100
|
+
conditions[:"#{key.column}_exclusive"] = value.exclude_end?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
puts "conditions = #{conditions.inspect}"
|
104
|
+
partition_class.find_by(conditions)
|
105
|
+
end
|
106
|
+
|
107
|
+
def find_or_create_for(hash)
|
108
|
+
find_for(hash) || create(hash)
|
109
|
+
end
|
110
|
+
|
111
|
+
def dump_age
|
112
|
+
if @options[:dump_age].kind_of?(Proc)
|
113
|
+
@options[:dump_age].call || 0
|
114
|
+
else
|
115
|
+
@options[:dump_age] || 0
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def archive?
|
120
|
+
@options[:archive] || false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module Partitioned #:nodoc:
|
4
|
+
class Key
|
5
|
+
attr_accessor :column, :type, :using
|
6
|
+
|
7
|
+
def initialize(column, options = {})
|
8
|
+
@column = column.to_s
|
9
|
+
@type = options[:ranged] ? :continuous : :discrete
|
10
|
+
end
|
11
|
+
|
12
|
+
def column_names
|
13
|
+
case @type
|
14
|
+
when :continuous then ["#{@column}_begin", "#{@column}_end"]
|
15
|
+
when :discrete then [@column]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_partition_table(model, opts = {})
|
20
|
+
table = model.table_name.dup
|
21
|
+
table << "_part_" + opts[:parent].partition_handle(:key_hash => opts[:key_hash]) if opts.has_key?(:parent)
|
22
|
+
partition_name = "#{model.table_name}_part_#{partition_handle(opts)}"
|
23
|
+
unless model.connection.tables.include?(partition_name)
|
24
|
+
model.connection.execute(<<-SQL)
|
25
|
+
CREATE TABLE #{partition_name} (
|
26
|
+
CHECK (#{apply_check(opts[:key_hash]).join(' AND ')})
|
27
|
+
) INHERITS (#{table});
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def partition_handle(opts)
|
33
|
+
handle = []
|
34
|
+
handle << opts[:parent].partition_handle(:key_hash => opts[:key_hash]) if opts.has_key?(:parent)
|
35
|
+
value = opts[:key_hash][@column.to_sym]
|
36
|
+
handle << case @type
|
37
|
+
when :discrete then value
|
38
|
+
when :continuous then [ value.begin, value.end ]
|
39
|
+
end
|
40
|
+
handle.flatten.map { |value|
|
41
|
+
handle_partition_value(value)
|
42
|
+
}.join("_")
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def apply_check(key_hash)
|
47
|
+
value = key_hash[@column.to_sym]
|
48
|
+
unless value
|
49
|
+
raise "No value provided for key #{@column}, hash is #{key_hash.inspect}"
|
50
|
+
end
|
51
|
+
case @type
|
52
|
+
when :discrete then ["#{@column} = '#{value}'"]
|
53
|
+
when :continuous then ["#{@column} >= '#{value.begin}'", "#{@column} <#{'=' unless value.exclude_end?} '#{value.end}'"]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_partition_value(value)
|
58
|
+
case value
|
59
|
+
when Date,Time,Timestamp
|
60
|
+
value.strftime("%Y%m%d%H%M")
|
61
|
+
when String
|
62
|
+
handle_partition_value(value.to_time) rescue value
|
63
|
+
else value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module Partitioned #:nodoc:
|
4
|
+
class Keys < Array
|
5
|
+
def columns
|
6
|
+
self.map(&:column)
|
7
|
+
end
|
8
|
+
|
9
|
+
def column_names
|
10
|
+
self.map { |k| k.column_names }.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the list of column names excluding this one
|
14
|
+
def remaining_columns(column)
|
15
|
+
self.column_names - [column]
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_partition_tables(model, opts = {})
|
19
|
+
each_with_index do |key, index|
|
20
|
+
key_opts = index == 0 ? opts : opts.merge(:parent => self[index - 1])
|
21
|
+
key.create_partition_table(model, key_opts)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def partition_handle(opts)
|
26
|
+
map { |k| k.partition_handle(opts) }.join("_")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module Partitioned #:nodoc:
|
4
|
+
class Partition < ActiveRecord::Base
|
5
|
+
class_attribute :factory
|
6
|
+
|
7
|
+
# TODO: WHAT THE HELL??
|
8
|
+
def self.with_key(hash)
|
9
|
+
self.scoped(:conditions => modified_attrs(hash)).first
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.column_names
|
13
|
+
[]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns true if the hash values should be stored in this partition
|
17
|
+
def include?(hash)
|
18
|
+
self.key.each_pair do |key, value|
|
19
|
+
unless value === hash[key]
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(attrs = {}, options = {})
|
27
|
+
super(self.class.modified_attrs(attrs), options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def drop!
|
31
|
+
self.transaction do
|
32
|
+
if partition_exists?
|
33
|
+
self.connection.execute "DROP TABLE #{name}"
|
34
|
+
end
|
35
|
+
self.destroy
|
36
|
+
if num_siblings == 0
|
37
|
+
# Delete the parent
|
38
|
+
self.connection.execute "DROP TABLE #{parent_name}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def num_siblings
|
44
|
+
parent = self.class.factory.keys[-2] # second to last
|
45
|
+
if parent
|
46
|
+
# TODO: This won't handle a ranged parent yet
|
47
|
+
self.class.count(:conditions => { parent.column => attributes[parent.column] })
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Will unlink the partition from the parent table but not delete
|
52
|
+
def unlink
|
53
|
+
self.transaction do
|
54
|
+
self.class.factory.model.connection.execute <<-SQL
|
55
|
+
ALTER TABLE #{name} NO INHERIT #{self.class.factory.model.table_name};
|
56
|
+
ALTER TABLE #{name} RENAME TO #{name}_unlinked;
|
57
|
+
SQL
|
58
|
+
self.destroy
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def dump
|
63
|
+
conn = self.class.factory.model.connection.raw_connection
|
64
|
+
raise "Partition does not exist" unless partition_exists?
|
65
|
+
`pg_dump -h #{conn.host} -U #{conn.user} -t #{self.tablename} #{conn.db} | gzip`
|
66
|
+
end
|
67
|
+
|
68
|
+
def size
|
69
|
+
rec = self.class.find(:first, :select => "relpages", :from => "pg_class", :conditions => "relname = '#{self.tablename}'")
|
70
|
+
rec[:relpages].to_i * 8192
|
71
|
+
end
|
72
|
+
|
73
|
+
def name
|
74
|
+
"#{self.class.factory.model.table_name}_part_#{self.class.factory.keys.partition_handle(:key_hash => key)}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def parent_name
|
78
|
+
return nil unless self.class.factory.keys[-2]
|
79
|
+
"#{self.class.factory.model.table_name}_part_#{self.class.factory.keys[-2].partition_handle(:key_hash => key)}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def partition_exists?
|
83
|
+
self.connection.table_exists?(name)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Modify parameters to suit ranges if required
|
87
|
+
private
|
88
|
+
def self.modified_attrs(attrs)
|
89
|
+
hash = {}
|
90
|
+
attrs.each_pair do |key, value|
|
91
|
+
if value.instance_of?(Range)
|
92
|
+
hash["#{key}_begin"] = value.begin
|
93
|
+
hash["#{key}_end"] = value.end
|
94
|
+
hash["#{key}_exclusive"] = value.exclude_end?
|
95
|
+
else
|
96
|
+
hash[key] = value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
hash
|
100
|
+
end
|
101
|
+
|
102
|
+
def key
|
103
|
+
hash = HashWithIndifferentAccess.new
|
104
|
+
self.class.factory.keys.each do |k|
|
105
|
+
case k.type
|
106
|
+
when :continuous
|
107
|
+
r_start = self.send("#{k.column}_begin")
|
108
|
+
r_end = self.send("#{k.column}_end")
|
109
|
+
r_exclusive = self.send("#{k.column}_exclusive")
|
110
|
+
hash[k.column] = Range.new(r_start, r_end, r_exclusive)
|
111
|
+
when :discrete
|
112
|
+
hash[k.column] = self.send(k.column)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
hash
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Partitioned
|
4
|
+
class Structure < ActiveRecord::Migration
|
5
|
+
def self.init_partition_catalog(model, keys, options = {})
|
6
|
+
create_table("#{model.table_name}_partitions", :force => options[:force]) do |t|
|
7
|
+
puts "keys are '#{keys.inspect}'"
|
8
|
+
keys.each do |key|
|
9
|
+
case key.type
|
10
|
+
when :discrete
|
11
|
+
t.column key.column, determine_column_type(model, key.column)
|
12
|
+
when :continuous
|
13
|
+
t.column "#{key.column}_begin", determine_column_type(model, key.column)
|
14
|
+
t.column "#{key.column}_end", determine_column_type(model, key.column)
|
15
|
+
t.column "#{key.column}_exclusive", :boolean
|
16
|
+
end
|
17
|
+
end
|
18
|
+
# TODO: Add key columns and indexes
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.determine_column_type(model, column)
|
23
|
+
found = model.columns.detect { |c| c.name == column.to_s }
|
24
|
+
raise "No such column '#{column}' on model '#{model}'" unless found
|
25
|
+
found.type
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'active_record/acts/partitioned/key'
|
2
|
+
require 'active_record/acts/partitioned/keys'
|
3
|
+
require 'active_record/acts/partitioned/partition'
|
4
|
+
require 'active_record/acts/partitioned/factory'
|
5
|
+
require 'active_record/acts/partitioned/structure'
|
6
|
+
require 'active_record/acts/partitioned/copy_proxy'
|
7
|
+
|
8
|
+
# ActsAsPartitioned
|
9
|
+
module ActiveRecord
|
10
|
+
class Base
|
11
|
+
class << self
|
12
|
+
def partitioned_classes
|
13
|
+
@@subclasses[ActiveRecord::Base].select(&:partitioned?)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Acts #:nodoc:
|
19
|
+
module Partitioned #:nodoc:
|
20
|
+
class PartitionError < StandardError ; end
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.extend(ClassMethods)
|
24
|
+
base.class_attribute :partitions
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def partition(*args)
|
29
|
+
options = args.extract_options!
|
30
|
+
# TODO: Use an anonymous class
|
31
|
+
eval <<-EVAL
|
32
|
+
class ActiveRecord::Acts::Partitioned::#{self.name}Partition < ActiveRecord::Acts::Partitioned::Partition
|
33
|
+
self.table_name = '#{self.table_name}_partitions'
|
34
|
+
end
|
35
|
+
EVAL
|
36
|
+
klass = "ActiveRecord::Acts::Partitioned::#{self.name}Partition".constantize
|
37
|
+
factory = Factory.new(self, klass, options)
|
38
|
+
args.each { |arg| factory.partition_by(key) }
|
39
|
+
yield factory if block_given?
|
40
|
+
factory.set_validations
|
41
|
+
self.partitions = factory
|
42
|
+
|
43
|
+
# TODO: Put this in sep rake task and call on factory - should this be called Proxy
|
44
|
+
#factory.migrate(:force => true)
|
45
|
+
end
|
46
|
+
|
47
|
+
def partitions
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def partitioned?
|
52
|
+
self.partitions.present?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_partitioned
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Draper
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-11 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.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Handle postgres style partitions in ActiveRecord
|
42
|
+
email:
|
43
|
+
- daniel@codefire.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- Gemfile
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- acts_as_partitioned.gemspec
|
54
|
+
- lib/active_record/acts/partitioned.rb
|
55
|
+
- lib/active_record/acts/partitioned/.key.rb.swp
|
56
|
+
- lib/active_record/acts/partitioned/cache/partition_cache.rb
|
57
|
+
- lib/active_record/acts/partitioned/cache/partition_cache_entry.rb
|
58
|
+
- lib/active_record/acts/partitioned/cache/partition_cache_entry_factory.rb
|
59
|
+
- lib/active_record/acts/partitioned/copy_proxy.rb
|
60
|
+
- lib/active_record/acts/partitioned/factory.rb
|
61
|
+
- lib/active_record/acts/partitioned/key.rb
|
62
|
+
- lib/active_record/acts/partitioned/keys.rb
|
63
|
+
- lib/active_record/acts/partitioned/partition.rb
|
64
|
+
- lib/active_record/acts/partitioned/structure.rb
|
65
|
+
- lib/acts_as_partitioned.rb
|
66
|
+
- lib/acts_as_partitioned/version.rb
|
67
|
+
homepage: ''
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
metadata: {}
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 2.2.2
|
88
|
+
signing_key:
|
89
|
+
specification_version: 4
|
90
|
+
summary: A plugin for active record
|
91
|
+
test_files: []
|