surus 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Guardfile +1 -1
- data/README.md +49 -19
- data/bench/benchmark_helper.rb +25 -3
- data/bench/database_structure.sql +55 -0
- data/bench/json_generation.rb +61 -0
- data/lib/surus.rb +8 -0
- data/lib/surus/json/array_agg_query.rb +9 -0
- data/lib/surus/json/association_scope_builder.rb +28 -0
- data/lib/surus/json/belongs_to_scope_builder.rb +13 -0
- data/lib/surus/json/has_and_belongs_to_many_scope_builder.rb +36 -0
- data/lib/surus/json/has_many_scope_builder.rb +22 -0
- data/lib/surus/json/model.rb +17 -0
- data/lib/surus/json/query.rb +78 -0
- data/lib/surus/json/row_query.rb +9 -0
- data/lib/surus/version.rb +1 -1
- data/spec/array/decimal_serializer_spec.rb +4 -4
- data/spec/array/float_serializer_spec.rb +4 -4
- data/spec/array/integer_serializer_spec.rb +3 -3
- data/spec/array/text_serializer_spec.rb +3 -3
- data/spec/database_structure.sql +51 -1
- data/spec/factories.rb +21 -0
- data/spec/hstore/serializer_spec.rb +6 -6
- data/spec/json/json_spec.rb +153 -0
- data/spec/spec_helper.rb +46 -6
- data/spec/synchronous_commit/connection_spec.rb +32 -24
- data/surus.gemspec +8 -3
- data/tmp/rspec_guard_result +1 -0
- metadata +146 -15
data/Guardfile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# A sample Guardfile
|
2
2
|
# More info at https://github.com/guard/guard#readme
|
3
3
|
|
4
|
-
guard 'rspec'
|
4
|
+
guard 'rspec' do
|
5
5
|
watch(%r{^spec/.+_spec\.rb$})
|
6
6
|
watch(%r{^lib/surus/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
7
|
watch('spec/spec_helper.rb') { "spec" }
|
data/README.md
CHANGED
@@ -7,14 +7,15 @@ Surus accelerates ActiveRecord with PostgreSQL specific types and
|
|
7
7
|
functionality. It enables indexed searching of serialized arrays and hashes.
|
8
8
|
It also can control PostgreSQL synchronous commit behavior. By relaxing
|
9
9
|
PostgreSQL's durability guarantee, transaction commit rate can be increased by
|
10
|
-
50% or more.
|
10
|
+
50% or more. It also directly generate JSON in PostgreSQL which can be
|
11
|
+
substantially faster than converting ActiveRecord objects to JSON.
|
11
12
|
|
12
13
|
# Installation
|
13
14
|
|
14
15
|
gem install surus
|
15
|
-
|
16
|
+
|
16
17
|
Or add to your Gemfile.
|
17
|
-
|
18
|
+
|
18
19
|
gem 'surus'
|
19
20
|
|
20
21
|
# Hstore
|
@@ -25,10 +26,10 @@ type that can be indexed for fast searching.
|
|
25
26
|
class User < ActiveRecord::Base
|
26
27
|
serialize :properties, Surus::Hstore::Serializer.new
|
27
28
|
end
|
28
|
-
|
29
|
+
|
29
30
|
User.create :properties => { :favorite_color => "green", :results_per_page => 20 }
|
30
31
|
User.create :properties => { :favorite_colors => ["green", "blue", "red"] }
|
31
|
-
|
32
|
+
|
32
33
|
Even though the underlying hstore can only use strings for keys and values
|
33
34
|
(and NULL for values) Surus can successfully maintain type for integers,
|
34
35
|
floats, bigdecimals, dates, and any value that YAML can serialize. It does
|
@@ -46,32 +47,32 @@ Hstores can be searched with helper scopes.
|
|
46
47
|
User.hstore_has_key(:properties, "favorite_color")
|
47
48
|
User.hstore_has_all_keys(:properties, "favorite_color", "gender")
|
48
49
|
User.hstore_has_any_keys(:properties, "favorite_color", "favorite_artist")
|
49
|
-
|
50
|
+
|
50
51
|
Hstore is a PostgreSQL extension. You can generate a migration to install it.
|
51
52
|
|
52
53
|
rails g surus:hstore:install
|
53
54
|
rake db:migrate
|
54
|
-
|
55
|
-
|
55
|
+
|
56
|
+
|
56
57
|
Read more in the [PostgreSQL hstore documentation](http://www.postgresql.org/docs/9.1/static/hstore.html).
|
57
|
-
|
58
|
+
|
58
59
|
# Array
|
59
60
|
|
60
61
|
Ruby arrays can be serialized to PostgreSQL arrays. Surus includes support
|
61
62
|
for text, integer, float, and decimal arrays.
|
62
63
|
|
63
64
|
class User < ActiveRecord::Base
|
64
|
-
|
65
|
+
|
65
66
|
serialize :favorite_integers, Surus::Array::IntegerSerializer.new
|
66
67
|
serialize :favorite_floats, Surus::Array::FloatSerializer.new
|
67
68
|
serialize :favorite_decimals, Surus::Array::DecimalSerializer.new
|
68
69
|
end
|
69
|
-
|
70
|
+
|
70
71
|
User.create :permissions => %w{ read_notes write_notes, manage_topics },
|
71
72
|
:favorite_integers => [1, 2, 3],
|
72
73
|
:favorite_floats => [1.3, 2.2, 3.1],
|
73
74
|
:favorite_decimals => [BigDecimal("3.14"), BigDecimal("4.23"]
|
74
|
-
|
75
|
+
|
75
76
|
Arrays can be searched with helper scopes.
|
76
77
|
|
77
78
|
User.array_has(:permissions, "admin")
|
@@ -92,15 +93,32 @@ entire session or per transaction.
|
|
92
93
|
User.synchronous_commit false
|
93
94
|
@user.save
|
94
95
|
end # This transaction can return before the data is written to the drive
|
95
|
-
|
96
|
+
|
96
97
|
# synchronous_commit returns to its former value outside of the transaction
|
97
98
|
User.synchronous_commit # -> true
|
98
|
-
|
99
|
+
|
99
100
|
# synchronous_commit can be turned off permanently
|
100
101
|
User.synchronous_commit false
|
101
|
-
|
102
|
+
|
102
103
|
Read more in the [PostgreSQL asynchronous commit documentation](http://www.postgresql.org/docs/9.1/interactive/wal-async-commit.html).
|
103
104
|
|
105
|
+
# JSON
|
106
|
+
|
107
|
+
PostgreSQL 9.2 added `row_to_json` and `array_to_json` functions. These
|
108
|
+
functions can be used to build JSON very quickly. Unfortunately, they are
|
109
|
+
somewhat cumbersome to use. The `find_json` and `all_json` methods are easy to
|
110
|
+
use wrappers around the lower level PostgreSQL functions that closely mimic
|
111
|
+
the Rails `to_json` interface.
|
112
|
+
|
113
|
+
User.find_json 1
|
114
|
+
User.find_json 1, columns: [:id, :name, :email]
|
115
|
+
Post.find_json 1, include: :author
|
116
|
+
User.find_json(user.id, include: {posts: {columns: [:id, :subject]}})
|
117
|
+
User.all_json
|
118
|
+
User.where(admin: true).all_json
|
119
|
+
User.all_json(columns: [:id, :name, :email], include: {posts: {columns: [:id, :subject]}})
|
120
|
+
Post.all_json(include: [:forum, :post])
|
121
|
+
|
104
122
|
# Benchmarks
|
105
123
|
|
106
124
|
Using PostgreSQL's hstore enables searches to be done quickly in the database.
|
@@ -126,6 +144,18 @@ Arrays are also searchable.
|
|
126
144
|
user system total real
|
127
145
|
Surus 0.120000 0.040000 0.160000 ( 0.531735)
|
128
146
|
|
147
|
+
JSON generation is with all_json and find_json is substantially faster than to_json.
|
148
|
+
|
149
|
+
jack@hk-47~/dev/surus$ ruby -I lib -I bench bench/json_generation.rb
|
150
|
+
Generating test data... Done.
|
151
|
+
user system total real
|
152
|
+
find_json: 1 record 500 times 0.140000 0.010000 0.150000 ( 0.205195)
|
153
|
+
to_json: 1 record 500 times 0.240000 0.010000 0.250000 ( 0.287435)
|
154
|
+
find_json: 1 record with 3 associations 500 times 0.480000 0.010000 0.490000 ( 0.796025)
|
155
|
+
to_json: 1 record with 3 associations 500 times 1.130000 0.050000 1.180000 ( 1.500837)
|
156
|
+
all_json: 50 records with 3 associations 20 times 0.030000 0.000000 0.030000 ( 0.090454)
|
157
|
+
to_json: 50 records with 3 associations 20 times 1.350000 0.020000 1.370000 ( 1.710151)
|
158
|
+
|
129
159
|
Disabling synchronous commit can improve commit speed by 50% or more.
|
130
160
|
|
131
161
|
jack@moya:~/work/surus$ ruby -I lib -I bench bench/synchronous_commit.rb
|
@@ -146,12 +176,12 @@ Disabling synchronous commit can improve commit speed by 50% or more.
|
|
146
176
|
disabled per transaction 0.970000 0.850000 1.820000 ( 2.478290)
|
147
177
|
enabled / single transaction 0.890000 0.500000 1.390000 ( 1.693629)
|
148
178
|
disabled / single transaction 0.820000 0.450000 1.270000 ( 1.554767)
|
149
|
-
|
179
|
+
|
150
180
|
Many more benchmarks are in the bench directory. Most accept parameters to
|
151
181
|
adjust the amount of test data.
|
152
|
-
|
182
|
+
|
153
183
|
## Running the benchmarks
|
154
|
-
|
184
|
+
|
155
185
|
1. Create a database
|
156
186
|
2. Configure bench/database.yml to connect to it.
|
157
187
|
3. Load bench/database_structure.sql into your bench database.
|
@@ -159,7 +189,7 @@ adjust the amount of test data.
|
|
159
189
|
the include paths for lib and bench)
|
160
190
|
|
161
191
|
|
162
|
-
|
192
|
+
|
163
193
|
# License
|
164
194
|
|
165
195
|
MIT
|
data/bench/benchmark_helper.rb
CHANGED
@@ -16,17 +16,17 @@ end
|
|
16
16
|
|
17
17
|
class EavMasterRecord < ActiveRecord::Base
|
18
18
|
has_many :eav_detail_records
|
19
|
-
|
19
|
+
|
20
20
|
def properties
|
21
21
|
@properties ||= eav_detail_records.each_with_object({}) do |d, hash|
|
22
22
|
hash[d.key] = d.value
|
23
23
|
end
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
def properties=(value)
|
27
27
|
@properties = value
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
after_save :persist_properties
|
31
31
|
def persist_properties
|
32
32
|
eav_detail_records.clear
|
@@ -54,6 +54,24 @@ end
|
|
54
54
|
class NarrowRecord < ActiveRecord::Base
|
55
55
|
end
|
56
56
|
|
57
|
+
class User < ActiveRecord::Base
|
58
|
+
has_many :posts, foreign_key: :author_id
|
59
|
+
end
|
60
|
+
|
61
|
+
class Forum < ActiveRecord::Base
|
62
|
+
has_many :posts
|
63
|
+
end
|
64
|
+
|
65
|
+
class Post < ActiveRecord::Base
|
66
|
+
belongs_to :forum
|
67
|
+
belongs_to :author, class_name: 'User'
|
68
|
+
has_and_belongs_to_many :tags
|
69
|
+
end
|
70
|
+
|
71
|
+
class Tag < ActiveRecord::Base
|
72
|
+
has_and_belongs_to_many :posts
|
73
|
+
end
|
74
|
+
|
57
75
|
def clean_database
|
58
76
|
YamlKeyValueRecord.delete_all
|
59
77
|
SurusKeyValueRecord.delete_all
|
@@ -62,4 +80,8 @@ def clean_database
|
|
62
80
|
YamlArrayRecord.delete_all
|
63
81
|
WideRecord.delete_all
|
64
82
|
NarrowRecord.delete_all
|
83
|
+
Post.destroy_all # destroy instead of delete so it removes join records in posts_tags
|
84
|
+
Forum.delete_all
|
85
|
+
User.delete_all
|
86
|
+
Tag.delete_all
|
65
87
|
end
|
@@ -90,3 +90,58 @@ CREATE TABLE narrow_records(
|
|
90
90
|
);
|
91
91
|
|
92
92
|
CREATE INDEX ON narrow_records (a);
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
DROP TABLE IF EXISTS users CASCADE;
|
97
|
+
|
98
|
+
CREATE TABLE users(
|
99
|
+
id serial PRIMARY KEY,
|
100
|
+
name varchar NOT NULL,
|
101
|
+
email varchar NOT NULL
|
102
|
+
);
|
103
|
+
|
104
|
+
|
105
|
+
|
106
|
+
DROP TABLE IF EXISTS forums CASCADE;
|
107
|
+
|
108
|
+
CREATE TABLE forums(
|
109
|
+
id serial PRIMARY KEY,
|
110
|
+
name varchar NOT NULL
|
111
|
+
);
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
DROP TABLE IF EXISTS posts CASCADE;
|
116
|
+
|
117
|
+
CREATE TABLE posts(
|
118
|
+
id serial PRIMARY KEY,
|
119
|
+
forum_id integer NOT NULL REFERENCES forums,
|
120
|
+
author_id integer NOT NULL REFERENCES users,
|
121
|
+
subject varchar NOT NULL,
|
122
|
+
body varchar NOT NULL
|
123
|
+
);
|
124
|
+
|
125
|
+
CREATE INDEX ON posts(forum_id);
|
126
|
+
CREATE INDEX ON posts(author_id);
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
DROP TABLE IF EXISTS tags CASCADE;
|
131
|
+
|
132
|
+
CREATE TABLE tags(
|
133
|
+
id serial PRIMARY KEY,
|
134
|
+
name varchar NOT NULL UNIQUE
|
135
|
+
);
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
DROP TABLE IF EXISTS posts_tags CASCADE;
|
140
|
+
|
141
|
+
CREATE TABLE posts_tags(
|
142
|
+
post_id integer NOT NULL REFERENCES posts,
|
143
|
+
tag_id integer NOT NULL REFERENCES tags,
|
144
|
+
PRIMARY KEY (post_id, tag_id)
|
145
|
+
);
|
146
|
+
|
147
|
+
CREATE INDEX ON posts_tags(tag_id);
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'benchmark_helper'
|
2
|
+
require 'faker'
|
3
|
+
require 'factory_girl'
|
4
|
+
FactoryGirl.find_definitions
|
5
|
+
|
6
|
+
print "Generating test data... "
|
7
|
+
|
8
|
+
clean_database
|
9
|
+
|
10
|
+
Forum.transaction do
|
11
|
+
forums = FactoryGirl.create_list :forum, 4
|
12
|
+
users = FactoryGirl.create_list :user, 20
|
13
|
+
tags = FactoryGirl.create_list :tag, 30
|
14
|
+
50.times do
|
15
|
+
FactoryGirl.create :post, forum: forums.sample, author: users.sample, tags: tags.sample(rand(0..5))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
puts "Done."
|
20
|
+
|
21
|
+
num_short_iterations = 500
|
22
|
+
num_long_iterations = 20
|
23
|
+
|
24
|
+
Benchmark.bm(55) do |x|
|
25
|
+
first_post = Post.first
|
26
|
+
x.report("find_json: 1 record #{num_short_iterations} times") do
|
27
|
+
num_short_iterations.times do
|
28
|
+
Post.find_json first_post.id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
x.report("to_json: 1 record #{num_short_iterations} times") do
|
33
|
+
num_short_iterations.times do
|
34
|
+
Post.find(first_post.id).to_json
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
x.report("find_json: 1 record with 3 associations #{num_short_iterations} times") do
|
39
|
+
num_short_iterations.times do
|
40
|
+
Post.find_json first_post.id, include: [:author, :forum, :tags]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
x.report("to_json: 1 record with 3 associations #{num_short_iterations} times") do
|
45
|
+
num_short_iterations.times do
|
46
|
+
Post.includes(:author, :forum).find(first_post.id).to_json include: [:author, :forum, :tags]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
x.report("all_json: 50 records with 3 associations #{num_long_iterations} times") do
|
51
|
+
num_long_iterations.times do
|
52
|
+
Post.all_json include: [:author, :forum, :tags]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
x.report("to_json: 50 records with 3 associations #{num_long_iterations} times") do
|
57
|
+
num_long_iterations.times do
|
58
|
+
Post.includes(:author, :forum).all.to_json include: [:author, :forum, :tags]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/surus.rb
CHANGED
@@ -9,3 +9,11 @@ require 'surus/array/decimal_serializer'
|
|
9
9
|
require 'surus/array/scope'
|
10
10
|
require 'surus/synchronous_commit/connection'
|
11
11
|
require 'surus/synchronous_commit/model'
|
12
|
+
require 'surus/json/query'
|
13
|
+
require 'surus/json/row_query'
|
14
|
+
require 'surus/json/array_agg_query'
|
15
|
+
require 'surus/json/model'
|
16
|
+
require 'surus/json/association_scope_builder'
|
17
|
+
require 'surus/json/belongs_to_scope_builder'
|
18
|
+
require 'surus/json/has_many_scope_builder'
|
19
|
+
require 'surus/json/has_and_belongs_to_many_scope_builder'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Surus
|
2
|
+
module JSON
|
3
|
+
class AssociationScopeBuilder
|
4
|
+
attr_reader :outside_scope
|
5
|
+
attr_reader :association
|
6
|
+
|
7
|
+
def initialize(outside_scope, association)
|
8
|
+
@outside_scope = outside_scope
|
9
|
+
@association = association
|
10
|
+
end
|
11
|
+
|
12
|
+
def outside_class
|
13
|
+
@outside_scope.klass
|
14
|
+
end
|
15
|
+
|
16
|
+
delegate :connection, to: :outside_class
|
17
|
+
delegate :quote_table_name, :quote_column_name, to: :connection
|
18
|
+
|
19
|
+
def conditions
|
20
|
+
association.options[:conditions]
|
21
|
+
end
|
22
|
+
|
23
|
+
def order
|
24
|
+
association.options[:order]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Surus
|
2
|
+
module JSON
|
3
|
+
class BelongsToScopeBuilder < AssociationScopeBuilder
|
4
|
+
def scope
|
5
|
+
association_scope = association
|
6
|
+
.klass
|
7
|
+
.where("#{quote_column_name association.active_record_primary_key}=#{quote_column_name association.foreign_key}")
|
8
|
+
association_scope = association_scope.where(conditions) if conditions
|
9
|
+
association_scope
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Surus
|
2
|
+
module JSON
|
3
|
+
class HasAndBelongsToManyScopeBuilder < AssociationScopeBuilder
|
4
|
+
def scope
|
5
|
+
association_scope = association
|
6
|
+
.klass
|
7
|
+
.joins("JOIN #{join_table} ON #{join_table}.#{association_foreign_key}=#{association_table}.#{association_primary_key}")
|
8
|
+
.where("#{outside_class.quoted_table_name}.#{association_primary_key}=#{join_table}.#{foreign_key}")
|
9
|
+
association_scope = association_scope.where(conditions) if conditions
|
10
|
+
association_scope = association_scope.order(order) if order
|
11
|
+
association_scope
|
12
|
+
end
|
13
|
+
|
14
|
+
def join_table
|
15
|
+
connection.quote_table_name association.options[:join_table]
|
16
|
+
end
|
17
|
+
|
18
|
+
def association_foreign_key
|
19
|
+
connection.quote_column_name association.association_foreign_key
|
20
|
+
end
|
21
|
+
|
22
|
+
def foreign_key
|
23
|
+
connection.quote_column_name association.foreign_key
|
24
|
+
end
|
25
|
+
|
26
|
+
def association_table
|
27
|
+
connection.quote_table_name association.klass.table_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def association_primary_key
|
31
|
+
connection.quote_column_name association.association_primary_key
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Surus
|
2
|
+
module JSON
|
3
|
+
class HasManyScopeBuilder < AssociationScopeBuilder
|
4
|
+
def scope
|
5
|
+
association_scope = association
|
6
|
+
.klass
|
7
|
+
.where("#{outside_primary_key}=#{association_foreign_key}")
|
8
|
+
association_scope = association_scope.where(conditions) if conditions
|
9
|
+
association_scope = association_scope.order(order) if order
|
10
|
+
association_scope
|
11
|
+
end
|
12
|
+
|
13
|
+
def outside_primary_key
|
14
|
+
"#{outside_class.quoted_table_name}.#{connection.quote_column_name association.active_record_primary_key}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def association_foreign_key
|
18
|
+
"#{connection.quote_column_name association.foreign_key}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|