valium 0.2.1

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.
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