delta_force 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'activerecord', '2.3.11'
data/Gemfile.lock ADDED
@@ -0,0 +1,33 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ delta_force (0.0.1)
5
+ activerecord (~> 2.3.0)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ activerecord (2.3.11)
11
+ activesupport (= 2.3.11)
12
+ activesupport (2.3.11)
13
+ columnize (0.3.2)
14
+ factory_girl (1.3.3)
15
+ linecache (0.43)
16
+ pg (0.10.1)
17
+ ruby-debug (0.10.4)
18
+ columnize (>= 0.1)
19
+ ruby-debug-base (~> 0.10.4.0)
20
+ ruby-debug-base (0.10.4)
21
+ linecache (>= 0.3)
22
+ shoulda (2.11.3)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ activerecord (= 2.3.11)
29
+ delta_force!
30
+ factory_girl
31
+ pg
32
+ ruby-debug
33
+ shoulda
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 by Joe Lind
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,80 @@
1
+ = DeltaForce
2
+
3
+ DeltaForce allows you to use some Postgres 8.4+ window functions in named scopes on your ActiveRecord models.
4
+
5
+ It is very much still a work in progress; please contribute if possible and let me know what I can do better!
6
+
7
+ == Installing
8
+
9
+ <b>DeltaForce only works with Rails 2.3</b>. I will be adding Rails 3 compatibility shortly.
10
+
11
+ In your Gemfile:
12
+
13
+ gem "delta_force"
14
+
15
+ == Example
16
+
17
+ Consider the following schema and model:
18
+
19
+ create_table "foos", :force => true do |t|
20
+ t.integer "bar_id"
21
+ t.float "x"
22
+ t.float "y"
23
+ t.float "z"
24
+ t.date "period"
25
+ end
26
+
27
+ class Foo < ActiveRecord::Base
28
+ tracks_changes_over_time do |changes|
29
+ changes.values :x, :y, :z, :over => :period, :by => :bar_id
30
+ end
31
+ end
32
+
33
+
34
+ You can now get the opening and closing values by bar_id as follows:
35
+ Foo.changes_in_x_and_y_and_z_by_bar_id_over_period
36
+
37
+ If the following Foo objects exist:
38
+
39
+ #<Foo id: 1, bar_id: 1, x: 1.0, y: 10.0, z: 100.0, period: "2011-03-11">
40
+ #<Foo id: 2, bar_id: 1, x: 2.0, y: 20.0, z: 200.0, period: "2011-03-10">
41
+ #<Foo id: 3, bar_id: 1, x: 3.0, y: 30.0, z: 300.0, period: "2011-03-09">
42
+ #<Foo id: 4, bar_id: 2, x: 4.0, y: 40.0, z: 400.0, period: "2011-03-08">
43
+ #<Foo id: 5, bar_id: 2, x: 5.0, y: 50.0, z: 500.0, period: "2011-03-07">
44
+ #<Foo id: 6, bar_id: 2, x: 6.0, y: 60.0, z: 600.0, period: "2011-03-06">
45
+ #<Foo id: 7, bar_id: 2, x: 7.0, y: 70.0, z: 700.0, period: "2011-03-05">
46
+
47
+ You can retrieve a set of modified Foo objects that have opening and closing values, partitioned by bar_id, for x, y, and z as follows:
48
+
49
+ Foo.changes_in_x_and_y_and_z_by_bar_id_over_period
50
+ => [#<Foo bar_id: 1>, #<Foo bar_id: 2>]
51
+
52
+ Foo.changes_in_x_and_y_and_z_by_bar_id_over_period.first.opening_x
53
+ => "3"
54
+
55
+ Foo.changes_in_x_and_y_and_z_by_bar_id_over_period.first.closing_x
56
+ => "1"
57
+
58
+ Foo.changes_in_x_and_y_and_z_by_bar_id_over_period.first.closing_y
59
+ => "10"
60
+
61
+ Foo.changes_in_x_and_y_and_z_by_bar_id_over_period.first.opening_y
62
+ => "30"
63
+
64
+ Foo.changes_in_x_and_y_and_z_by_bar_id_over_period.first.opening_period
65
+ => "2011-03-09"
66
+
67
+ Foo.changes_in_x_and_y_and_z_by_bar_id_over_period.first.closing_period
68
+ => "2011-03-11"
69
+
70
+ Note that opening_* and closing_* values are not currently typecast. I'd like to support that in the future.
71
+
72
+ == Under the hood
73
+
74
+ DeltaForce uses {Postgres 8.4+ window functions}[http://www.postgresql.org/docs/current/static/tutorial-window.html] to partition data by an arbitrary key.
75
+
76
+ This has been tested agains Postgres 8.4 and should also work with Postgres 9. Oracle provides the same set of functions, but I have not been able to test it against an Oracle instance yet.
77
+
78
+ == Copyright
79
+
80
+ Copyright (c) 2011 {Joe Lind}[http://github.com/joelind]. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ #require 'rspec/core/rake_task'
3
+ require 'rake/testtask'
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ Rake::TestTask.new(:test) do |test|
8
+ test.libs << 'test'
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "delta_force/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "delta_force"
7
+ s.version = DeltaForce::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Joe Lind"]
10
+ s.email = ["joelind@gmail.com"]
11
+ s.homepage = "http://github.com/joelind/delta_force"
12
+ s.summary = %q{ActiveRecord named scopes with Postgres window functions.}
13
+ s.description = %q{
14
+ DeltaForce lets you use Postgres 8.4+ window functions with ActiveRecord
15
+ }
16
+ s.post_install_message = %q{
17
+ *** Thanks for installing DeltaForce! ***
18
+ }
19
+
20
+ s.extra_rdoc_files = [
21
+ "LICENSE",
22
+ "README.rdoc"
23
+ ]
24
+
25
+ s.rubyforge_project = "delta_force"
26
+
27
+ s.add_dependency 'activerecord', '~> 2.3.0'
28
+ s.add_development_dependency 'shoulda'
29
+ s.add_development_dependency 'factory_girl'
30
+ s.add_development_dependency 'ruby-debug'
31
+ s.add_development_dependency 'pg'
32
+
33
+ s.files = `git ls-files`.split("\n")
34
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
35
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
36
+ s.require_paths = ["lib"]
37
+ end
@@ -0,0 +1,97 @@
1
+
2
+ module DeltaForce
3
+ class ChangeProxy
4
+
5
+ attr_reader :klass
6
+
7
+ def initialize(klass)
8
+ @klass = klass
9
+ end
10
+
11
+ def values(*value_fields)
12
+ options = value_fields.extract_options!.symbolize_keys
13
+
14
+ partition_column = "#{table_name}.#{options[:by].to_s}"
15
+ id_column = "#{table_name}.id"
16
+
17
+ period_field_name = options[:over].to_s
18
+ period_column = "#{table_name}.#{period_field_name}"
19
+
20
+ scope_name = options[:scope_name] || default_scope_name(value_fields, options)
21
+
22
+ window = "
23
+ (
24
+ PARTITION BY #{partition_column}
25
+ ORDER BY #{period_column} DESC, #{id_column} DESC ROWS
26
+ BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
27
+ )"
28
+
29
+ value_expressions = value_fields.collect do |value_field|
30
+ value_field_name = value_field.to_s
31
+ value_column = "#{table_name}.#{value_field_name}"
32
+
33
+ [
34
+ "last_value(#{value_column}) over #{window} as opening_#{value_field_name}",
35
+ "first_value(#{value_column}) over #{window} as closing_#{value_field_name}"
36
+ ]
37
+ end.flatten
38
+
39
+ klass.named_scope scope_name, :select => "distinct #{partition_column},
40
+ last_value(#{period_column}) over #{window} as opening_#{period_field_name},
41
+ first_value(#{period_column}) over #{window} as closing_#{period_field_name},
42
+ #{value_expressions.join ','}
43
+ "
44
+ end
45
+
46
+ alias :value :values
47
+
48
+ private
49
+
50
+ def default_scope_name(value_fields, options)
51
+ value_fields = value_fields.map(&:to_s).sort
52
+ period = options[:over].to_s
53
+ partition = options[:by].to_s
54
+
55
+ value_fields = value_fields.to_sentence(:words_connector => '_and_',
56
+ :two_words_connector => '_and_',
57
+ :last_word_connector => '_and_')
58
+
59
+ "changes_in_#{value_fields}_by_#{partition}_over_#{period}"
60
+ end
61
+
62
+ def table_name
63
+ klass.table_name
64
+ end
65
+
66
+ def calculate_changes(value_field_name, options = {})
67
+ options = options.symbolize_keys
68
+
69
+ value_field_name = value_field_name.to_s
70
+
71
+ partition_field_name = options[:partition_by]
72
+ partition_column = "#{table_name}.#{partition_field_name.to_s}"
73
+
74
+ value_column = "#{table_name}.#{value_field_name}"
75
+ id_column = "#{table_name}.id"
76
+
77
+ period_field_name = options[:period] || 'period'
78
+ period_column = "#{table_name}.#{period_field_name}"
79
+
80
+ scope_name = "changes_in_#{value_field_name}".to_sym
81
+
82
+ window = "
83
+ (
84
+ PARTITION BY #{partition_column}
85
+ ORDER BY #{period_column} DESC, #{id_column} DESC ROWS
86
+ BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
87
+ )"
88
+
89
+ named_scope scope_name, :select => "distinct #{partition_column},
90
+ last_value(#{period_column}) over #{window} as opening_#{period_field_name},
91
+ first_value(#{period_column}) over #{window} as closing_#{period_field_name},
92
+ last_value(#{value_column}) over #{window} as opening_#{value_field_name},
93
+ first_value(#{value_column}) over #{window} as closing_#{value_field_name}
94
+ "
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,41 @@
1
+ require 'delta_force/change_proxy'
2
+
3
+ module DeltaForce
4
+ module ClassMethods
5
+
6
+ def tracks_changes_over_time
7
+ yield DeltaForce::ChangeProxy.new(self)
8
+ end
9
+
10
+ def calculates_changes_in(value_field_name, options = {})
11
+ options = options.symbolize_keys
12
+
13
+ value_field_name = value_field_name.to_s
14
+
15
+ partition_field_name = options[:partition_by]
16
+ partition_column = "#{table_name}.#{partition_field_name.to_s}"
17
+
18
+ value_column = "#{table_name}.#{value_field_name}"
19
+ id_column = "#{table_name}.id"
20
+
21
+ period_field_name = options[:period] || 'period'
22
+ period_column = "#{table_name}.#{period_field_name}"
23
+
24
+ scope_name = "changes_in_#{value_field_name}".to_sym
25
+
26
+ window = "
27
+ (
28
+ PARTITION BY #{partition_column}
29
+ ORDER BY #{period_column} DESC, #{id_column} DESC ROWS
30
+ BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
31
+ )"
32
+
33
+ named_scope scope_name, :select => "distinct #{partition_column},
34
+ last_value(#{period_column}) over #{window} as opening_#{period_field_name},
35
+ first_value(#{period_column}) over#{window} as closing_#{period_field_name},
36
+ last_value(#{value_column}) over #{window} as opening_#{value_field_name},
37
+ first_value(#{value_column}) over #{window} as closing_#{value_field_name}
38
+ "
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module DeltaForce
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'delta_force/class_methods'
2
+
3
+ module DeltaForce
4
+ end
5
+
6
+ ActiveRecord::Base.extend DeltaForce::ClassMethods
@@ -0,0 +1,2 @@
1
+ Factory.define :bar do |bar|
2
+ end
@@ -0,0 +1,7 @@
1
+ Factory.define :foo do |foo|
2
+ foo.sequence(:period) { |n| n.days.ago.to_date }
3
+ foo.association(:bar)
4
+ foo.sequence(:x) { |n| n }
5
+ foo.sequence(:y) { |n| 10 * n }
6
+ foo.sequence(:z) { |n| 100 * n }
7
+ end
@@ -0,0 +1,3 @@
1
+ class Bar < ActiveRecord::Base
2
+ has_many :foos
3
+ end
@@ -0,0 +1,3 @@
1
+ class Foo < ActiveRecord::Base
2
+ belongs_to :bar
3
+ end
@@ -0,0 +1,12 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table "bars", :force => true
4
+
5
+ create_table "foos", :force => true do |t|
6
+ t.integer "bar_id"
7
+ t.float "x"
8
+ t.float "y"
9
+ t.float "z"
10
+ t.date "period"
11
+ end
12
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,30 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+ Bundler.setup
4
+ require 'test/unit'
5
+ require 'shoulda'
6
+ require 'factory_girl'
7
+ require 'active_record'
8
+ require 'delta_force'
9
+
10
+ FIXTURES_PATH = File.join(File.dirname(__FILE__), 'fixtures')
11
+
12
+ ActiveRecord::Base.establish_connection(
13
+ :adapter => 'postgresql',
14
+ :database => 'delta_force_test'
15
+ )
16
+
17
+ dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies
18
+ dep.autoload_paths.unshift FIXTURES_PATH
19
+
20
+ ActiveRecord::Base.silence do
21
+ ActiveRecord::Migration.verbose = false
22
+ load File.join(FIXTURES_PATH, 'schema.rb')
23
+ end
24
+
25
+ Dir[File.expand_path(File.dirname(__FILE__)) + "/factories/*.rb"].each do |file|
26
+ require file
27
+ end
28
+
29
+ class Test::Unit::TestCase
30
+ end
@@ -0,0 +1,71 @@
1
+ require 'helper'
2
+
3
+ class TestActiveRecordIntegration < Test::Unit::TestCase
4
+ context 'tracks_changes_over_time' do
5
+ setup do
6
+ Foo.tracks_changes_over_time do |changes|
7
+ changes.values :x, :y, :z, :over => :period, :by => :bar_id
8
+ end
9
+ end
10
+
11
+ should 'add a changes_in_* named scope' do
12
+ assert Foo.respond_to?(:changes_in_x_and_y_and_z_by_bar_id_over_period)
13
+ end
14
+
15
+ context 'with foos for a given bar' do
16
+ setup do
17
+ @bar = Factory(:bar)
18
+
19
+ @foos = 3.times.collect do |i|
20
+ 2.times.collect do
21
+ Factory :foo, :bar => @bar, :period => i.days.from_now.to_date,
22
+ :x=> (10 * i), :y => (100 * i), :z => (1000 * i)
23
+ end
24
+ end.flatten
25
+
26
+ #decoy foo
27
+ Factory :foo
28
+ end
29
+
30
+ context 'bar.foos.changes_in_x_and_y_and_z_by_bar_id_over_period' do
31
+ subject { @bar.foos.changes_in_x_and_y_and_z_by_bar_id_over_period }
32
+
33
+ should 'return a scope containig one object' do
34
+ assert_equal 1, subject.all.size
35
+ end
36
+
37
+ should 'return an object with opening_period' do
38
+ assert_equal @foos.first.period, subject.first.opening_period.to_date
39
+ end
40
+
41
+ should 'return an object with closing_period' do
42
+ assert_equal @foos.last.period, subject.first.closing_period.to_date
43
+ end
44
+
45
+ should 'return an object with closing_x' do
46
+ assert_equal @foos.last.x, subject.first.closing_x.to_f
47
+ end
48
+
49
+ should 'return an object with opening_x' do
50
+ assert_equal @foos.first.x, subject.first.opening_x.to_f
51
+ end
52
+
53
+ should 'return an object with closing_y' do
54
+ assert_equal @foos.last.y, subject.first.closing_y.to_f
55
+ end
56
+
57
+ should 'return an object with opening_y' do
58
+ assert_equal @foos.first.y, subject.first.opening_y.to_f
59
+ end
60
+
61
+ should 'return an object with closing_z' do
62
+ assert_equal @foos.last.z, subject.first.closing_z.to_f
63
+ end
64
+
65
+ should 'return an object with opening_z' do
66
+ assert_equal @foos.first.z, subject.first.opening_z.to_f
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: delta_force
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Joe Lind
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-12 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 2
32
+ - 3
33
+ - 0
34
+ version: 2.3.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: shoulda
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: factory_girl
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ type: :development
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: ruby-debug
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ type: :development
78
+ version_requirements: *id004
79
+ - !ruby/object:Gem::Dependency
80
+ name: pg
81
+ prerelease: false
82
+ requirement: &id005 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ type: :development
92
+ version_requirements: *id005
93
+ description: "\n DeltaForce lets you use Postgres 8.4+ window functions with ActiveRecord\n "
94
+ email:
95
+ - joelind@gmail.com
96
+ executables: []
97
+
98
+ extensions: []
99
+
100
+ extra_rdoc_files:
101
+ - LICENSE
102
+ - README.rdoc
103
+ files:
104
+ - .gitignore
105
+ - Gemfile
106
+ - Gemfile.lock
107
+ - LICENSE
108
+ - README.rdoc
109
+ - Rakefile
110
+ - delta_force.gemspec
111
+ - lib/delta_force.rb
112
+ - lib/delta_force/change_proxy.rb
113
+ - lib/delta_force/class_methods.rb
114
+ - lib/delta_force/version.rb
115
+ - test/factories/bar.rb
116
+ - test/factories/foo.rb
117
+ - test/fixtures/bar.rb
118
+ - test/fixtures/foo.rb
119
+ - test/fixtures/schema.rb
120
+ - test/helper.rb
121
+ - test/test_active_record_integration.rb
122
+ has_rdoc: true
123
+ homepage: http://github.com/joelind/delta_force
124
+ licenses: []
125
+
126
+ post_install_message: |
127
+
128
+ *** Thanks for installing DeltaForce! ***
129
+
130
+ rdoc_options: []
131
+
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ hash: 3
140
+ segments:
141
+ - 0
142
+ version: "0"
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ hash: 3
149
+ segments:
150
+ - 0
151
+ version: "0"
152
+ requirements: []
153
+
154
+ rubyforge_project: delta_force
155
+ rubygems_version: 1.3.7
156
+ signing_key:
157
+ specification_version: 3
158
+ summary: ActiveRecord named scopes with Postgres window functions.
159
+ test_files:
160
+ - test/factories/bar.rb
161
+ - test/factories/foo.rb
162
+ - test/fixtures/bar.rb
163
+ - test/fixtures/foo.rb
164
+ - test/fixtures/schema.rb
165
+ - test/helper.rb
166
+ - test/test_active_record_integration.rb