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 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', :version => 2 do
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
- serialize :permissions, Surus::Array::TextSerializer.new
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
@@ -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
@@ -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,9 @@
1
+ module Surus
2
+ module JSON
3
+ class ArrayAggQuery < Query
4
+ def to_sql
5
+ "select array_to_json(coalesce(array_agg(row_to_json(t)), '{}')) from (#{subquery_sql}) t"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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