columns_on_demand 2.0.0

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
+ .DS_Store
2
+ columns-on-demand-*.gem
3
+ Gemfile.lock
4
+ test/columns_on_demand_test.db
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Declare your gem's dependencies in transaction_isolation_level.gemspec.
4
+ # Bundler will treat runtime dependencies like base dependencies, and
5
+ # development dependencies will be added by default to the :development group.
6
+ gemspec
7
+
8
+ # Declare any dependencies that are still in development here instead of in
9
+ # your gemspec. These might include edge Rails or gems from your path or
10
+ # Git. Remember to move these dependencies to your gemspec before releasing
11
+ # your gem to rubygems.org.
12
+
13
+ # To use debugger
14
+ # gem 'ruby-debug19', :require => 'ruby-debug'
15
+ # gem 'ruby-debug'
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2009 Will Bryant, Sekuda Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,45 @@
1
+ ColumnsOnDemand
2
+ ===============
3
+
4
+ Lazily loads large columns on demand.
5
+
6
+ By default, does this for all TEXT (:text) and BLOB (:binary) columns, but a list
7
+ of specific columns to load on demand can be given.
8
+
9
+ This is useful to reduce the memory taken by Rails when loading a number of records
10
+ that have large columns if those particular columns are actually not required most
11
+ of the time. In this situation it can also greatly reduce the database query time
12
+ because loading large BLOB/TEXT columns generally means seeking to other database
13
+ pages since they are not stored wholly in the record's page itself.
14
+
15
+ Although this plugin is mainly used for BLOB and TEXT columns, it will actually
16
+ work on all types - and is just as useful for large string fields etc.
17
+
18
+
19
+ Compatibility
20
+ =============
21
+
22
+ Supports mysql, mysql2, postgresql, and sqlite3.
23
+
24
+ Currently tested against Rails 2.3.14, 3.0.17, 3.1.8, and 3.2.8. Note that
25
+ 3.0 and 3.1 have ActiveRecord regressions that will affect sqlite users.
26
+
27
+
28
+ Example
29
+ =======
30
+
31
+ # Example.all will exclude the file_data and processing_log columns from the
32
+ # SELECT query, and example.file_data and example.processing_log will load & cache
33
+ # that individual column value for the record instance.
34
+ class Example
35
+ columns_on_demand :file_data, :processing_log
36
+ end
37
+
38
+ # Scans the 'examples' table columns and registers all TEXT (:text) and BLOB (:binary)
39
+ # columns for loading on demand.
40
+ class Example
41
+ columns_on_demand
42
+ end
43
+
44
+
45
+ Copyright (c) 2008-2012 Will Bryant, Sekuda Ltd, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ desc 'Test the columns_on_demand plugin.'
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.libs << 'test'
14
+ t.pattern = 'test/*_test.rb'
15
+ t.verbose = true
16
+ end
@@ -0,0 +1,48 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/columns_on_demand/version', __FILE__)
3
+
4
+ spec = Gem::Specification.new do |gem|
5
+ gem.name = 'columns_on_demand'
6
+ gem.version = ColumnsOnDemand::VERSION
7
+ gem.summary = "Lazily loads large columns on demand."
8
+ gem.description = <<-EOF
9
+ Lazily loads large columns on demand.
10
+
11
+ By default, does this for all TEXT (:text) and BLOB (:binary) columns, but a list
12
+ of specific columns to load on demand can be given.
13
+
14
+ This is useful to reduce the memory taken by Rails when loading a number of records
15
+ that have large columns if those particular columns are actually not required most
16
+ of the time. In this situation it can also greatly reduce the database query time
17
+ because loading large BLOB/TEXT columns generally means seeking to other database
18
+ pages since they are not stored wholly in the record's page itself.
19
+
20
+ Although this plugin is mainly used for BLOB and TEXT columns, it will actually
21
+ work on all types - and is just as useful for large string fields etc.
22
+
23
+
24
+ Compatibility
25
+ =============
26
+
27
+ Supports mysql, mysql2, postgresql, and sqlite3.
28
+
29
+ Currently tested against Rails 2.3.14, 3.0.17, 3.1.8, and 3.2.8. Note that
30
+ 3.0 and 3.1 have ActiveRecord regressions that will affect sqlite users.
31
+ EOF
32
+ gem.has_rdoc = false
33
+ gem.author = "Will Bryant"
34
+ gem.email = "will.bryant@gmail.com"
35
+ gem.homepage = "http://github.com/willbryant/columns_on_demand"
36
+
37
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
38
+ gem.files = `git ls-files`.split("\n")
39
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
40
+ gem.require_path = "lib"
41
+
42
+ gem.add_dependency "activerecord"
43
+ gem.add_development_dependency "rake"
44
+ gem.add_development_dependency "mysql"
45
+ gem.add_development_dependency "mysql2"
46
+ gem.add_development_dependency "pg"
47
+ gem.add_development_dependency "sqlite3"
48
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'columns_on_demand'
2
+ ActiveRecord::Base.send(:extend, ColumnsOnDemand::BaseMethods)
@@ -0,0 +1,143 @@
1
+ module ColumnsOnDemand
2
+ module BaseMethods
3
+ def columns_on_demand(*columns_to_load_on_demand)
4
+ class_attribute :columns_to_load_on_demand, :instance_writer => false
5
+ self.columns_to_load_on_demand = columns_to_load_on_demand.empty? ? blob_and_text_columns : columns_to_load_on_demand.collect(&:to_s)
6
+
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+
10
+ if ActiveRecord::VERSION::MAJOR > 2
11
+ relation # set up @relation
12
+ class << @relation # but always modify @relation, not the temporary returned by .relation if there's a where(type condition)
13
+ def build_select_with_columns_on_demand(arel, selects)
14
+ unless selects.empty?
15
+ build_select_without_columns_on_demand(arel, selects)
16
+ else
17
+ arel.project(Arel::SqlLiteral.new(@klass.default_select(true)))
18
+ end
19
+ end
20
+ alias_method_chain :build_select, :columns_on_demand
21
+ end
22
+ end
23
+
24
+ class <<self
25
+ unless ActiveRecord.const_defined?(:AttributeMethods) &&
26
+ ActiveRecord::AttributeMethods::const_defined?(:Serialization) &&
27
+ ActiveRecord::AttributeMethods::Serialization::Attribute
28
+ alias_method_chain :define_read_method_for_serialized_attribute, :columns_on_demand
29
+ end
30
+ alias_method_chain :reset_column_information, :columns_on_demand
31
+ end
32
+ alias_method_chain :attributes, :columns_on_demand
33
+ alias_method_chain :attribute_names, :columns_on_demand
34
+ alias_method_chain :read_attribute, :columns_on_demand
35
+ alias_method_chain :read_attribute_before_type_cast, :columns_on_demand
36
+ alias_method_chain :missing_attribute, :columns_on_demand
37
+ alias_method_chain :reload, :columns_on_demand
38
+ end
39
+
40
+ def reset_column_information_with_columns_on_demand
41
+ @columns_to_select = nil
42
+ reset_column_information_without_columns_on_demand
43
+ end
44
+
45
+ def define_read_method_for_serialized_attribute_with_columns_on_demand(attr_name)
46
+ define_read_method_for_serialized_attribute_without_columns_on_demand(attr_name)
47
+ scope = method_defined?(:generated_attribute_methods) ? generated_attribute_methods : self
48
+ scope.module_eval("def #{attr_name}_with_columns_on_demand; ensure_loaded('#{attr_name}'); #{attr_name}_without_columns_on_demand; end; alias_method_chain :#{attr_name}, :columns_on_demand", __FILE__, __LINE__)
49
+ end
50
+
51
+ def blob_and_text_columns
52
+ columns.inject([]) do |blob_and_text_columns, column|
53
+ blob_and_text_columns << column.name if column.type == :binary || column.type == :text
54
+ blob_and_text_columns
55
+ end
56
+ end
57
+ end
58
+
59
+ module ClassMethods
60
+ # this is the method API as called by ActiveRecord 2.x. we also call it ourselves above in our ActiveRecord 3 extensions.
61
+ def default_select(qualified)
62
+ @columns_to_select ||= (columns.collect(&:name) - columns_to_load_on_demand).collect {|attr_name| connection.quote_column_name(attr_name)}
63
+ if qualified
64
+ quoted_table_name + '.' + @columns_to_select.join(", #{quoted_table_name}.")
65
+ else
66
+ @columns_to_select.join(", ")
67
+ end
68
+ end
69
+ end
70
+
71
+ module InstanceMethods
72
+ def columns_loaded
73
+ @columns_loaded ||= Set.new
74
+ end
75
+
76
+ def column_loaded?(attr_name)
77
+ !columns_to_load_on_demand.include?(attr_name) || !@attributes[attr_name].nil? || new_record? || columns_loaded.include?(attr_name)
78
+ end
79
+
80
+ def attributes_with_columns_on_demand
81
+ load_attributes(*columns_to_load_on_demand.reject {|attr_name| column_loaded?(attr_name)})
82
+ attributes_without_columns_on_demand
83
+ end
84
+
85
+ def attribute_names_with_columns_on_demand
86
+ (attribute_names_without_columns_on_demand + columns_to_load_on_demand).uniq.sort
87
+ end
88
+
89
+ def load_attributes(*attr_names)
90
+ return if attr_names.blank?
91
+ values = connection.select_rows(
92
+ "SELECT #{attr_names.collect {|attr_name| connection.quote_column_name(attr_name)}.join(", ")}" +
93
+ " FROM #{self.class.quoted_table_name}" +
94
+ " WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id, self.class.columns_hash[self.class.primary_key])}")
95
+ row = values.first || raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.class.name} with ID=#{id}")
96
+ attr_names.each_with_index do |attr_name, i|
97
+ columns_loaded << attr_name
98
+ @attributes[attr_name] = row[i]
99
+
100
+ if coder = self.class.serialized_attributes[attr_name]
101
+ if ActiveRecord.const_defined?(:AttributeMethods) &&
102
+ ActiveRecord::AttributeMethods::const_defined?(:Serialization) &&
103
+ ActiveRecord::AttributeMethods::Serialization::Attribute
104
+ # in 3.2 @attributes has a special Attribute struct to help cache both serialized and unserialized forms
105
+ @attributes[attr_name] = ActiveRecord::AttributeMethods::Serialization::Attribute.new(coder, @attributes[attr_name], :serialized)
106
+ elsif ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
107
+ # in 2.3 an 3.0, @attributes has the serialized form; from 3.1 it has the deserialized form
108
+ @attributes[attr_name] = coder.load @attributes[attr_name]
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ def ensure_loaded(attr_name)
115
+ load_attributes(attr_name.to_s) unless column_loaded?(attr_name.to_s)
116
+ end
117
+
118
+ def read_attribute_with_columns_on_demand(attr_name)
119
+ ensure_loaded(attr_name)
120
+ read_attribute_without_columns_on_demand(attr_name)
121
+ end
122
+
123
+ def read_attribute_before_type_cast_with_columns_on_demand(attr_name)
124
+ ensure_loaded(attr_name)
125
+ read_attribute_before_type_cast_without_columns_on_demand(attr_name)
126
+ end
127
+
128
+ def missing_attribute_with_columns_on_demand(attr_name, *args)
129
+ if columns_to_load_on_demand.include?(attr_name)
130
+ load_attributes(attr_name)
131
+ else
132
+ missing_attribute_without_columns_on_demand(attr_name, *args)
133
+ end
134
+ end
135
+
136
+ def reload_with_columns_on_demand(*args)
137
+ reload_without_columns_on_demand(*args).tap do
138
+ columns_loaded.clear
139
+ columns_to_load_on_demand.each {|attr_name| @attributes.delete(attr_name)}
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,3 @@
1
+ module ColumnsOnDemand
2
+ VERSION = '2.0.0'
3
+ end
@@ -0,0 +1,70 @@
1
+ if ActiveRecord::VERSION::MAJOR < 3 || (ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR < 1)
2
+ # proudly stolen from ActiveRecord's test suite, with addition of BEGIN and COMMIT
3
+ ActiveRecord::Base.connection.class.class_eval do
4
+ IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/, /^BEGIN$/, /^COMMIT$/]
5
+
6
+ def execute_with_query_record(sql, name = nil, &block)
7
+ $queries_executed ||= []
8
+ $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
9
+ execute_without_query_record(sql, name, &block)
10
+ end
11
+
12
+ alias_method_chain :execute, :query_record
13
+ end
14
+ elsif ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR < 2
15
+ # this is from 3.1's test suite. ugly.
16
+ class ActiveRecord::SQLCounter
17
+ cattr_accessor :ignored_sql
18
+ self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
19
+
20
+ # FIXME: this needs to be refactored so specific database can add their own
21
+ # ignored SQL. This ignored SQL is for Oracle.
22
+ ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
23
+
24
+ def initialize
25
+ $queries_executed = []
26
+ end
27
+
28
+ def call(name, start, finish, message_id, values)
29
+ sql = values[:sql]
30
+
31
+ # FIXME: this seems bad. we should probably have a better way to indicate
32
+ # the query was cached
33
+ unless ['CACHE', 'SCHEMA'].include?(values[:name]) # we have altered this from the original, to exclude SCHEMA as well
34
+ # debugger if sql =~ /^PRAGMA table_info/ && Kernel.caller.any? {|i| i.include?('test_it_creates_named_class_methods_if_a_')}
35
+ $queries_executed << sql unless self.class.ignored_sql.any? { |r| sql =~ r }
36
+ end
37
+ end
38
+ end
39
+ ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::SQLCounter.new)
40
+ else
41
+ # this is from 3.2's test suite. ugly.
42
+ class ActiveRecord::SQLCounter
43
+ cattr_accessor :ignored_sql
44
+ self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
45
+
46
+ # FIXME: this needs to be refactored so specific database can add their own
47
+ # ignored SQL. This ignored SQL is for Oracle.
48
+ ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
49
+
50
+ cattr_accessor :log
51
+ self.log = []
52
+
53
+ attr_reader :ignore
54
+
55
+ def initialize(ignore = self.class.ignored_sql)
56
+ @ignore = ignore
57
+ end
58
+
59
+ def call(name, start, finish, message_id, values)
60
+ sql = values[:sql]
61
+
62
+ # FIXME: this seems bad. we should probably have a better way to indicate
63
+ # the query was cached
64
+ return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql }
65
+ self.class.log << sql
66
+ end
67
+ end
68
+
69
+ ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::SQLCounter.new)
70
+ end
@@ -0,0 +1,273 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), 'schema'))
3
+ require File.expand_path(File.join(File.dirname(__FILE__), 'activerecord_count_queries'))
4
+
5
+ class Explicit < ActiveRecord::Base
6
+ columns_on_demand :file_data, :processing_log, :original_filename
7
+ end
8
+
9
+ class Implicit < ActiveRecord::Base
10
+ columns_on_demand
11
+ end
12
+
13
+ class Parent < ActiveRecord::Base
14
+ columns_on_demand
15
+
16
+ has_many :children
17
+ end
18
+
19
+ class Child < ActiveRecord::Base
20
+ columns_on_demand
21
+
22
+ belongs_to :parent
23
+ end
24
+
25
+ class ColumnsOnDemandTest < ActiveRecord::TestCase
26
+ def assert_not_loaded(record, attr_name)
27
+ assert !record.column_loaded?(attr_name.to_s)
28
+ end
29
+
30
+ def assert_loaded(record, attr_name)
31
+ assert record.column_loaded?(attr_name.to_s)
32
+ end
33
+
34
+ fixtures :all
35
+ self.use_transactional_fixtures = true
36
+
37
+ test "it lists explicitly given columns for loading on demand" do
38
+ assert_equal ["file_data", "processing_log", "original_filename"], Explicit.columns_to_load_on_demand
39
+ end
40
+
41
+ test "it lists all :binary and :text columns for loading on demand if none are explicitly given" do
42
+ assert_equal ["file_data", "processing_log", "results"], Implicit.columns_to_load_on_demand
43
+ end
44
+
45
+ test "it selects all the other columns for loading eagerly" do
46
+ assert_match /\W*id\W*, \W*results\W*, \W*processed_at\W*/, Explicit.default_select(false)
47
+ assert_match /\W*explicits\W*.results/, Explicit.default_select(true)
48
+
49
+ assert_match /\W*id\W*, \W*original_filename\W*, \W*processed_at\W*/, Implicit.default_select(false)
50
+ assert_match /\W*implicits\W*.original_filename/, Implicit.default_select(true)
51
+ end
52
+
53
+ test "it doesn't load the columns_to_load_on_demand straight away when finding the records" do
54
+ record = Implicit.find(:first)
55
+ assert_not_equal nil, record
56
+ assert_not_loaded record, "file_data"
57
+ assert_not_loaded record, "processing_log"
58
+
59
+ record = Implicit.find(:all).first
60
+ assert_not_equal nil, record
61
+ assert_not_loaded record, "file_data"
62
+ assert_not_loaded record, "processing_log"
63
+ end
64
+
65
+ test "it loads the columns when accessed as an attribute" do
66
+ record = Implicit.find(:first)
67
+ assert_equal "This is the file data!", record.file_data
68
+ assert_equal "Processed 0 entries OK", record.results
69
+ assert_equal record.results.object_id, record.results.object_id # should not have to re-find
70
+
71
+ record = Implicit.find(:all).first
72
+ assert_not_equal nil, record.file_data
73
+ end
74
+
75
+ test "it loads the columns only once even if nil" do
76
+ record = Implicit.find(:first)
77
+ assert_not_loaded record, "file_data"
78
+ assert_equal "This is the file data!", record.file_data
79
+ assert_loaded record, "file_data"
80
+ Implicit.update_all(:file_data => nil)
81
+
82
+ record = Implicit.find(:first)
83
+ assert_not_loaded record, "file_data"
84
+ assert_equal nil, record.file_data
85
+ assert_loaded record, "file_data"
86
+ assert_no_queries do
87
+ assert_equal nil, record.file_data
88
+ end
89
+ end
90
+
91
+ test "it loads the column when accessed using read_attribute" do
92
+ record = Implicit.find(:first)
93
+ assert_equal "This is the file data!", record.read_attribute(:file_data)
94
+ assert_equal "This is the file data!", record.read_attribute("file_data")
95
+ assert_equal "Processed 0 entries OK", record.read_attribute("results")
96
+ assert_equal record.read_attribute(:results).object_id, record.read_attribute("results").object_id # should not have to re-find
97
+ end
98
+
99
+ test "it loads the column when accessed using read_attribute_before_type_cast" do
100
+ record = Implicit.find(:first)
101
+ assert_equal "This is the file data!", record.read_attribute_before_type_cast("file_data")
102
+ assert_equal "Processed 0 entries OK", record.read_attribute_before_type_cast("results")
103
+ # read_attribute_before_type_cast doesn't tolerate symbol arguments as read_attribute does
104
+ end
105
+
106
+ test "it loads the column when generating #attributes" do
107
+ attributes = Implicit.find(:first).attributes
108
+ assert_equal "This is the file data!", attributes["file_data"]
109
+ end
110
+
111
+ test "loads all the columns in one query when generating #attributes" do
112
+ record = Implicit.find(:first)
113
+ assert_queries(1) do
114
+ attributes = record.attributes
115
+ assert_equal "This is the file data!", attributes["file_data"]
116
+ assert !attributes["processing_log"].blank?
117
+ end
118
+ end
119
+
120
+ test "it loads the column when generating #to_json" do
121
+ ActiveRecord::Base.include_root_in_json = true
122
+ json = Implicit.find(:first).to_json
123
+ assert_equal "This is the file data!", ActiveSupport::JSON.decode(json)["implicit"]["file_data"]
124
+ end
125
+
126
+ test "it loads the column for #clone" do
127
+ record = Implicit.find(:first).clone
128
+ assert_equal "This is the file data!", record.file_data
129
+
130
+ record = Implicit.find(:first).clone.tap(&:save!)
131
+ assert_equal "This is the file data!", Implicit.find(record.id).file_data
132
+ end
133
+
134
+ test "it clears the column on reload, and can load it again" do
135
+ record = Implicit.find(:first)
136
+ old_object_id = record.file_data.object_id
137
+ Implicit.update_all(:file_data => "New file data")
138
+
139
+ record.reload
140
+
141
+ assert_not_loaded record, "file_data"
142
+ assert_equal "New file data", record.file_data
143
+ end
144
+
145
+ test "it doesn't override custom :select finds" do
146
+ record = Implicit.find(:first, :select => "id, file_data")
147
+ klass = ActiveRecord.const_defined?(:MissingAttributeError) ? ActiveRecord::MissingAttributeError : ActiveModel::MissingAttributeError
148
+ assert_raise klass do
149
+ record.processed_at # explicitly not loaded, overriding default
150
+ end
151
+ assert_equal "This is the file data!", record.instance_variable_get("@attributes")["file_data"] # already loaded, overriding default
152
+ end
153
+
154
+ test "it raises normal ActiveRecord::RecordNotFound if the record is deleted before the column load" do
155
+ record = Implicit.find(:first)
156
+ Implicit.delete_all
157
+
158
+ assert_raise ActiveRecord::RecordNotFound do
159
+ record.file_data
160
+ end
161
+ end
162
+
163
+ test "it doesn't raise on column access if the record is deleted after the column load" do
164
+ record = Implicit.find(:first)
165
+ record.file_data
166
+ Implicit.delete_all
167
+
168
+ assert_equal "This is the file data!", record.file_data # check it doesn't raise
169
+ end
170
+
171
+ test "it updates the select strings when columns are changed and the column information is reset" do
172
+ ActiveRecord::Schema.define(:version => 1) do
173
+ create_table :dummies, :force => true do |t|
174
+ t.string :some_field
175
+ t.binary :big_field
176
+ end
177
+ end
178
+
179
+ class Dummy < ActiveRecord::Base
180
+ columns_on_demand
181
+ end
182
+
183
+ assert_match /\W*id\W*, \W*some_field\W*/, Dummy.default_select(false)
184
+
185
+ ActiveRecord::Schema.define(:version => 2) do
186
+ create_table :dummies, :force => true do |t|
187
+ t.string :some_field
188
+ t.binary :big_field
189
+ t.string :another_field
190
+ end
191
+ end
192
+
193
+ assert_match /\W*id\W*, \W*some_field\W*/, Dummy.default_select(false)
194
+ Dummy.reset_column_information
195
+ assert_match /\W*id\W*, \W*some_field\W*, \W*another_field\W*/, Dummy.default_select(false)
196
+ end
197
+
198
+ test "it handles STI models" do
199
+ ActiveRecord::Schema.define(:version => 1) do
200
+ create_table :stis, :force => true do |t|
201
+ t.string :type
202
+ t.string :some_field
203
+ t.binary :big_field
204
+ end
205
+ end
206
+
207
+ class Sti < ActiveRecord::Base
208
+ columns_on_demand
209
+ end
210
+
211
+ class StiChild < Sti
212
+ columns_on_demand :some_field
213
+ end
214
+
215
+ assert_match /\W*id\W*, \W*type\W*, \W*some_field\W*/, Sti.default_select(false)
216
+ assert_match /\W*id\W*, \W*type\W*, \W*big_field\W*/, StiChild.default_select(false)
217
+ end
218
+
219
+ test "it works on child records loaded from associations" do
220
+ parent = parents(:some_parent)
221
+ child = parent.children.find(:first)
222
+ assert_not_loaded child, "test_data"
223
+ assert_equal "Some test data", child.test_data
224
+ end
225
+
226
+ test "it works on parent records loaded from associations" do
227
+ child = children(:a_child_of_some_parent)
228
+ parent = child.parent
229
+ assert_not_loaded parent, "info"
230
+ assert_equal "Here's some info.", parent.info
231
+ end
232
+
233
+ test "it doesn't break validates_presence_of" do
234
+ class ValidatedImplicit < ActiveRecord::Base
235
+ set_table_name "implicits"
236
+ columns_on_demand
237
+ validates_presence_of :original_filename, :file_data, :results
238
+ end
239
+
240
+ assert !ValidatedImplicit.new(:original_filename => "test.txt").valid?
241
+ instance = ValidatedImplicit.create!(:original_filename => "test.txt", :file_data => "test file data", :results => "test results")
242
+ instance.update_attributes!({}) # file_data and results are already loaded
243
+ new_instance = ValidatedImplicit.find(instance.id)
244
+ new_instance.update_attributes!({}) # file_data and results aren't loaded yet, but will be loaded to validate
245
+ end
246
+
247
+ test "it works with serialized columns" do
248
+ class Serializing < ActiveRecord::Base
249
+ columns_on_demand
250
+ serialize :data
251
+ end
252
+
253
+ data = {:foo => '1', :bar => '2', :baz => '3'}
254
+ original_record = Serializing.create!(:data => data)
255
+ assert_equal data, original_record.data
256
+
257
+ record = Serializing.find(:first)
258
+ assert_not_loaded record, "data"
259
+ assert_equal data, record.data
260
+ assert_equal false, record.data_changed?
261
+ assert_equal false, record.changed?
262
+ assert_equal data, record.data
263
+
264
+ record.data = "replacement"
265
+ assert_equal true, record.data_changed?
266
+ assert_equal true, record.changed?
267
+ record.save!
268
+
269
+ record = Serializing.find(:first)
270
+ assert_not_loaded record, "data"
271
+ assert_equal "replacement", record.data
272
+ end
273
+ end
data/test/database.yml ADDED
@@ -0,0 +1,14 @@
1
+ mysql:
2
+ adapter: mysql
3
+ database: columns_on_demand_test
4
+ username: root
5
+ mysql2:
6
+ adapter: mysql2
7
+ database: columns_on_demand_test
8
+ username: root
9
+ postgresql:
10
+ adapter: postgresql
11
+ database: columns_on_demand_test
12
+ sqlite3:
13
+ adapter: sqlite3
14
+ database: /tmp/columns_on_demand_test.db
@@ -0,0 +1,3 @@
1
+ a_child_of_some_parent:
2
+ parent: some_parent
3
+ test_data: Some test data
@@ -0,0 +1,10 @@
1
+ first_file:
2
+ original_filename: somefile.txt
3
+ file_data: This is the file data!
4
+ processing_log: "Processing somefile.txt\n
5
+ Processing header\n
6
+ Processed header\n
7
+ Processing footer\n
8
+ Processed footer\n"
9
+ results: Processed 0 entries OK
10
+ processed_at: 2008-12-23 21:38:10
@@ -0,0 +1,2 @@
1
+ some_parent:
2
+ info: Here's some info.
data/test/schema.rb ADDED
@@ -0,0 +1,30 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+ create_table :explicits, :force => true do |t|
3
+ t.string :original_filename, :null => false
4
+ t.binary :file_data
5
+ t.text :processing_log
6
+ t.text :results
7
+ t.datetime :processed_at
8
+ end
9
+
10
+ create_table :implicits, :force => true do |t|
11
+ t.string :original_filename, :null => false
12
+ t.binary :file_data
13
+ t.text :processing_log
14
+ t.text :results
15
+ t.datetime :processed_at
16
+ end
17
+
18
+ create_table :parents, :force => true do |t|
19
+ t.text :info
20
+ end
21
+
22
+ create_table :children, :force => true do |t|
23
+ t.integer :parent_id, :null => false
24
+ t.text :test_data
25
+ end
26
+
27
+ create_table :serializings, :force => true do |t|
28
+ t.binary :data
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ RAILS_ROOT = File.expand_path("../../..")
2
+ if File.exist?("#{RAILS_ROOT}/config/boot.rb")
3
+ require "#{RAILS_ROOT}/config/boot.rb"
4
+ else
5
+ require 'rubygems'
6
+ end
7
+
8
+ puts "Rails: #{ENV['RAILS_VERSION'] || 'default'}"
9
+ gem 'activesupport', ENV['RAILS_VERSION']
10
+ gem 'activerecord', ENV['RAILS_VERSION']
11
+
12
+ require 'test/unit'
13
+ require 'active_support'
14
+ require 'active_support/test_case'
15
+ require 'active_record'
16
+ require 'active_record/fixtures'
17
+
18
+ begin
19
+ require 'ruby-debug'
20
+ Debugger.start
21
+ rescue LoadError
22
+ # ruby-debug not installed, no debugging for you
23
+ end
24
+
25
+ ActiveRecord::Base.configurations = YAML::load(IO.read(File.join(File.dirname(__FILE__), "database.yml")))
26
+ configuration = ActiveRecord::Base.configurations[ENV['RAILS_ENV']]
27
+ raise "use RAILS_ENV=#{ActiveRecord::Base.configurations.keys.sort.join '/'} to test this plugin" unless configuration
28
+ ActiveRecord::Base.establish_connection configuration
29
+
30
+ ActiveSupport::TestCase.send(:include, ActiveRecord::TestFixtures) if ActiveRecord.const_defined?('TestFixtures')
31
+ ActiveSupport::TestCase.fixture_path = File.join(File.dirname(__FILE__), "fixtures")
32
+
33
+ require File.expand_path(File.join(File.dirname(__FILE__), '../init')) # load columns_on_demand
metadata ADDED
@@ -0,0 +1,194 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: columns_on_demand
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15
5
+ prerelease:
6
+ segments:
7
+ - 2
8
+ - 0
9
+ - 0
10
+ version: 2.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Will Bryant
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-08-18 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rake
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: mysql
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: mysql2
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :development
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: pg
78
+ prerelease: false
79
+ requirement: &id005 !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ type: :development
89
+ version_requirements: *id005
90
+ - !ruby/object:Gem::Dependency
91
+ name: sqlite3
92
+ prerelease: false
93
+ requirement: &id006 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ type: :development
103
+ version_requirements: *id006
104
+ description: |
105
+ Lazily loads large columns on demand.
106
+
107
+ By default, does this for all TEXT (:text) and BLOB (:binary) columns, but a list
108
+ of specific columns to load on demand can be given.
109
+
110
+ This is useful to reduce the memory taken by Rails when loading a number of records
111
+ that have large columns if those particular columns are actually not required most
112
+ of the time. In this situation it can also greatly reduce the database query time
113
+ because loading large BLOB/TEXT columns generally means seeking to other database
114
+ pages since they are not stored wholly in the record's page itself.
115
+
116
+ Although this plugin is mainly used for BLOB and TEXT columns, it will actually
117
+ work on all types - and is just as useful for large string fields etc.
118
+
119
+
120
+ Compatibility
121
+ =============
122
+
123
+ Supports mysql, mysql2, postgresql, and sqlite3.
124
+
125
+ Currently tested against Rails 2.3.14, 3.0.17, 3.1.8, and 3.2.8. Note that
126
+ 3.0 and 3.1 have ActiveRecord regressions that will affect sqlite users.
127
+
128
+ email: will.bryant@gmail.com
129
+ executables: []
130
+
131
+ extensions: []
132
+
133
+ extra_rdoc_files: []
134
+
135
+ files:
136
+ - .gitignore
137
+ - Gemfile
138
+ - MIT-LICENSE
139
+ - README
140
+ - Rakefile
141
+ - columns_on_demand.gemspec
142
+ - init.rb
143
+ - lib/columns_on_demand.rb
144
+ - lib/columns_on_demand/version.rb
145
+ - test/activerecord_count_queries.rb
146
+ - test/columns_on_demand_test.rb
147
+ - test/database.yml
148
+ - test/fixtures/children.yml
149
+ - test/fixtures/implicits.yml
150
+ - test/fixtures/parents.yml
151
+ - test/schema.rb
152
+ - test/test_helper.rb
153
+ homepage: http://github.com/willbryant/columns_on_demand
154
+ licenses: []
155
+
156
+ post_install_message:
157
+ rdoc_options: []
158
+
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ none: false
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ hash: 3
167
+ segments:
168
+ - 0
169
+ version: "0"
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ none: false
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ hash: 3
176
+ segments:
177
+ - 0
178
+ version: "0"
179
+ requirements: []
180
+
181
+ rubyforge_project:
182
+ rubygems_version: 1.8.15
183
+ signing_key:
184
+ specification_version: 3
185
+ summary: Lazily loads large columns on demand.
186
+ test_files:
187
+ - test/activerecord_count_queries.rb
188
+ - test/columns_on_demand_test.rb
189
+ - test/database.yml
190
+ - test/fixtures/children.yml
191
+ - test/fixtures/implicits.yml
192
+ - test/fixtures/parents.yml
193
+ - test/schema.rb
194
+ - test/test_helper.rb