ghazel-slim-attributes 0.6.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT_LICENCE +20 -0
- data/README +44 -0
- data/Rakefile +19 -0
- data/VERSION.yml +4 -0
- data/ext/extconf.rb +26 -0
- data/ext/slim_attrib_ext.c +210 -0
- data/lib/slim_attributes.rb +54 -0
- data/test/benchmark.rb +34 -0
- data/test/database.yml +6 -0
- data/test/products.rb +40 -0
- data/test/slim_attributes_test.rb +169 -0
- data/test/slim_db_test_utils.rb +38 -0
- metadata +65 -0
data/MIT_LICENCE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Stephen Sykes
|
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,44 @@
|
|
1
|
+
SlimAttributes
|
2
|
+
==============
|
3
|
+
|
4
|
+
This is a small patch to the ActiveRecord Mysql adaptor that stops rails from using the all_hashes / each_hash mechanism - which is what is called when you do a find.
|
5
|
+
|
6
|
+
It is faster, and uses less memory.
|
7
|
+
|
8
|
+
Measuring with just ActiveRecord code - fetching stuff from the database - we see anything up to a 50% (or more) speed increase, but I suppose it really depends on your system and environment, and what you are doing with the results from the database. The more columns your tables have, the better the improvement will likely be. Measure your own system and send me the results!
|
9
|
+
|
10
|
+
|
11
|
+
Installation
|
12
|
+
============
|
13
|
+
|
14
|
+
You're going to need the mysql headers for this to work.
|
15
|
+
|
16
|
+
Try:
|
17
|
+
gem install slim-attributes -- --with-mysql-config
|
18
|
+
or:
|
19
|
+
gem install slim-attributes
|
20
|
+
|
21
|
+
then add this to environment.rb:
|
22
|
+
require 'slim_attributes'
|
23
|
+
|
24
|
+
|
25
|
+
Description
|
26
|
+
===========
|
27
|
+
|
28
|
+
The reason for overriding all_hashes is threefold:
|
29
|
+
|
30
|
+
* making a hash of each and every row returned from the database is slow
|
31
|
+
* ruby makes frozen copies of each column name string (for the keys) which results in a great many strings which are not really needed
|
32
|
+
* we observe that it's not often that all the fields of rows fetched from the database are actually used
|
33
|
+
|
34
|
+
So this is an alternative implementation of all_hashes that returns a 'fake hash' which contains a hash of the column names (the same hash of names is used for every row), and also contains the row data in an area memcpy'd directly from the mysql API.
|
35
|
+
|
36
|
+
The field contents are then instantiated into Ruby strings on demand - ruby strings are only made if you need them. Note that if you always look at all the columns when you fetch data from the database then this won't necessarily be faster that the unpatched mysql adapter. But it won't be much slower either, and we do expect that most times not all the columns from a result set are accessed.
|
37
|
+
|
38
|
+
|
39
|
+
===========
|
40
|
+
|
41
|
+
No warranty - this gem has been tested, but not in all environments. However, that said, we are using it in our production environment with good results.
|
42
|
+
Please report bugs to sdsykes at symbol gmail pip com.
|
43
|
+
|
44
|
+
Copyright (c) 2008 Stephen Sykes, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |s|
|
6
|
+
s.name = "slim-attributes"
|
7
|
+
s.summary = "Slim-attributes - lazy instantiation of attributes for ActiveRecord"
|
8
|
+
s.email = "sdsykes@gmail.com"
|
9
|
+
s.homepage = "http://github.com/sdsykes/slim-attributes"
|
10
|
+
s.description = "Slim attributes boosts speed in Rails/Mysql ActiveRecord Models by avoiding
|
11
|
+
instantiating Hashes for each result row, and lazily instantiating attributes as needed."
|
12
|
+
s.authors = ["Stephen Sykes"]
|
13
|
+
s.files = FileList["[A-Z]*", "{ext,lib,test}/**/*"]
|
14
|
+
s.extensions = "ext/extconf.rb"
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://
|
18
|
+
gems.github.com"
|
19
|
+
end
|
data/VERSION.yml
ADDED
data/ext/extconf.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# this code is borrowed from Mysql/Ruby, credit to TOMITA Masahiro
|
2
|
+
# it works for me under OS X and Fedora, hope it works for you also
|
3
|
+
|
4
|
+
require 'mkmf'
|
5
|
+
|
6
|
+
if /mswin32/ =~ RUBY_PLATFORM
|
7
|
+
inc, lib = dir_config('mysql')
|
8
|
+
exit 1 unless have_library("libmysql")
|
9
|
+
elsif mc = with_config('mysql-config') then
|
10
|
+
mc = 'mysql_config' if mc == true
|
11
|
+
cflags = `#{mc} --cflags`.chomp
|
12
|
+
exit 1 if $? != 0
|
13
|
+
libs = `#{mc} --libs`.chomp
|
14
|
+
exit 1 if $? != 0
|
15
|
+
$CPPFLAGS += ' ' + cflags
|
16
|
+
$libs = libs + " " + $libs
|
17
|
+
else
|
18
|
+
inc, lib = dir_config('mysql', '/usr/local')
|
19
|
+
libs = ['m', 'z', 'socket', 'nsl', 'mygcc']
|
20
|
+
while not find_library('mysqlclient', 'mysql_query', lib, "#{lib}/mysql") do
|
21
|
+
exit 1 if libs.empty?
|
22
|
+
have_library(libs.shift)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
create_makefile("slim_attrib_ext")
|
@@ -0,0 +1,210 @@
|
|
1
|
+
// Author: Stephen Sykes
|
2
|
+
// http://pennysmalls.com
|
3
|
+
|
4
|
+
#include "ruby.h"
|
5
|
+
#include "st.h"
|
6
|
+
|
7
|
+
#include <mysql.h>
|
8
|
+
#include <errmsg.h>
|
9
|
+
#include <mysqld_error.h>
|
10
|
+
|
11
|
+
#define GetMysqlRes(obj) (Check_Type(obj, T_DATA), ((struct mysql_res*)DATA_PTR(obj))->res)
|
12
|
+
#define GetCharPtr(obj) (Check_Type(obj, T_DATA), (char*)DATA_PTR(obj))
|
13
|
+
#define GetCharStarPtr(obj) (Check_Type(obj, T_DATA), (char**)DATA_PTR(obj))
|
14
|
+
|
15
|
+
VALUE cRowHash, cClass;
|
16
|
+
ID pointers_id, row_info_id, field_indexes_id, real_hash_id, to_hash_id;
|
17
|
+
|
18
|
+
#define MAX_CACHED_COLUMN_IDS 40
|
19
|
+
ID column_ids[MAX_CACHED_COLUMN_IDS];
|
20
|
+
|
21
|
+
// from mysql/ruby
|
22
|
+
struct mysql_res {
|
23
|
+
MYSQL_RES* res;
|
24
|
+
char freed;
|
25
|
+
};
|
26
|
+
|
27
|
+
// row info
|
28
|
+
#define SLIM_IS_NULL (char)0x01
|
29
|
+
#define SLIM_IS_SET (char)0x02
|
30
|
+
|
31
|
+
#define GET_COL_IV_ID(str, cnum) (cnum < MAX_CACHED_COLUMN_IDS ? column_ids[cnum] : (sprintf(str, "@col_%ld", cnum), rb_intern(str)))
|
32
|
+
|
33
|
+
#define REAL_HASH_EXISTS NIL_P(field_indexes = rb_ivar_get(obj, field_indexes_id))
|
34
|
+
|
35
|
+
// This replaces the usual all_hashes method defined in mysql_adaptor.rb
|
36
|
+
//
|
37
|
+
// It copies the data from the mysql result into allocated memory
|
38
|
+
// ready for creating ruby strings from on demand, instead of creating
|
39
|
+
// all the data into ruby strings immediately.
|
40
|
+
// all_hashes returns an array of result rows
|
41
|
+
static VALUE all_hashes(VALUE obj) {
|
42
|
+
MYSQL_RES *res = GetMysqlRes(obj);
|
43
|
+
MYSQL_FIELD *fields = mysql_fetch_fields(res);
|
44
|
+
MYSQL_ROW row;
|
45
|
+
VALUE all_hashes_ary, col_names_hsh, frh;
|
46
|
+
my_ulonglong nr = mysql_num_rows(res);
|
47
|
+
unsigned int nf = mysql_num_fields(res);
|
48
|
+
unsigned int i, j, s, len;
|
49
|
+
unsigned long *lengths;
|
50
|
+
char *row_info_space, **pointers_space, *p;
|
51
|
+
|
52
|
+
// make a hash of column names
|
53
|
+
col_names_hsh = rb_hash_new();
|
54
|
+
for (i=0; i < nf; i++) {
|
55
|
+
rb_hash_aset(col_names_hsh, rb_str_new2(fields[i].name), INT2FIX(i));
|
56
|
+
}
|
57
|
+
|
58
|
+
// make the array of result rows
|
59
|
+
all_hashes_ary = rb_ary_new2(nr);
|
60
|
+
|
61
|
+
for (i=0; i < nr; i++) {
|
62
|
+
row = mysql_fetch_row(res); // get the row data and lengths from mysql
|
63
|
+
lengths = mysql_fetch_lengths(res);
|
64
|
+
for (s=j=0; j < nf; j++) s += lengths[j]; // s = total of lengths
|
65
|
+
pointers_space = ruby_xmalloc((nf + 1) * sizeof(char *) + s); // space for data pointers & data
|
66
|
+
if (!pointers_space) {
|
67
|
+
rb_raise(rb_eNoMemError, "out of memory");
|
68
|
+
}
|
69
|
+
p = *pointers_space = (char *)(pointers_space + nf + 1); // pointer to first data item
|
70
|
+
row_info_space = ruby_xcalloc(nf, 1); // space for flags for each column in this row
|
71
|
+
if (!row_info_space) {
|
72
|
+
ruby_xfree(pointers_space);
|
73
|
+
rb_raise(rb_eNoMemError, "out of memory");
|
74
|
+
}
|
75
|
+
for (j=0; j < nf; j++) {
|
76
|
+
len = (unsigned int)lengths[j];
|
77
|
+
if (len) {
|
78
|
+
memcpy(p, row[j], len); // copy row data in
|
79
|
+
p += len;
|
80
|
+
} else if (!row[j]) row_info_space[j] = SLIM_IS_NULL; // flag so we can handle null
|
81
|
+
pointers_space[j + 1] = p;
|
82
|
+
}
|
83
|
+
frh = rb_class_new_instance(0, NULL, cRowHash); // create the row object
|
84
|
+
rb_ivar_set(frh, pointers_id, Data_Wrap_Struct(cClass, 0, ruby_xfree, pointers_space));
|
85
|
+
rb_ivar_set(frh, row_info_id, Data_Wrap_Struct(cClass, 0, ruby_xfree, row_info_space));
|
86
|
+
rb_ivar_set(frh, field_indexes_id, col_names_hsh);
|
87
|
+
rb_ary_store(all_hashes_ary, i, frh); // store it in the array
|
88
|
+
}
|
89
|
+
return all_hashes_ary;
|
90
|
+
}
|
91
|
+
|
92
|
+
// This does the actual work of creating a ruby string when one is demanded, typically through
|
93
|
+
// a call to an active record model property method.
|
94
|
+
static VALUE fetch_by_index(VALUE obj, VALUE index) {
|
95
|
+
VALUE contents;
|
96
|
+
char *row_info, **pointers, *start, col_name[16];
|
97
|
+
ID col_id;
|
98
|
+
long col_number = FIX2LONG(index);
|
99
|
+
unsigned int length;
|
100
|
+
row_info = GetCharPtr(rb_ivar_get(obj, row_info_id)) + col_number; // flags for this column
|
101
|
+
if (*row_info == SLIM_IS_NULL) return Qnil; // return nil if null from db
|
102
|
+
col_id = GET_COL_IV_ID(col_name, col_number);
|
103
|
+
if (*row_info == SLIM_IS_SET) return rb_ivar_get(obj, col_id); // was made to a string already
|
104
|
+
pointers = GetCharStarPtr(rb_ivar_get(obj, pointers_id)); // find the data and make ruby string
|
105
|
+
start = pointers[col_number];
|
106
|
+
length = pointers[col_number + 1] - start;
|
107
|
+
contents = rb_tainted_str_new(start, length);
|
108
|
+
rb_ivar_set(obj, col_id, contents); // it is efficient to save the string in an instance variable
|
109
|
+
*row_info = SLIM_IS_SET;
|
110
|
+
return contents;
|
111
|
+
}
|
112
|
+
|
113
|
+
// This is the [] method of the row data object.
|
114
|
+
// It checks for a real hash, but if none exists it will call fetch_by_index
|
115
|
+
static VALUE slim_fetch(VALUE obj, VALUE name) {
|
116
|
+
VALUE field_indexes, hash_lookup;
|
117
|
+
|
118
|
+
if (REAL_HASH_EXISTS) return rb_hash_aref(rb_ivar_get(obj, real_hash_id), name);
|
119
|
+
|
120
|
+
hash_lookup = rb_hash_aref(field_indexes, name);
|
121
|
+
if (NIL_P(hash_lookup)) return Qnil;
|
122
|
+
return fetch_by_index(obj, hash_lookup);
|
123
|
+
}
|
124
|
+
|
125
|
+
// This is the []= method of the row data object.
|
126
|
+
// It either operates on the real hash if it exists, or sets the appropriate
|
127
|
+
// column instance variable
|
128
|
+
static VALUE set_element(VALUE obj, VALUE name, VALUE val) {
|
129
|
+
VALUE field_indexes, hash_lookup;
|
130
|
+
long col_number;
|
131
|
+
char col_name[16];
|
132
|
+
ID col_id;
|
133
|
+
|
134
|
+
if (REAL_HASH_EXISTS) return rb_hash_aset(rb_ivar_get(obj, real_hash_id), name, val);
|
135
|
+
|
136
|
+
hash_lookup = rb_hash_aref(field_indexes, name);
|
137
|
+
if (NIL_P(hash_lookup)) return rb_hash_aset(rb_funcall(obj, to_hash_id, 0), name, val);
|
138
|
+
col_number = FIX2LONG(hash_lookup);
|
139
|
+
col_id = GET_COL_IV_ID(col_name, col_number);
|
140
|
+
rb_ivar_set(obj, col_id, val);
|
141
|
+
GetCharPtr(rb_ivar_get(obj, row_info_id))[col_number] = SLIM_IS_SET;
|
142
|
+
return val;
|
143
|
+
}
|
144
|
+
|
145
|
+
// This is the dup method of the row data object.
|
146
|
+
// When the query cache is used, the result of all_hashes is dupped before
|
147
|
+
// being passed back to the user.
|
148
|
+
// Subsequent queries of the same SQL will get another dup of the results.
|
149
|
+
// So we must implement dup in an efficient way (without converting to a real hash).
|
150
|
+
//
|
151
|
+
// Note: this method currently ignores any columns that have been assigned to using
|
152
|
+
// []= before calling dup (the original values will be seen in the dup). This works ok
|
153
|
+
// for active record usage, but perhaps could cause unexpected behaviour if model
|
154
|
+
// attributes are dupped by the user after changing them.
|
155
|
+
static VALUE slim_dup(VALUE obj) {
|
156
|
+
VALUE frh, field_indexes;
|
157
|
+
int nf, i;
|
158
|
+
char *row_info_space;
|
159
|
+
|
160
|
+
if (REAL_HASH_EXISTS) return rb_obj_dup(rb_ivar_get(obj, real_hash_id));
|
161
|
+
|
162
|
+
nf = RHASH(field_indexes)->tbl->num_entries;
|
163
|
+
row_info_space = ruby_xmalloc(nf); // dup needs its own set of flags
|
164
|
+
if (!row_info_space) rb_raise(rb_eNoMemError, "out of memory");
|
165
|
+
memcpy(row_info_space, GetCharPtr(rb_ivar_get(obj, row_info_id)), nf);
|
166
|
+
for (i=0; i < nf; i++) row_info_space[i] &= ~SLIM_IS_SET; // remove any set flags
|
167
|
+
frh = rb_class_new_instance(0, NULL, cRowHash); // make the new row data object
|
168
|
+
rb_ivar_set(frh, pointers_id, rb_ivar_get(obj, pointers_id));
|
169
|
+
rb_ivar_set(frh, row_info_id, Data_Wrap_Struct(cClass, 0, ruby_xfree, row_info_space));
|
170
|
+
rb_ivar_set(frh, field_indexes_id, field_indexes);
|
171
|
+
return frh;
|
172
|
+
}
|
173
|
+
|
174
|
+
// This is the has_key? method of the row data object.
|
175
|
+
// Calls to model property methods in AR cause a call to has_key?, so it
|
176
|
+
// is implemented here in C for speed.
|
177
|
+
static VALUE has_key(VALUE obj, VALUE name) {
|
178
|
+
VALUE field_indexes;
|
179
|
+
|
180
|
+
if (REAL_HASH_EXISTS) return (st_lookup(RHASH(rb_ivar_get(obj, real_hash_id))->tbl, name, 0) ? Qtrue : Qfalse);
|
181
|
+
else return (st_lookup(RHASH(field_indexes)->tbl, name, 0) ? Qtrue : Qfalse);
|
182
|
+
}
|
183
|
+
|
184
|
+
void Init_slim_attrib_ext() {
|
185
|
+
int i;
|
186
|
+
char col_name[16];
|
187
|
+
VALUE c = rb_cObject;
|
188
|
+
|
189
|
+
c = rb_const_get_at(c, rb_intern("Mysql"));
|
190
|
+
c = rb_const_get_at(c, rb_intern("Result"));
|
191
|
+
rb_define_method(c, "all_hashes", (VALUE(*)(ANYARGS))all_hashes, 0);
|
192
|
+
cRowHash = rb_const_get_at(c, rb_intern("RowHash"));
|
193
|
+
cClass = rb_define_class("CObjects", cRowHash);
|
194
|
+
// set up methods
|
195
|
+
rb_define_private_method(cRowHash, "fetch_by_index", (VALUE(*)(ANYARGS))fetch_by_index, 1);
|
196
|
+
rb_define_method(cRowHash, "[]", (VALUE(*)(ANYARGS))slim_fetch, 1);
|
197
|
+
rb_define_method(cRowHash, "[]=", (VALUE(*)(ANYARGS))set_element, 2);
|
198
|
+
rb_define_method(cRowHash, "dup", (VALUE(*)(ANYARGS))slim_dup, 0);
|
199
|
+
rb_define_method(cRowHash, "has_key?", (VALUE(*)(ANYARGS))has_key, 1);
|
200
|
+
// set up some symbols that we will need
|
201
|
+
pointers_id = rb_intern("@pointers");
|
202
|
+
row_info_id = rb_intern("@row_info");
|
203
|
+
field_indexes_id = rb_intern("@field_indexes");
|
204
|
+
real_hash_id = rb_intern("@real_hash");
|
205
|
+
to_hash_id = rb_intern("to_hash");
|
206
|
+
for(i=0; i < MAX_CACHED_COLUMN_IDS; i++) {
|
207
|
+
sprintf(col_name, "@col_%d", i);
|
208
|
+
column_ids[i] = rb_intern(col_name);
|
209
|
+
}
|
210
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Author: Stephen Sykes
|
2
|
+
# http://pennysmalls.com
|
3
|
+
|
4
|
+
require 'mysql'
|
5
|
+
|
6
|
+
class Mysql::Result; class RowHash; end; end
|
7
|
+
|
8
|
+
require 'slim_attrib_ext'
|
9
|
+
|
10
|
+
class Mysql::Result
|
11
|
+
class RowHash
|
12
|
+
def marshal_dump
|
13
|
+
to_hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def marshal_load(hash)
|
17
|
+
@real_hash = hash
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :include?, :has_key?
|
21
|
+
|
22
|
+
def keys
|
23
|
+
@real_hash ? @real_hash.keys : @field_indexes.keys
|
24
|
+
end
|
25
|
+
|
26
|
+
# If you want to do anything other than [], []=, dup, keys and has_key? then
|
27
|
+
# we'll handle that by doing the operation on a real ruby hash.
|
28
|
+
# This should be the exception though, and the efficiencies of using slim-attributes
|
29
|
+
# are lost when this happens.
|
30
|
+
def to_hash
|
31
|
+
return @real_hash if @real_hash
|
32
|
+
@real_hash = {}
|
33
|
+
@field_indexes.each_pair {|name, index| @real_hash[name] = fetch_by_index(index)}
|
34
|
+
@field_indexes = nil
|
35
|
+
@real_hash
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_a
|
39
|
+
to_hash.to_a
|
40
|
+
end
|
41
|
+
|
42
|
+
# Load up all the attributes before a freeze
|
43
|
+
alias_method :regular_freeze, :freeze
|
44
|
+
|
45
|
+
def freeze
|
46
|
+
to_hash.freeze
|
47
|
+
regular_freeze
|
48
|
+
end
|
49
|
+
|
50
|
+
def method_missing(name, *args, &block)
|
51
|
+
to_hash.send(name, *args, &block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/test/benchmark.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Author: Stephen Sykes
|
2
|
+
# http://pennysmalls.com
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'active_record'
|
6
|
+
require File.dirname(__FILE__) + "/products"
|
7
|
+
require File.dirname(__FILE__) + "/slim_db_test_utils"
|
8
|
+
|
9
|
+
SlimDbTestUtils.connect_and_create_db
|
10
|
+
Product.create_large_product_table
|
11
|
+
Product.make_some_products
|
12
|
+
|
13
|
+
def do_benchmark(name)
|
14
|
+
start_t = Time.now
|
15
|
+
|
16
|
+
2000.times do |n|
|
17
|
+
product = Product.find(:all)[n % 100]
|
18
|
+
x = product.name
|
19
|
+
y = product.comment
|
20
|
+
z = product.created_at
|
21
|
+
end
|
22
|
+
|
23
|
+
end_t = Time.now
|
24
|
+
time_taken = end_t - start_t
|
25
|
+
puts "#{name}: #{time_taken}s"
|
26
|
+
time_taken
|
27
|
+
end
|
28
|
+
|
29
|
+
time1 = do_benchmark("Without slim-attributes")
|
30
|
+
require 'slim_attributes'
|
31
|
+
time2 = do_benchmark("With slim-attributes")
|
32
|
+
puts "Diff: #{"%.2f"%(100 - time2 / time1 * 100)}%"
|
33
|
+
|
34
|
+
SlimDbTestUtils.remove_db
|
data/test/database.yml
ADDED
data/test/products.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Author: Stephen Sykes
|
2
|
+
# http://pennysmalls.com
|
3
|
+
|
4
|
+
class Product < ActiveRecord::Base
|
5
|
+
|
6
|
+
def self.create_product_table
|
7
|
+
ActiveRecord::Base.connection.drop_table(:products) rescue ActiveRecord::StatementInvalid
|
8
|
+
ActiveRecord::Base.connection.create_table(:products) do |t|
|
9
|
+
t.column :name, :string, :limit => 60
|
10
|
+
t.column :created_at, :datetime
|
11
|
+
t.column :number, :integer
|
12
|
+
t.column :nil_test, :string
|
13
|
+
t.column :comment, :text
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create_large_product_table
|
18
|
+
ActiveRecord::Base.connection.drop_table(:products) rescue ActiveRecord::StatementInvalid
|
19
|
+
ActiveRecord::Base.connection.create_table(:products) do |t|
|
20
|
+
t.column :name, :string, :limit => 60
|
21
|
+
t.column :created_at, :datetime
|
22
|
+
t.column :number, :integer
|
23
|
+
t.column :nil_test, :string
|
24
|
+
t.column :comment, :text
|
25
|
+
40.times {|n| t.column "test_col_#{n}", :string}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def self.make_some_products
|
31
|
+
100.times do |n|
|
32
|
+
Product.create(:name=>"product_#{n}", :number=>n, :comment=>"Made by the test suite")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def attributes_iv
|
37
|
+
@attributes
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# Author: Stephen Sykes
|
2
|
+
# http://pennysmalls.com
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'active_record'
|
6
|
+
require 'active_record/version'
|
7
|
+
require 'slim_attributes'
|
8
|
+
require 'test/unit'
|
9
|
+
require File.dirname(__FILE__) + "/products"
|
10
|
+
require File.dirname(__FILE__) + "/slim_db_test_utils"
|
11
|
+
|
12
|
+
class SlimAttributesTest < Test::Unit::TestCase
|
13
|
+
def setup
|
14
|
+
SlimDbTestUtils.connect_and_create_db
|
15
|
+
Product.create_product_table
|
16
|
+
Product.make_some_products
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_finds_all
|
20
|
+
items = Product.find(:all)
|
21
|
+
assert items.size == 100, "must find all 100 items"
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_items_have_correct_attributes
|
25
|
+
items = Product.find(:all, :order=>"id")
|
26
|
+
items.each_index do |i|
|
27
|
+
check_attributes_for(items[i], i)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_item_attributes_can_be_changed
|
32
|
+
item = Product.find(:first)
|
33
|
+
old_name = item.name.dup
|
34
|
+
item.name << "more"
|
35
|
+
assert_equal old_name + "more", item.name, "change must stick"
|
36
|
+
item.nil_test = "not a nil"
|
37
|
+
assert_equal "not a nil", item.nil_test, "change must stick"
|
38
|
+
item.name = "something else"
|
39
|
+
assert_equal "something else", item.name, "change must stick"
|
40
|
+
item.save
|
41
|
+
item = Product.find(:first)
|
42
|
+
assert_equal "something else", item.name, "change must persist in DB"
|
43
|
+
assert_equal "not a nil", item.nil_test, "change must persist in DB"
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_item_attributes_to_a_works
|
47
|
+
item = Product.find_by_id(1)
|
48
|
+
arr = item.attributes_iv.to_a
|
49
|
+
expected = [["id","1"], ["name", "product_0"], ["number","0"], ["comment","Made by the test suite"], ["created_at", item.created_at_before_type_cast], ["nil_test", nil]]
|
50
|
+
arr.each do |a|
|
51
|
+
assert expected.include?(a), "array must match"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_item_can_be_marshalled
|
56
|
+
item = Product.find_by_id(1)
|
57
|
+
mi = Marshal.load(Marshal.dump(item))
|
58
|
+
check_attributes_for(mi, 0)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_has_key_and_include
|
62
|
+
item = Product.find_by_id(1)
|
63
|
+
assert item.attributes_iv.has_key?("name"), "must have key name"
|
64
|
+
assert item.attributes_iv.include?("name"), "must have key name"
|
65
|
+
assert !item.attributes_iv.has_key?("name1"), "must not have key name1"
|
66
|
+
assert !item.attributes_iv.include?("name1"), "must not have key name1"
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_keys
|
70
|
+
item = Product.find_by_id(1)
|
71
|
+
assert_equal ["id", "name", "created_at", "number", "comment", "nil_test"].sort, item.attributes_iv.keys.sort, "keys must work"
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_to_hash
|
75
|
+
item = Product.find_by_id(1)
|
76
|
+
expected = {"id"=>"1", "name"=>"product_0", "number"=>"0", "comment"=>"Made by the test suite", "created_at"=>item.created_at_before_type_cast, "nil_test"=>nil}
|
77
|
+
hash = item.attributes_iv.to_hash
|
78
|
+
assert_equal Hash, hash.class, "to_hash must result in a Hash"
|
79
|
+
assert_equal expected, hash, "to_hash must work"
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_fake_hash_can_be_updated
|
83
|
+
item = Product.find_by_id(1)
|
84
|
+
old_name = item.name
|
85
|
+
item.attributes_iv.update("name"=>"foobar")
|
86
|
+
assert_equal "foobar", item.name, "update must work"
|
87
|
+
item.name_will_change! if ActiveRecord::VERSION::STRING >= "2.1.0"
|
88
|
+
item.save
|
89
|
+
item = Product.find_by_id(1)
|
90
|
+
assert_equal "foobar", item.name, "update must work and stick through db"
|
91
|
+
assert_equal 0, item.number, "other attributes must not be changed in db"
|
92
|
+
item.name = old_name
|
93
|
+
item.save
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_can_assign_to_non_columns_in_hash
|
97
|
+
item = Product.find_by_id(1)
|
98
|
+
fh = item.attributes_iv
|
99
|
+
fh["something"] = 23
|
100
|
+
fh[:symbol] = "23"
|
101
|
+
assert_equal 23, fh["something"], "assignment to non col"
|
102
|
+
assert_equal "23", fh[:symbol], "assignment to non col"
|
103
|
+
assert fh.keys.include?("something"), "keys must include new"
|
104
|
+
assert fh.keys.include?(:symbol), "keys must include new"
|
105
|
+
assert fh.keys.include?("name"), "keys must still include old"
|
106
|
+
assert_equal 23, item.something, "non col becomes accessible with method call"
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_key_can_be_deleted
|
110
|
+
item = Product.find_by_id(1)
|
111
|
+
fh = item.attributes_iv
|
112
|
+
fh.delete("name")
|
113
|
+
assert_nil fh["name"], "name must be nil now"
|
114
|
+
assert_raises(ActiveRecord::MissingAttributeError, "name must raise on method call") {item.name}
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_gc
|
118
|
+
GC.start
|
119
|
+
assert true, "gc didn't crash"
|
120
|
+
end
|
121
|
+
|
122
|
+
# rails query cache uses dup
|
123
|
+
def test_dup
|
124
|
+
items = Product.find(:all)
|
125
|
+
attr_dup = items[0].attributes_iv.dup
|
126
|
+
assert_equal "product_0", attr_dup["name"], "name must be correct in dup'd attributes"
|
127
|
+
attr_dup = items[0].attributes_iv.dup
|
128
|
+
assert_equal "product_0", attr_dup["name"], "name must be correct in dup'd attributes 2nd time"
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_cached_result
|
132
|
+
ActiveRecord::Base.connection.cache do
|
133
|
+
item1 = Product.find_by_id(1)
|
134
|
+
item1.name = "foo"
|
135
|
+
item2 = Product.find_by_id(1)
|
136
|
+
assert_equal "product_0", item2.name, "name must be original from cached query"
|
137
|
+
item3 = Product.find_by_id(1)
|
138
|
+
item1.name = "bar"
|
139
|
+
assert_equal "product_0", item3.name, "name must be original from cached query"
|
140
|
+
item2.name << "_test"
|
141
|
+
# unmodified rails fails this test - but does it matter either way?
|
142
|
+
check_attributes_for(item3, 0)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_accessing_destroyed_object_attributes
|
147
|
+
item1 = Product.find_by_id(1)
|
148
|
+
assert_equal false, item1.frozen?
|
149
|
+
item1.destroy # object is frozen
|
150
|
+
assert_equal true, item1.frozen?
|
151
|
+
check_attributes_for(item1, 0)
|
152
|
+
assert_raises(TypeError) {item1.name = "another product"}
|
153
|
+
end
|
154
|
+
|
155
|
+
def teardown
|
156
|
+
SlimDbTestUtils.remove_db
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def check_attributes_for(item, i)
|
162
|
+
assert_equal "product_#{i}", item.name, "item name must be right"
|
163
|
+
assert_equal i, item.number, "item number must be right"
|
164
|
+
assert_equal i + 1, item.id, "item id must be right"
|
165
|
+
assert_equal "Made by the test suite", item.comment, "item comment must be right"
|
166
|
+
assert ((1.minute.ago)..(Time.now)) === item.created_at, "item created_at must be reasonable"
|
167
|
+
assert_nil item.nil_test, "nil_test must be nil"
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Author: Stephen Sykes
|
2
|
+
# http://pennysmalls.com
|
3
|
+
|
4
|
+
module SlimDbTestUtils
|
5
|
+
DB_NAME = "slim_attributes_test"
|
6
|
+
|
7
|
+
def self.db_config
|
8
|
+
@config ||= YAML.load(File.read(File.dirname(__FILE__) + "/database.yml"))
|
9
|
+
@config[DB_NAME]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.connect_and_create_db
|
13
|
+
connect_with_config(db_config)
|
14
|
+
unless ActiveRecord::Base.connected? # database did not exist (as expected)
|
15
|
+
connect_with_config(db_config.merge({"database"=>nil}))
|
16
|
+
ActiveRecord::Base.connection.create_database(db_config["database"])
|
17
|
+
connect_with_config(db_config)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.connect_with_config(config)
|
22
|
+
begin
|
23
|
+
ActiveRecord::Base.establish_connection(config)
|
24
|
+
ActiveRecord::Base.connection
|
25
|
+
rescue Mysql::Error
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.remove_db
|
30
|
+
if ActiveRecord::Base.connected?
|
31
|
+
ActiveRecord::Base.connection.execute("DROP DATABASE #{db_config["database"]}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.import_sql(file)
|
36
|
+
`mysql -p#{db_config["password"]} -u #{db_config["username"]} #{db_config["database"]} < #{File.dirname(__FILE__)}/#{file}`
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ghazel-slim-attributes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.6.3.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen Sykes
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-12 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Slim attributes boosts speed in Rails/Mysql ActiveRecord Models by avoiding instantiating Hashes for each result row, and lazily instantiating attributes as needed.
|
17
|
+
email: sdsykes@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions:
|
21
|
+
- ext/extconf.rb
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- MIT_LICENCE
|
26
|
+
- Rakefile
|
27
|
+
- README
|
28
|
+
- VERSION.yml
|
29
|
+
- ext/extconf.rb
|
30
|
+
- ext/slim_attrib_ext.c
|
31
|
+
- lib/slim_attributes.rb
|
32
|
+
- test/benchmark.rb
|
33
|
+
- test/database.yml
|
34
|
+
- test/products.rb
|
35
|
+
- test/slim_attributes_test.rb
|
36
|
+
- test/slim_db_test_utils.rb
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: http://github.com/sdsykes/slim-attributes
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options:
|
41
|
+
- --inline-source
|
42
|
+
- --charset=UTF-8
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.2.0
|
61
|
+
signing_key:
|
62
|
+
specification_version: 2
|
63
|
+
summary: Slim-attributes - lazy instantiation of attributes for ActiveRecord
|
64
|
+
test_files: []
|
65
|
+
|