surus 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|