├── init.rb ├── test ├── fixtures │ ├── parents.yml │ ├── children.yml │ └── implicits.yml ├── database.yml ├── schema.rb ├── test_helper.rb ├── activerecord_count_queries.rb └── columns_on_demand_test.rb ├── lib ├── columns_on_demand │ └── version.rb └── columns_on_demand.rb ├── .gitignore ├── .travis.yml ├── test_all.sh ├── Rakefile ├── Gemfile ├── MIT-LICENSE ├── columns_on_demand.gemspec └── README.md /init.rb: -------------------------------------------------------------------------------- 1 | require 'columns_on_demand' 2 | -------------------------------------------------------------------------------- /test/fixtures/parents.yml: -------------------------------------------------------------------------------- 1 | some_parent: 2 | info: Here's some info. 3 | -------------------------------------------------------------------------------- /lib/columns_on_demand/version.rb: -------------------------------------------------------------------------------- 1 | module ColumnsOnDemand 2 | VERSION = '6.1.0' 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | columns_on_demand-*.gem 3 | Gemfile.lock 4 | test/columns_on_demand_test.db 5 | -------------------------------------------------------------------------------- /test/fixtures/children.yml: -------------------------------------------------------------------------------- 1 | a_child_of_some_parent: 2 | parent: some_parent 3 | test_data: Some test data 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | dist: bionic 6 | rvm: 7 | - 2.6 8 | services: 9 | - postgresql 10 | - mysql 11 | before_script: 12 | - createdb -U postgres columns_on_demand_test 13 | - mysqladmin -u root create columns_on_demand_test 14 | script: ./test_all.sh 15 | -------------------------------------------------------------------------------- /test_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | for version in 6.1.7.8 7.0.8.4 7.1.3.4 7.2.0.beta2 6 | do 7 | RAILS_VERSION=$version SQLITE3_VERSION=1.5.0 bundle update rails sqlite3 8 | RAILS_ENV=sqlite3 bundle exec rake 9 | RAILS_ENV=postgresql bundle exec rake 10 | RAILS_ENV=mysql2 bundle exec rake 11 | done 12 | -------------------------------------------------------------------------------- /test/database.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | adapter: mysql 3 | database: columns_on_demand_test 4 | mysql2: 5 | adapter: mysql2 6 | database: columns_on_demand_test 7 | postgresql: 8 | adapter: postgresql 9 | database: columns_on_demand_test 10 | sqlite3: 11 | adapter: sqlite3 12 | database: /tmp/columns_on_demand_test.db 13 | -------------------------------------------------------------------------------- /test/fixtures/implicits.yml: -------------------------------------------------------------------------------- 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 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Declare your gem's dependencies in columns_on_demand.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 | gem 'rails', ENV['RAILS_VERSION'] 14 | gem 'sqlite3', ENV['SQLITE3_VERSION'] -------------------------------------------------------------------------------- /test/schema.rb: -------------------------------------------------------------------------------- 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 | 31 | create_table :stis, :force => true do |t| 32 | t.string :type 33 | t.string :some_field 34 | t.binary :big_field 35 | end 36 | end -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2018 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. 21 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 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 | puts "Env: #{ENV['RAILS_ENV'] || 'not set'}" 10 | gem 'activesupport', ENV['RAILS_VERSION'] 11 | gem 'activerecord', ENV['RAILS_VERSION'] 12 | 13 | require 'minitest/autorun' 14 | require 'active_support' 15 | require 'active_support/test_case' 16 | require 'active_record' 17 | require 'active_record/fixtures' 18 | 19 | begin 20 | require 'byebug' 21 | rescue LoadError 22 | # 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.find_db_config(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 | 32 | if ActiveSupport::TestCase.respond_to?(:fixture_paths) 33 | ActiveSupport::TestCase.fixture_paths = [File.join(File.dirname(__FILE__), "fixtures")] 34 | else 35 | ActiveSupport::TestCase.fixture_path = File.join(File.dirname(__FILE__), "fixtures") 36 | end 37 | 38 | require File.expand_path(File.join(File.dirname(__FILE__), '../init')) # load columns_on_demand 39 | -------------------------------------------------------------------------------- /columns_on_demand.gemspec: -------------------------------------------------------------------------------- 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 | EOF 23 | gem.author = "Will Bryant" 24 | gem.email = "will.bryant@gmail.com" 25 | gem.homepage = "http://github.com/willbryant/columns_on_demand" 26 | gem.license = "MIT" 27 | 28 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 29 | gem.files = `git ls-files`.split("\n") 30 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 31 | gem.require_path = "lib" 32 | 33 | gem.add_dependency "activerecord" 34 | gem.add_development_dependency "rake" 35 | gem.add_development_dependency "mysql2" 36 | gem.add_development_dependency "pg" 37 | gem.add_development_dependency "sqlite3" 38 | gem.add_development_dependency "byebug" 39 | end 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 7.2.0.beta2, 7.1.3.4, 7.0.8.4, and 6.1.7.8, with older gem versions compatible with earlier Rails versions. 25 | 26 | 27 | Example 28 | ======= 29 | 30 | `Example.all` will exclude the `file_data` and `processing_log` columns from the 31 | `SELECT` query, and `example.file_data` and `example.processing_log` will load & cache 32 | that individual column value for the record instance: 33 | 34 | ```ruby 35 | class Example 36 | columns_on_demand :file_data, :processing_log 37 | end 38 | ``` 39 | 40 | Scans the `examples` table columns and registers all TEXT (`:text`) and BLOB (`:binary`) columns for loading on demand: 41 | 42 | ```ruby 43 | class Example 44 | columns_on_demand 45 | end 46 | ``` 47 | 48 | Thanks 49 | ====== 50 | 51 | * Tim Connor (@tlconnor) 52 | * Tobias Matthies (@tobmatth) 53 | * Phil Ross (@philr) 54 | * Jens Schmidt (@w3dot0) 55 | 56 | Copyright (c) 2008-2024 Will Bryant, Sekuda Ltd, released under the MIT license 57 | -------------------------------------------------------------------------------- /test/activerecord_count_queries.rb: -------------------------------------------------------------------------------- 1 | if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR < 2 2 | # this is from 3.1's test suite. ugly. 3 | class SQLCounter 4 | cattr_accessor :ignored_sql 5 | 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/] 6 | 7 | # FIXME: this needs to be refactored so specific database can add their own 8 | # ignored SQL. This ignored SQL is for Oracle. 9 | ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] 10 | 11 | def self.clear_log 12 | $queries_executed = [] 13 | end 14 | 15 | def initialize 16 | $queries_executed.clear 17 | end 18 | 19 | def call(name, start, finish, message_id, values) 20 | sql = values[:sql] 21 | 22 | # FIXME: this seems bad. we should probably have a better way to indicate 23 | # the query was cached 24 | unless ['CACHE', 'SCHEMA'].include?(values[:name]) # we have altered this from the original, to exclude SCHEMA as well 25 | # debugger if sql =~ /^PRAGMA table_info/ && Kernel.caller.any? {|i| i.include?('test_it_creates_named_class_methods_if_a_')} 26 | $queries_executed << sql unless self.class.ignored_sql.any? { |r| sql =~ r } 27 | end 28 | end 29 | end 30 | ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) 31 | else 32 | # this is from 3.2's test suite. ugly. 33 | class SQLCounter 34 | cattr_accessor :ignored_sql 35 | 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/] 36 | 37 | # FIXME: this needs to be refactored so specific database can add their own 38 | # ignored SQL. This ignored SQL is for Oracle. 39 | ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] 40 | 41 | cattr_accessor :log 42 | self.log = [] 43 | 44 | attr_reader :ignore 45 | 46 | def self.clear_log 47 | self.log.clear 48 | end 49 | 50 | def initialize(ignore = self.class.ignored_sql) 51 | @ignore = ignore 52 | end 53 | 54 | def call(name, start, finish, message_id, values) 55 | sql = values[:sql] 56 | 57 | # FIXME: this seems bad. we should probably have a better way to indicate 58 | # the query was cached 59 | return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql } 60 | self.class.log << sql 61 | end 62 | end 63 | 64 | ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) 65 | end 66 | -------------------------------------------------------------------------------- /lib/columns_on_demand.rb: -------------------------------------------------------------------------------- 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 | prepend InstanceMethods 9 | 10 | class < nil) 96 | 97 | record = Implicit.first 98 | assert_not_loaded record, "file_data" 99 | assert_nil record.file_data 100 | assert_loaded record, "file_data" 101 | assert_no_queries do 102 | assert_nil record.file_data 103 | end 104 | end 105 | 106 | test "it loads the column when accessed using read_attribute" do 107 | record = Implicit.first 108 | assert_equal "This is the file data!", record.read_attribute(:file_data) 109 | assert_equal "This is the file data!", record.read_attribute("file_data") 110 | assert_equal "Processed 0 entries OK", record.read_attribute("results") 111 | assert_equal record.read_attribute(:results).object_id, record.read_attribute("results").object_id # should not have to re-find 112 | end 113 | 114 | test "it loads the column when accessed using read_attribute_before_type_cast" do 115 | record = Implicit.first 116 | if Implicit.connection.class.name =~ /PostgreSQL/ && ActiveRecord::VERSION::MAJOR >= 4 117 | # newer versions of activerecord show the encoded binary format used for blob columns in postgresql in the before_type_cast output, whereas older ones had already decoded that 118 | assert_equal "\\x54686973206973207468652066696c65206461746121", record.read_attribute_before_type_cast("file_data") 119 | else 120 | assert_equal "This is the file data!", record.read_attribute_before_type_cast("file_data") 121 | end 122 | assert_equal "Processed 0 entries OK", record.read_attribute_before_type_cast("results") 123 | # read_attribute_before_type_cast doesn't tolerate symbol arguments as read_attribute does 124 | end 125 | 126 | test "it loads the column when generating #attributes" do 127 | attributes = Implicit.first.attributes 128 | assert_equal "This is the file data!", attributes["file_data"] 129 | end 130 | 131 | test "loads all the columns in one query when generating #attributes" do 132 | record = Implicit.first 133 | assert_queries(1) do 134 | attributes = record.attributes 135 | assert_equal "This is the file data!", attributes["file_data"] 136 | assert !attributes["processing_log"].blank? 137 | end 138 | end 139 | 140 | test "it doesn't load the column when generating #attributes if not included in the select() list" do 141 | attributes = Implicit.select("id, original_filename").first.attributes 142 | assert_equal "somefile.txt", attributes["original_filename"] 143 | assert !attributes.has_key?("file_data") 144 | end 145 | 146 | test "it loads the column when generating #attributes if included in the select() list" do 147 | attributes = Implicit.select("id, original_filename, file_data").first.attributes 148 | assert_equal "somefile.txt", attributes["original_filename"] 149 | assert_equal "This is the file data!", attributes["file_data"] 150 | end 151 | 152 | test "it loads the column when changing its value" do 153 | record = Implicit.first 154 | record.file_data = 'This is the new file data!' 155 | assert_equal "This is the file data!", record.file_data_was 156 | assert_equal ["This is the file data!", "This is the new file data!"], record.changes[:file_data] 157 | end 158 | 159 | test "it loads the column when generating #to_json" do 160 | ActiveRecord::Base.include_root_in_json = true 161 | json = Implicit.first.to_json 162 | assert_equal "This is the file data!", ActiveSupport::JSON.decode(json)["implicit"]["file_data"] 163 | end 164 | 165 | test "it loads the column for #clone" do 166 | record = Implicit.first.clone 167 | assert_equal "This is the file data!", record.file_data 168 | 169 | record = Implicit.first.clone.tap(&:save!) 170 | assert_equal "This is the file data!", Implicit.find(record.id).file_data 171 | end 172 | 173 | test "it clears the column on reload, and can load it again" do 174 | record = Implicit.first 175 | old_object_id = record.file_data.object_id 176 | Implicit.update_all(:file_data => "New file data") 177 | 178 | record.reload 179 | 180 | assert_not_loaded record, "file_data" 181 | assert_equal "New file data", record.file_data 182 | assert_not_equal old_object_id, record.file_data.object_id 183 | end 184 | 185 | test "it does not think the column has been loaded if a reloaded instance that has not loaded the attribute is saved" do 186 | record = Implicit.first 187 | record.file_data = "New file data" 188 | record.save! 189 | 190 | record.reload 191 | record.save! 192 | 193 | assert_equal "New file data", record.file_data 194 | end 195 | 196 | test "it does not think the column has been loaded if a fresh instance that has not loaded the attribute is saved" do 197 | record = Implicit.first 198 | record.file_data = "New file data" 199 | record.save! 200 | 201 | record = Implicit.find(record.id) 202 | record.save! 203 | 204 | assert_equal "New file data", record.file_data 205 | end 206 | 207 | test "it doesn't override custom select() finds" do 208 | record = Implicit.select("id, file_data").first 209 | klass = ActiveRecord.const_defined?(:MissingAttributeError) ? ActiveRecord::MissingAttributeError : ActiveModel::MissingAttributeError 210 | assert_raise klass do 211 | record.processed_at # explicitly not loaded, overriding default 212 | end 213 | assert_loaded record, :file_data 214 | end 215 | 216 | test "it doesn't load the on demand columns with select *" do 217 | record = Implicit.select(Implicit.arel_table[Arel.star]).first 218 | assert_not_loaded record, "file_data" 219 | assert_not_loaded record, "processing_log" 220 | 221 | record = Implicit.select('*').first 222 | assert_not_loaded record, "file_data" 223 | assert_not_loaded record, "processing_log" 224 | end 225 | 226 | test "it raises normal ActiveRecord::RecordNotFound if the record is deleted before the column load" do 227 | record = Implicit.first 228 | Implicit.delete_all 229 | 230 | assert_raise ActiveRecord::RecordNotFound do 231 | record.file_data 232 | end 233 | end 234 | 235 | test "it doesn't raise on column access if the record is deleted after the column load" do 236 | record = Implicit.first 237 | record.file_data 238 | Implicit.delete_all 239 | 240 | assert_equal "This is the file data!", record.file_data # check it doesn't raise 241 | end 242 | 243 | test "it reports the columns in the class-level attribute_method?" do 244 | assert(Implicit.attribute_method?("file_data")) 245 | end 246 | 247 | test "it reports the columns in the instance-level attribute_method?" do 248 | assert(Implicit.first.send(:attribute_method?, "file_data")) 249 | end 250 | 251 | test "it makes the instance classes respond_to the attribute even if not loaded" do 252 | assert(Implicit.first.respond_to?(:file_data)) 253 | end 254 | 255 | test "it handles STI models" do 256 | class Sti < ActiveRecord::Base 257 | columns_on_demand 258 | end 259 | 260 | class StiChild < Sti 261 | columns_on_demand :some_field 262 | end 263 | 264 | assert_match(/\W*id\W*, \W*type\W*, \W*some_field\W*/, Sti.default_select(false)) 265 | assert_match(/\W*id\W*, \W*type\W*, \W*big_field\W*/, StiChild.default_select(false)) 266 | end 267 | 268 | test "it works on child records loaded from associations" do 269 | parent = parents(:some_parent) 270 | child = parent.children.first 271 | assert_not_loaded child, "test_data" 272 | assert_equal "Some test data", child.test_data 273 | end 274 | 275 | test "it works on parent records loaded from associations" do 276 | child = children(:a_child_of_some_parent) 277 | parent = child.parent 278 | assert_not_loaded parent, "info" 279 | assert_equal "Here's some info.", parent.info 280 | end 281 | 282 | test "it works on child records loaded from associations with includes" do 283 | parent = Parent.includes(:children).first 284 | child = parent.children.first 285 | assert_not_loaded child, "test_data" 286 | assert_equal "Some test data", child.test_data 287 | end 288 | 289 | test "it works on parent records loaded from associations with includes" do 290 | child = Child.includes(:parent).first 291 | parent = child.parent 292 | assert_not_loaded parent, "info" 293 | assert_equal "Here's some info.", parent.info 294 | end 295 | 296 | test "it doesn't break validates_presence_of" do 297 | class ValidatedImplicit < ActiveRecord::Base 298 | self.table_name = "implicits" 299 | columns_on_demand 300 | validates_presence_of :original_filename, :file_data, :results 301 | end 302 | 303 | assert !ValidatedImplicit.new(:original_filename => "test.txt").valid? 304 | instance = ValidatedImplicit.create!(:original_filename => "test.txt", :file_data => "test file data", :results => "test results") 305 | assert instance.valid? # file_data and results are already loaded 306 | new_instance = ValidatedImplicit.find(instance.id) 307 | assert new_instance.valid? # file_data and results aren't loaded yet, but will be loaded to validate 308 | end 309 | 310 | test "it works with serialized columns" do 311 | class Serializing < ActiveRecord::Base 312 | columns_on_demand 313 | serialize :data 314 | end 315 | 316 | data = {:foo => '1', :bar => '2', :baz => '3'} 317 | original_record = Serializing.create!(:data => data) 318 | assert_equal data, original_record.data 319 | 320 | record = Serializing.first 321 | assert_not_loaded record, "data" 322 | assert_equal false, record.data_changed? 323 | 324 | assert_not_loaded record, "data" 325 | assert_equal data, record.data 326 | assert_equal false, record.data_changed? 327 | assert_equal false, record.changed? 328 | assert_equal data, record.data 329 | assert_equal data, record.data_was 330 | 331 | record.data = "replacement" 332 | assert_equal true, record.data_changed? 333 | assert_equal true, record.changed? 334 | record.save! 335 | 336 | record = Serializing.first 337 | assert_not_loaded record, "data" 338 | assert_equal "replacement", record.data 339 | end 340 | 341 | test "it doesn't create duplicate columns in SELECT queries" do 342 | implicits = Arel::Table.new(:implicits) 343 | reference_sql = implicits.project(implicits[:id]).to_sql 344 | select_sql = Implicit.select("#{Implicit.quoted_table_name}.#{Implicit.connection.quote_column_name("id")}").to_sql 345 | assert_equal select_sql, reference_sql 346 | end 347 | end 348 | 349 | class ColumnsOnDemandSchemaTest < ActiveSupport::TestCase 350 | if respond_to?(:use_transactional_tests=) 351 | self.use_transactional_tests = false 352 | else 353 | self.use_transactional_fixtures = false 354 | end 355 | 356 | test "it updates the select strings when columns are changed and the column information is reset" do 357 | ActiveRecord::Schema.define(:version => 1) do 358 | create_table :dummies, :force => true do |t| 359 | t.string :some_field 360 | t.binary :big_field 361 | end 362 | end 363 | 364 | class Dummy < ActiveRecord::Base 365 | columns_on_demand 366 | end 367 | 368 | assert_match(/\W*id\W*, \W*some_field\W*/, Dummy.default_select(false)) 369 | 370 | ActiveRecord::Schema.define(:version => 2) do 371 | create_table :dummies, :force => true do |t| 372 | t.string :some_field 373 | t.binary :big_field 374 | t.string :another_field 375 | end 376 | end 377 | 378 | assert_match(/\W*id\W*, \W*some_field\W*/, Dummy.default_select(false)) 379 | Dummy.reset_column_information 380 | assert_match(/\W*id\W*, \W*some_field\W*, \W*another_field\W*/, Dummy.default_select(false)) 381 | end 382 | end 383 | --------------------------------------------------------------------------------