slim_scrooge 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile ADDED
@@ -0,0 +1,85 @@
1
+ h1. SlimScrooge - serious optimisation of mysql for activerecord
2
+
3
+ h2. What is it?
4
+
5
+ It's an optimization layer to ensure your application only fetches the database content needed to minimize wire traffic, excessive SQL queries and reduce conversion overheads to native Ruby types.
6
+
7
+ SlimScrooge implements both lazy loading of attributes fetched from mysql, as well as inline query optimisation, automatically restricting the columns fetched based on what was used during previous passes through the same part of your code.
8
+
9
+ h2. Benchmark
10
+
11
+ SlimScrooge performs best when the database is not on the same machine as your rails app. In this case the overhead of fetching unnecessary data from the database can become more important.
12
+
13
+ I ran a benchmark that consisted of fetching 400 real urls (culled from the log file) from our complex web app. In this test I found a consistent 12% improvement in performance over plain active record. Not earth-shattering, but worthwhile. In future releases I expect further gains.
14
+
15
+ h2. Installation
16
+
17
+ <pre>
18
+ # if you haven't already, add gemcutter to your gem sources
19
+ sudo gem install gemcutter
20
+ gem tumble
21
+ # install slim_scrooge
22
+ sudo gem install slim_scrooge
23
+ </pre>
24
+
25
+ h3. Requirements
26
+
27
+ * "Slim-attributes":http://github.com/sdsykes/slim-attributes
28
+
29
+ h3. What it does
30
+
31
+ <pre>
32
+ # 1st request, sql is unchanged but columns accesses are recorded
33
+ Brochure Load SlimScrooged 1st time (27.1ms) SELECT * FROM `brochures` WHERE (expires_at IS NULL)
34
+
35
+ # 2nd request, only fetch columns that were used the first time
36
+ Brochure Load SlimScrooged (4.5ms) SELECT `brochures`.expires_at,`brochures`.operator_id,`brochures`.id FROM
37
+ `brochures` WHERE (expires_at IS NULL)
38
+
39
+ # 2nd request, later in code we need another column which causes a reload of all remaining columns
40
+ Brochure Reload SlimScrooged (0.6ms) `brochures`.name,`brochures`.comment,`brochures`.image_height,`brochures`.id,
41
+ `brochures`.tel,`brochures`.long_comment,`brochures`.image_name,`brochures`.image_width FROM
42
+ `brochures` WHERE `brochures`.id IN ('5646','5476','4562','3456','4567','7355')
43
+
44
+ # 3rd request
45
+ Brochure Load SlimScrooged (4.5ms) SELECT `brochures`.expires_at,`brochures`.operator_id,`brochures`.name,
46
+ `brochures`.id FROM `brochures` WHERE (expires_at IS NULL)
47
+ </pre>
48
+
49
+ h3. Technical discussion
50
+
51
+ SlimScrooge hooks in at just one particular place in the mysql adapter - and that place is the select_all method. All select queries pass through this method.
52
+
53
+ SlimScrooge is able to record each call (and where it came from in your code), and to modify queries that do SELECT * FROM en-route to mysql so that they only select the rows that are actually used by that piece of code.
54
+
55
+ How does SlimScrooge know which columns are actually used?
56
+
57
+ Enter Slim-attributes. Slim-attributes also hooks in at one place, the all_hashes method - and in particular it returns a proxy for the hash for each row, and doesn't actually instantiate any ruby objects for the columns in each row until they are needed.
58
+
59
+ Clearly slim-attributes knows when each column is accessed, and this information can be collected by SlimScrooge for future use.
60
+
61
+ In fact for efficiency, column accesses are only recorded when they are for newly accessed columns. Columns we already know about are accessed completely normally, and slim-attributes provides its usual lazy-loading benefit without any additional overhead.
62
+
63
+ h3. Caveats
64
+
65
+ It is possible to delete an object and then to try to use its attributes to access another object that perhaps must be also deleted (like the option :dependent=>:destroy in Rails associations).
66
+
67
+ SlimScrooge particularly checks for columns used by :dependent=>:destroy, but if you are doing something similar manually in your code then you may run into problems. Your attempt to access the key of the dependent object could cause a reload if the column is not already noted by SlimScrooge, and the reload will fail if you have already destroyed the parent object.
68
+
69
+ h2. Tests
70
+
71
+ SlimScrooge performs the full activerecord test suite without errors, except for a couple of tests that check the number of queries performed.
72
+
73
+ To run the tests you need to set your database up with appropriate access for the rails user, and make activerecord_unittest and activerecord_unittest2 databases. Then run:
74
+
75
+ <pre>
76
+ rake test_with_active_record
77
+ </pre>
78
+
79
+ h2. References
80
+ * "Scrooge":http://github.com/methodmissing/scrooge
81
+ * "Slim-attributes":http://github.com/sdsykes/slim-attributes
82
+
83
+ h2. Authors
84
+ * Stephen Sykes (sdsykes)
85
+ * Special thanks to Lourens Naudé (methodmissing) for the original idea, and the C implementation of callsite_hash as well as some other bits of code that I borrowed from the original project.
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'test/helper'
4
+
5
+ Rake::TestTask.new(:test_with_active_record) do |t|
6
+ t.libs << SlimScrooge::ActiveRecordTest::AR_TEST_SUITE
7
+ t.libs << SlimScrooge::ActiveRecordTest.connection
8
+ t.test_files = SlimScrooge::ActiveRecordTest.test_files
9
+ t.ruby_opts = ["-r #{File.join(File.dirname(__FILE__), 'test', 'active_record_setup')}"]
10
+ t.verbose = true
11
+ end
12
+
13
+ begin
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |s|
16
+ s.name = "slim_scrooge"
17
+ s.summary = "Slim_scrooge - lazy instantiation of attributes and query optimisation for ActiveRecord"
18
+ s.email = "sdsykes@gmail.com"
19
+ s.homepage = "http://github.com/sdsykes/slim_scrooge"
20
+ s.description = "Slim scrooge boosts speed in Rails/Mysql ActiveRecord Models by lazily instantiating attributes as needed, and only querying the database for what is needed."
21
+ s.authors = ["Stephen Sykes"]
22
+ s.files = FileList["[A-Z]*", "{ext,lib,test}/**/*"]
23
+ s.extensions = "ext/extconf.rb"
24
+ s.add_dependency('slim-attributes', '>= 0.7.0')
25
+ end
26
+ Jeweler::GemcutterTasks.new
27
+ rescue LoadError
28
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://
29
+ gems.github.com"
30
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :build:
5
+ :patch: 1
@@ -0,0 +1,67 @@
1
+ /* Author: Lourens Naudé */
2
+
3
+ #include "ruby.h"
4
+ #include "node.h"
5
+ #include "env.h"
6
+
7
+ static int strhash(register const char *string) {
8
+ register int c;
9
+
10
+ register int val = 0;
11
+
12
+ while ((c = *string++) != '\0') {
13
+ val = val*997 + c;
14
+ }
15
+
16
+ return val + (val>>5);
17
+ }
18
+
19
+ static VALUE rb_f_callsite( VALUE obj ) {
20
+ struct FRAME *frame = ruby_frame;
21
+ NODE *n;
22
+ int lev = -1;
23
+ int csite = 0;
24
+
25
+ if (frame->last_func == ID_ALLOCATOR) {
26
+ frame = frame->prev;
27
+ }
28
+ if (lev < 0) {
29
+ ruby_set_current_source();
30
+ if (frame->last_func) {
31
+ csite += strhash(ruby_sourcefile) + ruby_sourceline + frame->last_func;
32
+ }
33
+ else if (ruby_sourceline == 0) {
34
+ csite += strhash(ruby_sourcefile);
35
+ }
36
+ else {
37
+ csite += strhash(ruby_sourcefile) + ruby_sourceline;
38
+ }
39
+ if (lev < -1) return INT2FIX(csite);
40
+ }
41
+ else {
42
+ while (lev-- > 0) {
43
+ frame = frame->prev;
44
+ if (!frame) {
45
+ csite = 0;
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ for (; frame && (n = frame->node); frame = frame->prev) {
51
+ if (frame->prev && frame->prev->last_func) {
52
+ if (frame->prev->node == n) {
53
+ if (frame->prev->last_func == frame->last_func) continue;
54
+ }
55
+ csite += strhash(n->nd_file) + nd_line(n) + frame->prev->last_func;
56
+ }
57
+ else {
58
+ csite += strhash(n->nd_file) + nd_line(n);
59
+ }
60
+ }
61
+
62
+ return INT2FIX(csite);
63
+ }
64
+
65
+ void Init_callsite_hash() {
66
+ rb_define_global_function("callsite_hash", rb_f_callsite, 0);
67
+ }
data/ext/extconf.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'mkmf'
2
+
3
+ dir_config('callsite_hash')
4
+
5
+ create_makefile('callsite_hash')
6
+
7
+ $defs.push("-DRUBY18") if have_var('rb_trap_immediate', ['ruby.h', 'rubysig.h'])
@@ -0,0 +1,68 @@
1
+ # Author: Stephen Sykes
2
+
3
+ module SlimScrooge
4
+ class Callsite
5
+ ScroogeComma = ",".freeze
6
+ ScroogeRegexJoin = /(?:LEFT|INNER|OUTER|CROSS)*\s*(?:STRAIGHT_JOIN|JOIN)/i
7
+
8
+ attr_accessor :seen_columns
9
+ attr_reader :columns_hash, :primary_key, :model_class
10
+
11
+ class << self
12
+ def make_callsite(model_class, original_sql)
13
+ if use_scrooge?(model_class, original_sql)
14
+ new(model_class)
15
+ else
16
+ nil
17
+ end
18
+ end
19
+
20
+ def use_scrooge?(model_class, original_sql)
21
+ original_sql =~ select_regexp(model_class.table_name) &&
22
+ model_class.columns_hash.has_key?(model_class.primary_key) &&
23
+ original_sql !~ ScroogeRegexJoin
24
+ end
25
+
26
+ def select_regexp(table_name)
27
+ %r{SELECT (`?(?:#{table_name})?`?.?\\*) FROM}
28
+ end
29
+ end
30
+
31
+ def initialize(model_class)
32
+ @all_columns = SimpleSet.new(model_class.column_names)
33
+ @model_class = model_class
34
+ @quoted_table_name = model_class.quoted_table_name
35
+ @primary_key = model_class.primary_key
36
+ @columns_hash = model_class.columns_hash
37
+ @select_regexp = self.class.select_regexp(model_class.table_name)
38
+ @seen_columns = SimpleSet.new(essential_columns)
39
+ end
40
+
41
+ def essential_columns
42
+ @model_class.reflect_on_all_associations.inject([@model_class.primary_key]) do |arr, assoc|
43
+ if assoc.options[:dependent] && assoc.macro == :belongs_to
44
+ arr << assoc.association_foreign_key
45
+ end
46
+ arr
47
+ end
48
+ end
49
+
50
+ def scrooged_sql(seen_columns, sql)
51
+ sql.gsub(@select_regexp, "SELECT #{scrooge_select_sql(seen_columns)} FROM")
52
+ end
53
+
54
+ def missing_columns(fetched_columns)
55
+ (@all_columns - SimpleSet.new(fetched_columns)) << @primary_key
56
+ end
57
+
58
+ def reload_sql(primary_keys, fetched_columns)
59
+ sql_keys = primary_keys.collect{|pk| "'#{pk}'"}.join(ScroogeComma)
60
+ cols = scrooge_select_sql(missing_columns(fetched_columns))
61
+ "SELECT #{cols} FROM #{@quoted_table_name} WHERE #{@quoted_table_name}.#{@primary_key} IN (#{sql_keys})"
62
+ end
63
+
64
+ def scrooge_select_sql(set)
65
+ set.collect{|name| "#{@quoted_table_name}.#{name}"}.join(ScroogeComma)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,44 @@
1
+ # Author: Stephen Sykes
2
+
3
+ module SlimScrooge
4
+ class Callsites
5
+ CallsitesMutex = Mutex.new
6
+ @@callsites = {}
7
+
8
+ class << self
9
+ def has_key?(callsite_key)
10
+ @@callsites.has_key?(callsite_key)
11
+ end
12
+
13
+ def [](callsite_key)
14
+ @@callsites[callsite_key]
15
+ end
16
+
17
+ def callsite_key(callsite_hash, sql)
18
+ callsite_hash + sql.gsub(/WHERE.*/i, "").hash
19
+ end
20
+
21
+ def create(sql, callsite_key, name)
22
+ begin
23
+ model_class = name.split.first.constantize
24
+ rescue NameError, NoMethodError
25
+ add_callsite(callsite_key, nil)
26
+ else
27
+ add_callsite(callsite_key, Callsite.make_callsite(model_class, sql))
28
+ end
29
+ end
30
+
31
+ def add_callsite(callsite_key, callsite)
32
+ CallsitesMutex.synchronize do
33
+ @@callsites[callsite_key] = callsite
34
+ end
35
+ end
36
+
37
+ def add_seen_column(callsite, seen_column)
38
+ CallsitesMutex.synchronize do
39
+ callsite.seen_columns << seen_column
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ # Author: Stephen Sykes
2
+
3
+ module SlimScrooge
4
+ class MonitoredHash < Hash
5
+ attr_accessor :callsite, :result_set
6
+
7
+ def self.[](original_hash, callsite, result_set)
8
+ hash = super(original_hash)
9
+ hash.callsite = callsite
10
+ hash.result_set = result_set
11
+ hash
12
+ end
13
+
14
+ def [](name)
15
+ if @callsite.columns_hash.has_key?(name)
16
+ @result_set.reload! if @result_set && name != @callsite.primary_key
17
+ Callsites.add_seen_column(@callsite, name)
18
+ end
19
+ super
20
+ end
21
+
22
+ def []=(name, value)
23
+ if @result_set && @callsite.columns_hash.has_key?(name)
24
+ @result_set.reload!
25
+ end
26
+ super
27
+ end
28
+
29
+ def keys
30
+ @result_set ? @callsite.columns_hash.keys : super
31
+ end
32
+
33
+ def has_key?(name)
34
+ @result_set ? @callsite.columns_hash.has_key?(name) : super
35
+ end
36
+
37
+ alias_method :include?, :has_key?
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ # Author: Stephen Sykes
2
+
3
+ module SlimScrooge
4
+ class ResultSet
5
+ attr_reader :rows, :callsite_key
6
+
7
+ def initialize(rows, callsite_key, fetched_columns)
8
+ @rows = rows
9
+ @callsite_key = callsite_key
10
+ @fetched_columns = fetched_columns
11
+ end
12
+
13
+ def rows_by_key(key)
14
+ @rows.inject({}) {|hash, row| hash[row[key]] = row; hash}
15
+ end
16
+
17
+ def reload!
18
+ callsite = Callsites[@callsite_key]
19
+ rows_hash = rows_by_key(callsite.primary_key)
20
+ sql = callsite.reload_sql(rows_hash.keys, @fetched_columns)
21
+ model_class = callsite.model_class
22
+ new_rows = model_class.connection.send(:select, sql, "#{model_class.name} Reload SlimScrooged")
23
+ new_rows.each do |row|
24
+ if old_row = rows_hash[row[callsite.primary_key]]
25
+ old_row.real_hash.result_set = nil
26
+ row.each {|col, value| old_row[col] = value}
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ module SlimScrooge
2
+ class SimpleSet < Hash
3
+ class << self
4
+ # Creates a new set containing the given objects
5
+ def [](*ary)
6
+ new(ary)
7
+ end
8
+ end
9
+
10
+ # Create a new SimpleSet containing the unique members of _arr_
11
+ def initialize(arr = [])
12
+ Array(arr).each {|x| self[x] = true}
13
+ end
14
+
15
+ # Add a value to the set, and return it
16
+ def <<(value)
17
+ self[value] = true
18
+ self
19
+ end
20
+
21
+ # Invokes block once for each item in the set. Creates an array
22
+ # containing the values returned by the block.
23
+ def collect(&block)
24
+ keys.collect(&block)
25
+ end
26
+
27
+ alias_method :to_a, :keys
28
+
29
+ # Returns set after elements in other have been removed
30
+ def -(other)
31
+ SimpleSet.new(collect {|k| other[k] ? nil : k}.compact)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # Author: Stephen Sykes
2
+
3
+ module SlimScrooge
4
+ module SelectAll
5
+ def self.included(base)
6
+ base.alias_method_chain :select_all, :slim_scrooge
7
+ end
8
+
9
+ def select_all_with_slim_scrooge(sql, name = nil)
10
+ callsite_key = SlimScrooge::Callsites.callsite_key(callsite_hash, sql)
11
+ if SlimScrooge::Callsites.has_key?(callsite_key)
12
+ if callsite = SlimScrooge::Callsites[callsite_key]
13
+ seen_columns = callsite.seen_columns.dup
14
+ rows = select_all_without_slim_scrooge(callsite.scrooged_sql(seen_columns, sql), name + " SlimScrooged")
15
+ result_set = SlimScrooge::ResultSet.new(rows.dup, callsite_key, seen_columns)
16
+ rows.each {|row| row.real_hash = MonitoredHash[{}, callsite, result_set]}
17
+ else
18
+ select_all_without_slim_scrooge(sql, name)
19
+ end
20
+ elsif callsite = SlimScrooge::Callsites.create(sql, callsite_key, name)
21
+ rows = select_all_without_slim_scrooge(sql, name + " SlimScrooged 1st time")
22
+ rows.each {|row| row.real_hash = MonitoredHash[row.to_hash, callsite, nil]}
23
+ rows
24
+ else
25
+ select_all_without_slim_scrooge(sql, name)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:include, SlimScrooge::SelectAll)
@@ -0,0 +1,10 @@
1
+ # Author: Stephen Sykes
2
+
3
+ require 'callsite_hash'
4
+ require 'slim_attributes'
5
+ require 'slim_scrooge/simple_set'
6
+ require 'slim_scrooge/callsites'
7
+ require 'slim_scrooge/callsite'
8
+ require 'slim_scrooge/result_set'
9
+ require 'slim_scrooge/monitored_hash'
10
+ require 'slim_scrooge/slim_scrooge'
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ SlimScrooge::ActiveRecordTest.setup
data/test/helper.rb ADDED
@@ -0,0 +1,91 @@
1
+ require File.join(File.dirname(__FILE__), 'setup')
2
+ require 'active_support/test_case'
3
+
4
+ module SlimScrooge
5
+ class Test
6
+ class << self
7
+
8
+ def setup
9
+ setup_constants
10
+ make_sqlite_config
11
+ make_sqlite_connection
12
+ load_models
13
+ load(SCHEMA_ROOT + "/schema.rb")
14
+ require 'test/unit'
15
+ end
16
+
17
+ def test_files
18
+ glob("#{File.dirname(__FILE__)}/**/*_test.rb")
19
+ end
20
+
21
+ def test_model_files
22
+ %w{course}
23
+ end
24
+
25
+ private
26
+
27
+ def setup_constants
28
+ set_constant('TEST_ROOT') {File.expand_path(File.dirname(__FILE__))}
29
+ set_constant('SCHEMA_ROOT') {TEST_ROOT + "/schema"}
30
+ end
31
+
32
+ def make_sqlite_config
33
+ ActiveRecord::Base.configurations = {
34
+ 'db' => {
35
+ :adapter => 'sqlite3',
36
+ :database => 'test_db',
37
+ :timeout => 5000
38
+ }
39
+ }
40
+ end
41
+
42
+ def load_models
43
+ test_model_files.each {|f| require File.join(File.dirname(__FILE__), "models", f)}
44
+ end
45
+
46
+ def make_sqlite_connection
47
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['db'])
48
+ end
49
+
50
+ def set_constant(constant)
51
+ Object.const_set(constant, yield) unless Object.const_defined?(constant)
52
+ end
53
+
54
+ def glob(pattern)
55
+ Dir.glob(pattern)
56
+ end
57
+ end
58
+ end
59
+
60
+ class ActiveRecordTest < Test
61
+ class << self
62
+ def setup
63
+ setup_constants
64
+ end
65
+
66
+ def test_files
67
+ glob("#{AR_TEST_SUITE}/cases/**/*_test.rb").sort
68
+ end
69
+
70
+ def connection
71
+ File.join(AR_TEST_SUITE, 'connections', 'native_mysql')
72
+ end
73
+
74
+ private
75
+
76
+ def setup_constants
77
+ set_constant('MYSQL_DB_USER') {'rails'}
78
+ set_constant('AR_TEST_SUITE') {find_active_record_test_suite}
79
+ end
80
+
81
+ def find_active_record_test_suite
82
+ ts = ($:).grep(/activerecord/).last.split('/')
83
+ ts.pop
84
+ ts << 'test'
85
+ ts.join('/')
86
+ end
87
+ end
88
+
89
+ AR_TEST_SUITE = find_active_record_test_suite
90
+ end
91
+ end
@@ -0,0 +1,2 @@
1
+ class Course < ActiveRecord::Base
2
+ end
@@ -0,0 +1,5 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :courses, :force => true do |t|
3
+ t.column :name, :string, :null => false
4
+ end
5
+ end
data/test/setup.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'active_record/connection_adapters/mysql_adapter'
4
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ require 'slim_scrooge'
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slim_scrooge
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Sykes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-23 00:00:00 +03:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: slim-attributes
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.7.0
24
+ version:
25
+ description: Slim scrooge boosts speed in Rails/Mysql ActiveRecord Models by lazily instantiating attributes as needed, and only querying the database for what is needed.
26
+ email: sdsykes@gmail.com
27
+ executables: []
28
+
29
+ extensions:
30
+ - ext/extconf.rb
31
+ extra_rdoc_files:
32
+ - README.textile
33
+ files:
34
+ - README.textile
35
+ - Rakefile
36
+ - VERSION.yml
37
+ - ext/callsite_hash.c
38
+ - ext/extconf.rb
39
+ - lib/slim_scrooge.rb
40
+ - lib/slim_scrooge/callsite.rb
41
+ - lib/slim_scrooge/callsites.rb
42
+ - lib/slim_scrooge/monitored_hash.rb
43
+ - lib/slim_scrooge/result_set.rb
44
+ - lib/slim_scrooge/simple_set.rb
45
+ - lib/slim_scrooge/slim_scrooge.rb
46
+ - test/active_record_setup.rb
47
+ - test/helper.rb
48
+ - test/models/course.rb
49
+ - test/schema/schema.rb
50
+ - test/setup.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/sdsykes/slim_scrooge
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --charset=UTF-8
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.5
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Slim_scrooge - lazy instantiation of attributes and query optimisation for ActiveRecord
79
+ test_files:
80
+ - test/active_record_setup.rb
81
+ - test/helper.rb
82
+ - test/models/course.rb
83
+ - test/schema/schema.rb
84
+ - test/setup.rb