valium 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "http://rubygems.org"
2
+ gemspec
3
+
4
+ rails = ENV['RAILS'] || 'master'
5
+
6
+ case rails
7
+ when /\// # A path
8
+ path rails do
9
+ gem 'activerecord'
10
+ end
11
+ when /^v/ # A tagged version
12
+ git 'git://github.com/rails/rails.git', :tag => rails do
13
+ gem 'activerecord'
14
+ end
15
+ else
16
+ git 'git://github.com/rails/rails.git', :branch => rails do
17
+ gem 'activerecord'
18
+ end
19
+ end
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Valium
2
+
3
+ Suffering from ActiveRecord instantiation anxiety? Try Valium. It
4
+ saves your CPU and memory for more important things, retrieving
5
+ just the values you're interested in seeing.
6
+
7
+ ## Usage
8
+
9
+ ### In your Gemfile:
10
+
11
+ gem 'valium'
12
+
13
+ ### In your code:
14
+
15
+ You can select a single value...
16
+
17
+ ```ruby
18
+ Post.where(:published => true)[:title]
19
+ # - OR -
20
+ Post.where(:published => true).value_of :title
21
+ # => ["First Post", "Another Awesome Post", ...]
22
+ ```
23
+
24
+ ... or several ...
25
+
26
+ ```ruby
27
+ Employee.where(:title => 'Sr. Monkey Wrangler')[:first_name, :last_name, :hired_at]
28
+ # - OR -
29
+ Employee.where(:title => 'Sr. Monkey Wrangler').value_of :first_name, :last_name, :hired_at
30
+ # => [["Ernie", "Miller", 2009-09-21 08:00:00 -0400],
31
+ ["Herb", "Myers", 2002-02-13 09:00:00 -0400], ...]
32
+ ```
33
+
34
+ Values returned by Valium will be the data types you'd expect, just
35
+ as though you instantiated the ActiveRecord object and used the
36
+ accessor. This includes serialized attributes:
37
+
38
+ ```ruby
39
+ class Animal < ActiveRecord::Base
40
+ serialize :extra_info
41
+ end
42
+
43
+ Animal.where(:genus => 'felis')[:species, :extra_info]
44
+ # => [["catus", {:domestic => true}], ["lolcatus", {:can_has_cheezburger => true}], ...]
45
+ ```
46
+
47
+ ## Why would I use this?
48
+
49
+ It's not uncommon for Rails apps to need only one or two attributes
50
+ from a bunch of ActiveRecord objects. They'll have code like this:
51
+
52
+ ```ruby
53
+ MyModel.some_scope.map(&:id)
54
+ ```
55
+
56
+ Or, if the developer is a bit more clever about saving memory, he
57
+ might use code like this:
58
+
59
+ ```ruby
60
+ MyModel.some_scope.select(:id).map(&:id)
61
+ ```
62
+
63
+ This helps a good deal with memory usage, but even if we cut down
64
+ on the memory usage a bit, the truth is that no matter what we try,
65
+ **instantiating ActiveRecord objects is slow.**
66
+
67
+ This is because ActiveRecord provides all kinds of awesome stuff,
68
+ none of which we need, if we're just looking to grab one or two
69
+ values and print them out, or perform a quick calculation, or
70
+ something.
71
+
72
+ Check out [this gist of a benchmark script and results](https://gist.github.com/1166964)
73
+ to see just how much it can hurt when you're instantiating
74
+ unnecessary ActiveRecord objects.
75
+
76
+ TL;DR: It hurts *a lot*. For retrieving a single value, using Valium
77
+ is nearly 10x faster than mapping over ActiveRecord objects.
78
+ For multiple values, it's about 5x faster. Even if you're
79
+ deserializing attributes, where more time, proportionally, gets
80
+ spent in the deserialization process than with normal attributes,
81
+ Valium can be up to twice as fast as mapping over ActiveRecord
82
+ objects, in my tests.
83
+
84
+ ## Conclusion
85
+
86
+ You knew everything I mentioned above, already. In fact,
87
+ you're probably mocking me right now:
88
+
89
+ "ZOMG OBJECTS USE MEMORY AND INSTANTIATION TAKES CPU CYCLES!
90
+ I R GENIUS!"
91
+
92
+ Yeah. It's not rocket science. This is just a quick little
93
+ (seriously, around 100 LOC) gem that provides some intuitive
94
+ syntax around a common pattern and doesn't stomp on any
95
+ existing ActiveRecord functionality. It "just works."
96
+
97
+ Give it a try. Your code will thank you for it.
98
+
99
+ ## Copyright
100
+
101
+ Copyright &copy; 2011 [Ernie Miller](http://twitter.com/erniemiller)
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |rspec|
5
+ rspec.rspec_opts = ['--backtrace']
6
+ end
7
+
8
+ task :default => :spec
9
+
10
+ desc "Open an irb session with Valium and the sample data used in specs"
11
+ task :console do
12
+ require 'irb'
13
+ require 'irb/completion'
14
+ require 'console'
15
+ ARGV.clear
16
+ IRB.start
17
+ end
data/lib/valium.rb ADDED
@@ -0,0 +1,105 @@
1
+ require "valium/version"
2
+ require 'active_record'
3
+
4
+ module Valium
5
+ if ActiveRecord::VERSION::MAJOR == 3
6
+
7
+ if ActiveRecord::VERSION::MINOR == 0 # We need to use the old deserialize code
8
+
9
+ def valium_deserialize(value, klass)
10
+ if value.is_a?(String) && value =~ /^---/
11
+ result = YAML::load(value) rescue value
12
+ if result.nil? || result.is_a?(klass)
13
+ result
14
+ else
15
+ raise SerializationTypeMismatch,
16
+ "Expected a #{klass}, but was a #{result.class}"
17
+ end
18
+ else
19
+ value
20
+ end
21
+ end
22
+
23
+ else # we're on 3.1+, yay for coder.load!
24
+
25
+ def valium_deserialize(value, coder)
26
+ coder.load(value)
27
+ end
28
+
29
+ end # Minor version check
30
+
31
+ def value_of(*attr_names)
32
+ attr_names.map! do |attr_name|
33
+ attr_name = attr_name.to_s
34
+ attr_name == 'id' ? primary_key : attr_name
35
+ end
36
+
37
+ if attr_names.size > 1
38
+ valium_select_multiple(attr_names)
39
+ else
40
+ valium_select_one(attr_names.first)
41
+ end
42
+ end
43
+
44
+ alias :[] :value_of
45
+
46
+ def valium_select_multiple(attr_names)
47
+ columns = attr_names.map {|n| columns_hash[n]}
48
+ coders = attr_names.map {|n| serialized_attributes[n]}
49
+
50
+ connection.select_rows(
51
+ except(:select).select(attr_names.map {|n| arel_table[n]}).to_sql
52
+ ).map! do |values|
53
+ values.each_with_index do |value, index|
54
+ values[index] = valium_cast(value, columns[index], coders[index])
55
+ end
56
+ end
57
+ end
58
+
59
+ def valium_select_one(attr_name)
60
+ column = columns_hash[attr_name]
61
+ coder = serialized_attributes[attr_name]
62
+
63
+ connection.select_rows(
64
+ except(:select).select(arel_table[attr_name]).to_sql
65
+ ).map! do |values|
66
+ valium_cast(values[0], column, coder)
67
+ end
68
+ end
69
+
70
+ def valium_cast(value, column, coder_or_klass)
71
+ if value.nil? || !column
72
+ value
73
+ elsif coder_or_klass
74
+ valium_deserialize(value, coder_or_klass)
75
+ else
76
+ column.type_cast(value)
77
+ end
78
+ end
79
+
80
+ module Relation
81
+ def value_of(*args)
82
+ if args.size > 0 && args.all? {|a| String === a || Symbol === a}
83
+ args.map! do |attr_name|
84
+ attr_name = attr_name.to_s
85
+ attr_name == 'id' ? @klass.primary_key : attr_name
86
+ end
87
+
88
+ if loaded? && (empty? || args.all? {|a| first.attributes.has_key? a})
89
+ to_a.map {|record| args.map {|a| record[a]}}
90
+ else
91
+ scoping { @klass[*args] }
92
+ end
93
+ else
94
+ to_a[*args]
95
+ end
96
+ end
97
+
98
+ alias :[] :value_of
99
+ end
100
+
101
+ end # Major version check
102
+ end
103
+
104
+ ActiveRecord::Base.extend Valium
105
+ ActiveRecord::Relation.send :include, Valium::Relation
@@ -0,0 +1,3 @@
1
+ module Valium
2
+ VERSION = "0.2.1"
3
+ end
data/spec/console.rb ADDED
@@ -0,0 +1,7 @@
1
+ Dir[File.expand_path('../../spec/{helpers,support}/*.rb', __FILE__)].each do |f|
2
+ require f
3
+ end
4
+
5
+ Schema.create
6
+
7
+ require 'valium'
@@ -0,0 +1,9 @@
1
+ module ValiumHelper
2
+ def queries_for
3
+ $queries_executed = []
4
+ yield
5
+ $queries_executed
6
+ ensure
7
+ %w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) }
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ require 'active_record'
2
+
3
+ module ActiveRecord
4
+ class SQLCounter
5
+ IGNORED_SQL = [/^PRAGMA /, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/,
6
+ /SELECT name\s+FROM sqlite_master\s+WHERE type = 'table' AND NOT name = 'sqlite_sequence'/]
7
+
8
+ # FIXME: this needs to be refactored so specific database can add their own
9
+ # ignored SQL. This ignored SQL is for Oracle.
10
+ IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
11
+
12
+ def initialize
13
+ $queries_executed = []
14
+ end
15
+
16
+ def call(name, start, finish, message_id, values)
17
+ sql = values[:sql]
18
+
19
+ unless 'CACHE' == values[:name]
20
+ $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
21
+ end
22
+ end
23
+ end
24
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
25
+ end
26
+
27
+ Dir[File.expand_path('../{helpers,support}/*.rb', __FILE__)].each do |f|
28
+ require f
29
+ end
30
+
31
+ RSpec.configure do |config|
32
+ config.before(:suite) do
33
+ puts '=' * 80
34
+ puts "Running specs against ActiveRecord #{ActiveRecord::VERSION::STRING}..."
35
+ puts '=' * 80
36
+ Schema.create
37
+ end
38
+
39
+ config.include ValiumHelper
40
+ end
41
+
42
+ require 'valium'
@@ -0,0 +1,50 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.establish_connection(
4
+ :adapter => 'sqlite3',
5
+ :database => ':memory:'
6
+ )
7
+
8
+ class Person < ActiveRecord::Base
9
+ serialize :extra_info
10
+ end
11
+
12
+ class Widget < ActiveRecord::Base
13
+ serialize :extra_info
14
+ set_primary_key :widget_id
15
+ end
16
+
17
+ module Schema
18
+ def self.create
19
+ ActiveRecord::Base.silence do
20
+ ActiveRecord::Migration.verbose = false
21
+
22
+ ActiveRecord::Schema.define do
23
+ create_table :people, :force => true do |t|
24
+ t.string :first_name
25
+ t.string :last_name
26
+ t.integer :age
27
+ t.text :extra_info
28
+ t.timestamps
29
+ end
30
+
31
+ create_table :widgets, :force => true, :primary_key => :widget_id do |t|
32
+ t.string :name
33
+ t.text :extra_info
34
+ t.timestamps
35
+ end
36
+ end
37
+ end
38
+
39
+ 1.upto(100) do |num|
40
+ Person.create! :first_name => "Person", :last_name => "Number#{num}", :age => num % 99,
41
+ :extra_info => {:a_key => "Value Number #{num}"}
42
+ end
43
+
44
+ 1.upto(100) do |num|
45
+ Widget.create! :name => "Widget #{num}",
46
+ :extra_info => {:a_key => "Value Number #{num}"}
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,167 @@
1
+ require 'spec_helper'
2
+
3
+ describe Valium do
4
+
5
+ context 'with a symbol key' do
6
+ subject { Person[:id] }
7
+ it { should have(100).ids }
8
+ it { should eq((1..100).to_a) }
9
+ end
10
+
11
+ context 'with value_of syntax' do
12
+ subject { Person.value_of :id }
13
+ it { should have(100).ids }
14
+ it { should eq((1..100).to_a) }
15
+ end
16
+
17
+ context 'with a string key' do
18
+ subject { Person['id'] }
19
+ it { should have(100).ids }
20
+ it { should eq((1..100).to_a) }
21
+ end
22
+
23
+ context 'with multiple keys' do
24
+ subject { Person[:id, :last_name] }
25
+ it { should have(100).elements }
26
+ it { should eq((1..100).map {|n| [n, "Number#{n}"]})}
27
+ end
28
+
29
+ context 'with a datetime column' do
30
+ subject { Person[:created_at] }
31
+ it { should have(100).datetimes }
32
+ it { should be_all {|d| Time === d}}
33
+ end
34
+
35
+ context 'with a serialized column' do
36
+ subject { Person[:extra_info] }
37
+ it { should have(100).hashes }
38
+ it { should eq 1.upto(100).map {|n| {:a_key => "Value Number #{n}"} }}
39
+ end
40
+
41
+ context 'with an alternate primary key and an :id select' do
42
+ subject { Widget[:id] }
43
+ it { should have(100).ids }
44
+ it { should eq (1..100).to_a}
45
+ end
46
+
47
+ context 'with an alternate primary key and an alternate primary key select' do
48
+ subject { Widget[:widget_id] }
49
+ it { should have(100).ids }
50
+ it { should eq (1..100).to_a}
51
+ end
52
+
53
+ context 'with a scope' do
54
+ subject { Person.where(:id => [1,50,100])[:last_name] }
55
+ it { should have(3).last_names }
56
+ it { should eq ['Number1', 'Number50', 'Number100'] }
57
+ end
58
+
59
+ context 'with a scope and value_of syntax' do
60
+ subject { Person.where(:id => [1,50,100]).value_of :id }
61
+ it { should have(3).ids }
62
+ it { should eq [1,50,100] }
63
+ end
64
+
65
+ context 'with a scope, an alternate primary key, and an :id select' do
66
+ subject {Widget.where(:widget_id => [1,50,100])[:id] }
67
+ it { should have(3).ids }
68
+ it { should eq [1,50,100]}
69
+ end
70
+
71
+ context 'with a scope, an alternate primary key, and an alternate primary key select' do
72
+ subject { Widget.where(:widget_id => [1,50,100])[:widget_id] }
73
+ it { should have(3).widget_ids }
74
+ it { should eq [1,50,100]}
75
+ end
76
+
77
+ context 'with a loaded scope' do
78
+ subject do
79
+ Person.where(:id => [1,50,100]).tap do |relation|
80
+ relation.all
81
+ end
82
+ end
83
+
84
+ # We'll generate the first query when we call "subject", but won't
85
+ # need another query
86
+ specify { queries_for { subject[:id] }.should have(1).query }
87
+
88
+ specify { subject[:id, :created_at, :extra_info].
89
+ should eq Person.where(:id => [1,50,100])[:id, :created_at, :extra_info] }
90
+ end
91
+
92
+ context 'with a loaded scope, an alternate primary key, and an :id select' do
93
+ subject do
94
+ Widget.where(:widget_id => [1,50,100]).tap do |relation|
95
+ relation.all
96
+ end
97
+ end
98
+
99
+ # We'll generate the first query when we call "subject", but won't
100
+ # need another query
101
+ specify { queries_for { subject[:id] }.should have(1).query }
102
+
103
+ specify { subject[:id, :created_at, :extra_info].
104
+ should eq Widget.where(:widget_id => [1,50,100])[:id, :created_at, :extra_info] }
105
+ end
106
+
107
+ context 'with a loaded scope but missing attributes' do
108
+ subject do
109
+ Person.select(:id).where(:id => [1,50,100]).tap do |relation|
110
+ relation.all
111
+ end
112
+ end
113
+
114
+ # We'll generate the first query when we call "subject", but won't
115
+ # need another query
116
+ specify { queries_for { subject[:id] }.should have(1).query }
117
+
118
+ # We'll need to run our own query for the attributes
119
+ specify { queries_for { subject[:first_name] }.should have(2).queries }
120
+
121
+ specify { subject[:id, :created_at, :extra_info].
122
+ should eq Person.where(:id => [1,50,100])[:id, :created_at, :extra_info] }
123
+ end
124
+
125
+ context 'with a loaded scope, an alternate primary key, and missing attributes' do
126
+ subject do
127
+ Widget.select(:widget_id).where(:widget_id => [1,50,100]).tap do |relation|
128
+ relation.all
129
+ end
130
+ end
131
+
132
+ # We'll generate the first query when we call "subject", but won't
133
+ # need another query
134
+ specify { queries_for { subject[:id] }.should have(1).query }
135
+ specify { queries_for { subject[:widget_id] }.should have(1).query }
136
+
137
+ # We'll need to run our own query for the attributes
138
+ specify { queries_for { subject[:name] }.should have(2).queries }
139
+
140
+ specify { subject[:id, :created_at, :extra_info].
141
+ should eq Widget.where(:widget_id => [1,50,100])[:id, :created_at, :extra_info] }
142
+ end
143
+
144
+ context 'with a scope and multiple keys' do
145
+ subject { Person.where(:id => [1,50,100])[:last_name, :id, :extra_info] }
146
+ it { should have(3).elements }
147
+ it { should eq [1,50,100].map {|n| ["Number#{n}", n, {:a_key => "Value Number #{n}"}]}}
148
+ end
149
+
150
+ context 'with a relation array index' do
151
+ subject { Person.where(:id => [1,50,100])[1] }
152
+ it { should eq Person.find(50) }
153
+ end
154
+
155
+ context 'with a relation array start and length' do
156
+ subject { Person.where(:id => 1..20)[10,3] }
157
+ it { should have(3).people }
158
+ it { should eq Person.offset(10).limit(3) }
159
+ end
160
+
161
+ context 'with a relation array range' do
162
+ subject { Person.where(:id => 1..20)[0..9] }
163
+ it { should have(10).people }
164
+ it { should eq Person.first(10) }
165
+ end
166
+
167
+ end
data/valium.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "valium/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "valium"
7
+ s.version = Valium::VERSION
8
+ s.authors = ["Ernie Miller"]
9
+ s.email = ["ernie@metautonomo.us"]
10
+ s.homepage = "http://github.com/ernie/valium"
11
+ s.summary = %q{
12
+ Access attribute values directly, without instantiating ActiveRecord objects.
13
+ }
14
+ s.description = %q{
15
+ Suffering from ActiveRecord instantiation anxiety? Try Valium. It
16
+ saves your CPU and memory for more important things, retrieving
17
+ just the values you're interested in seeing.
18
+ }
19
+
20
+ s.rubyforge_project = "valium"
21
+
22
+ s.add_dependency 'activerecord', '>= 3.0.2'
23
+ s.add_development_dependency 'rspec', '~> 2.6.0'
24
+ s.add_development_dependency 'sqlite3', '~> 1.3.3'
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
+ s.require_paths = ["lib"]
30
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: valium
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ernie Miller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-25 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &70159201437300 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70159201437300
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70159201401980 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 2.6.0
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70159201401980
36
+ - !ruby/object:Gem::Dependency
37
+ name: sqlite3
38
+ requirement: &70159201388400 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 1.3.3
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70159201388400
47
+ description: ! "\n Suffering from ActiveRecord instantiation anxiety? Try Valium.
48
+ It\n saves your CPU and memory for more important things, retrieving\n just
49
+ the values you're interested in seeing.\n "
50
+ email:
51
+ - ernie@metautonomo.us
52
+ executables: []
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - .gitignore
57
+ - Gemfile
58
+ - README.md
59
+ - Rakefile
60
+ - lib/valium.rb
61
+ - lib/valium/version.rb
62
+ - spec/console.rb
63
+ - spec/helpers/valium_helper.rb
64
+ - spec/spec_helper.rb
65
+ - spec/support/schema.rb
66
+ - spec/valium/valium_spec.rb
67
+ - valium.gemspec
68
+ homepage: http://github.com/ernie/valium
69
+ licenses: []
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project: valium
88
+ rubygems_version: 1.8.6
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: Access attribute values directly, without instantiating ActiveRecord objects.
92
+ test_files:
93
+ - spec/console.rb
94
+ - spec/helpers/valium_helper.rb
95
+ - spec/spec_helper.rb
96
+ - spec/support/schema.rb
97
+ - spec/valium/valium_spec.rb