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
@@ -0,0 +1,17 @@
|
|
1
|
+
module Surus
|
2
|
+
module JSON
|
3
|
+
module Model
|
4
|
+
def find_json(id, options={})
|
5
|
+
sql = RowQuery.new(where(id: id), options).to_sql
|
6
|
+
connection.select_value sql
|
7
|
+
end
|
8
|
+
|
9
|
+
def all_json(options={})
|
10
|
+
sql = ArrayAggQuery.new(scoped, options).to_sql
|
11
|
+
connection.select_value sql
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveRecord::Base.extend Surus::JSON::Model
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Surus
|
2
|
+
module JSON
|
3
|
+
class Query
|
4
|
+
attr_reader :original_scope
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
def initialize(original_scope, options={})
|
8
|
+
@original_scope = original_scope
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def klass
|
14
|
+
original_scope.klass
|
15
|
+
end
|
16
|
+
|
17
|
+
def subquery_sql
|
18
|
+
select(columns.map(&:to_s).join(', ')).to_sql
|
19
|
+
end
|
20
|
+
|
21
|
+
def columns
|
22
|
+
selected_columns + association_columns
|
23
|
+
end
|
24
|
+
|
25
|
+
def table_columns
|
26
|
+
klass.columns
|
27
|
+
end
|
28
|
+
|
29
|
+
def selected_columns
|
30
|
+
if options.key? :columns
|
31
|
+
options[:columns]
|
32
|
+
else
|
33
|
+
table_columns.map(&:name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def association_columns
|
38
|
+
included_associations_name_and_options.map do |association_name, association_options|
|
39
|
+
association = klass.reflect_on_association association_name
|
40
|
+
subquery = case association.source_macro
|
41
|
+
when :belongs_to
|
42
|
+
association_scope = BelongsToScopeBuilder.new(original_scope, association).scope
|
43
|
+
RowQuery.new(association_scope, association_options).to_sql
|
44
|
+
when :has_many
|
45
|
+
association_scope = HasManyScopeBuilder.new(original_scope, association).scope
|
46
|
+
ArrayAggQuery.new(association_scope, association_options).to_sql
|
47
|
+
when :has_and_belongs_to_many
|
48
|
+
association_scope = HasAndBelongsToManyScopeBuilder.new(original_scope, association).scope
|
49
|
+
ArrayAggQuery.new(association_scope, association_options).to_sql
|
50
|
+
end
|
51
|
+
"(#{subquery}) #{association_name}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def included_associations_name_and_options
|
56
|
+
_include = options[:include]
|
57
|
+
if _include.nil?
|
58
|
+
{}
|
59
|
+
elsif _include.kind_of?(::Hash)
|
60
|
+
_include
|
61
|
+
elsif _include.kind_of?(::Array)
|
62
|
+
_include.each_with_object({}) do |e, hash|
|
63
|
+
if e.kind_of?(Hash)
|
64
|
+
hash.merge!(e)
|
65
|
+
else
|
66
|
+
hash[e] = {}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
{_include => {}}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
delegate :connection, :quoted_table_name, to: :klass
|
75
|
+
delegate :select, to: :original_scope
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/surus/version.rb
CHANGED
@@ -7,17 +7,17 @@ describe Surus::Array::DecimalSerializer do
|
|
7
7
|
[[BigDecimal("0.0")], "single element"],
|
8
8
|
[[BigDecimal("1.0"), BigDecimal("2.0")], "multiple elements"],
|
9
9
|
[[BigDecimal("-1.0"), BigDecimal("-2.0")], "negative element"],
|
10
|
-
[[BigDecimal("1232493289348929843.323422349274382923")], "high magnitude element"],
|
10
|
+
[[BigDecimal("1232493289348929843.323422349274382923")], "high magnitude element"],
|
11
11
|
[[BigDecimal("1.0"), BigDecimal("2.0"), BigDecimal("2.0")], "duplicated elements"],
|
12
12
|
[[BigDecimal("1.0"), nil], "an element is nil"],
|
13
13
|
[(10_000.times.map { |n| n * BigDecimal("1.13312") }).to_a, "huge array"]
|
14
14
|
]
|
15
|
-
|
15
|
+
|
16
16
|
round_trip_examples.each do |value, description|
|
17
17
|
it "round trips when #{description}" do
|
18
18
|
r = DecimalArrayRecord.create! :decimals => value
|
19
19
|
r.reload
|
20
|
-
r.decimals.
|
21
|
-
end
|
20
|
+
expect(r.decimals).to eq(value)
|
21
|
+
end
|
22
22
|
end
|
23
23
|
end
|
@@ -7,17 +7,17 @@ describe Surus::Array::FloatSerializer do
|
|
7
7
|
[[0.0], "single element"],
|
8
8
|
[[1.0, 2.0], "multiple elements"],
|
9
9
|
[[-1.0, -2.0], "negative element"],
|
10
|
-
[[4.4325349e+45, 1.2324323e+77, 1.1242342e-57, 3e99], "high magnitude elements"],
|
10
|
+
[[4.4325349e+45, 1.2324323e+77, 1.1242342e-57, 3e99], "high magnitude elements"],
|
11
11
|
[[1.0, 2.0, 2.0, 2.0], "duplicated elements"],
|
12
12
|
[[1.0, nil], "an element is nil"],
|
13
13
|
[(10_000.times.map { |n| n * 1.0 }).to_a, "huge array"]
|
14
14
|
]
|
15
|
-
|
15
|
+
|
16
16
|
round_trip_examples.each do |value, description|
|
17
17
|
it "round trips when #{description}" do
|
18
18
|
r = FloatArrayRecord.create! :floats => value
|
19
19
|
r.reload
|
20
|
-
r.floats.
|
21
|
-
end
|
20
|
+
expect(r.floats).to eq(value)
|
21
|
+
end
|
22
22
|
end
|
23
23
|
end
|
@@ -11,12 +11,12 @@ describe Surus::Array::IntegerSerializer do
|
|
11
11
|
[[1, nil], "an element is nil"],
|
12
12
|
[(1..10_000).to_a, "huge array"]
|
13
13
|
]
|
14
|
-
|
14
|
+
|
15
15
|
round_trip_examples.each do |value, description|
|
16
16
|
it "round trips when #{description}" do
|
17
17
|
r = IntegerArrayRecord.create! :integers => value
|
18
18
|
r.reload
|
19
|
-
r.integers.
|
20
|
-
end
|
19
|
+
expect(r.integers).to eq(value)
|
20
|
+
end
|
21
21
|
end
|
22
22
|
end
|
@@ -18,12 +18,12 @@ describe Surus::Array::TextSerializer do
|
|
18
18
|
[[%q~foo \\ / " ; ' ( ) {}bar \\'~, "bar"], "an element many special characters"],
|
19
19
|
[("aaa".."zzz").to_a, "huge array"]
|
20
20
|
]
|
21
|
-
|
21
|
+
|
22
22
|
round_trip_examples.each do |value, description|
|
23
23
|
it "round trips when #{description}" do
|
24
24
|
r = TextArrayRecord.create! :texts => value
|
25
25
|
r.reload
|
26
|
-
r.texts.
|
27
|
-
end
|
26
|
+
expect(r.texts).to eq(value)
|
27
|
+
end
|
28
28
|
end
|
29
29
|
end
|
data/spec/database_structure.sql
CHANGED
@@ -36,9 +36,59 @@ CREATE TABLE float_array_records(
|
|
36
36
|
|
37
37
|
|
38
38
|
|
39
|
-
DROP TABLE IF EXISTS
|
39
|
+
DROP TABLE IF EXISTS decimal_array_records;
|
40
40
|
|
41
41
|
CREATE TABLE decimal_array_records(
|
42
42
|
id serial PRIMARY KEY,
|
43
43
|
decimals decimal[]
|
44
|
+
);
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
DROP TABLE IF EXISTS users CASCADE;
|
49
|
+
|
50
|
+
CREATE TABLE users(
|
51
|
+
id serial PRIMARY KEY,
|
52
|
+
name varchar NOT NULL,
|
53
|
+
email varchar NOT NULL
|
54
|
+
);
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
DROP TABLE IF EXISTS forums CASCADE;
|
59
|
+
|
60
|
+
CREATE TABLE forums(
|
61
|
+
id serial PRIMARY KEY,
|
62
|
+
name varchar NOT NULL
|
63
|
+
);
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
DROP TABLE IF EXISTS posts CASCADE;
|
68
|
+
|
69
|
+
CREATE TABLE posts(
|
70
|
+
id serial PRIMARY KEY,
|
71
|
+
forum_id integer NOT NULL REFERENCES forums,
|
72
|
+
author_id integer NOT NULL REFERENCES users,
|
73
|
+
subject varchar NOT NULL,
|
74
|
+
body varchar NOT NULL
|
75
|
+
);
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
DROP TABLE IF EXISTS tags CASCADE;
|
80
|
+
|
81
|
+
CREATE TABLE tags(
|
82
|
+
id serial PRIMARY KEY,
|
83
|
+
name varchar NOT NULL UNIQUE
|
84
|
+
);
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
DROP TABLE IF EXISTS posts_tags CASCADE;
|
89
|
+
|
90
|
+
CREATE TABLE posts_tags(
|
91
|
+
post_id integer NOT NULL REFERENCES posts,
|
92
|
+
tag_id integer NOT NULL REFERENCES tags,
|
93
|
+
PRIMARY KEY (post_id, tag_id)
|
44
94
|
);
|
data/spec/factories.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :forum do
|
3
|
+
name { Faker::Lorem.sentence }
|
4
|
+
end
|
5
|
+
|
6
|
+
factory :post do
|
7
|
+
forum
|
8
|
+
author
|
9
|
+
subject { Faker::Lorem.sentence }
|
10
|
+
body { Faker::Lorem.paragraph }
|
11
|
+
end
|
12
|
+
|
13
|
+
factory :tag do
|
14
|
+
sequence(:name) { |n| "#{Faker::Lorem.word} - #{n}" }
|
15
|
+
end
|
16
|
+
|
17
|
+
factory :user, aliases: [:author] do
|
18
|
+
name { Faker::Internet.user_name }
|
19
|
+
email { Faker::Internet.email }
|
20
|
+
end
|
21
|
+
end
|
@@ -8,7 +8,7 @@ describe Surus::Hstore::Serializer do
|
|
8
8
|
[{"foo" => "bar", "baz" => "quz"}, "multiple key/value pairs"],
|
9
9
|
[{"foo" => nil}, "value is nil"],
|
10
10
|
]
|
11
|
-
|
11
|
+
|
12
12
|
[
|
13
13
|
['"', 'double quote (")'],
|
14
14
|
["'", "single quote (')"],
|
@@ -22,13 +22,13 @@ describe Surus::Hstore::Serializer do
|
|
22
22
|
round_trip_examples << [{"foo#{value}foo" => "bar"}, "key with #{description} in middle"]
|
23
23
|
round_trip_examples << [{"foo#{value}" => "bar"}, "key with #{description} at end"]
|
24
24
|
round_trip_examples << [{value => "bar"}, "key is #{description}"]
|
25
|
-
|
25
|
+
|
26
26
|
round_trip_examples << [{"foo" => "#{value}bar"}, "value with #{description} at beginning"]
|
27
27
|
round_trip_examples << [{"foo" => "bar#{value}bar"}, "value with #{description} in middle"]
|
28
28
|
round_trip_examples << [{"foo" => "bar#{value}"}, "value with #{description} at end"]
|
29
29
|
round_trip_examples << [{"foo" => value}, "value is #{description}"]
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
[
|
33
33
|
[:foo, "symbol"],
|
34
34
|
[0, "integer 0"],
|
@@ -55,12 +55,12 @@ describe Surus::Hstore::Serializer do
|
|
55
55
|
round_trip_examples << [{value => "bar"}, "key is #{description}"]
|
56
56
|
round_trip_examples << [{value => value}, "key and value are each #{description}"]
|
57
57
|
end
|
58
|
-
|
58
|
+
|
59
59
|
round_trip_examples.each do |value, description|
|
60
60
|
it "round trips when #{description}" do
|
61
61
|
r = HstoreRecord.create! :properties => value
|
62
62
|
r.reload
|
63
|
-
r.properties.
|
64
|
-
end
|
63
|
+
expect(r.properties).to eq(value)
|
64
|
+
end
|
65
65
|
end
|
66
66
|
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'oj'
|
3
|
+
ActiveRecord::Base.include_root_in_json = false
|
4
|
+
|
5
|
+
describe 'json' do
|
6
|
+
describe 'find_json' do
|
7
|
+
context 'with only id parameter' do
|
8
|
+
it 'is entire row as json' do
|
9
|
+
user = FactoryGirl.create :user
|
10
|
+
to_json = Oj.load user.to_json
|
11
|
+
find_json = Oj.load User.find_json(user.id)
|
12
|
+
expect(find_json).to eq(to_json)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'with columns parameter' do
|
17
|
+
it 'is selected row columns as json' do
|
18
|
+
user = FactoryGirl.create :user
|
19
|
+
to_json = Oj.load user.to_json only: [:id, :name]
|
20
|
+
find_json = Oj.load User.find_json(user.id, columns: [:id, :name])
|
21
|
+
expect(find_json).to eq(to_json)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with includes option' do
|
26
|
+
it 'includes entire belongs_to object' do
|
27
|
+
post = FactoryGirl.create :post
|
28
|
+
to_json = Oj.load post.to_json(include: :author)
|
29
|
+
find_json = Oj.load Post.find_json(post.id, include: :author)
|
30
|
+
expect(find_json).to eq(to_json)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'filters by belongs_to conditions' do
|
34
|
+
post = FactoryGirl.create :post
|
35
|
+
find_json = Oj.load Post.find_json(post.id, include: :forum_with_impossible_conditions)
|
36
|
+
expect(find_json.fetch('forum_with_impossible_conditions')).to be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'includes multiple entire belongs_to objects' do
|
40
|
+
post = FactoryGirl.create :post
|
41
|
+
to_json = Oj.load post.to_json(include: [:author, :forum])
|
42
|
+
find_json = Oj.load Post.find_json(post.id, include: [:author, :forum])
|
43
|
+
expect(find_json).to eq(to_json)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'includes only selected columns of belongs_to object' do
|
47
|
+
post = FactoryGirl.create :post
|
48
|
+
to_json = Oj.load post.to_json(include: {author: {only: [:id, :name]}})
|
49
|
+
find_json = Oj.load Post.find_json(post.id, include: {author: {columns: [:id, :name]}})
|
50
|
+
expect(find_json).to eq(to_json)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'includes entire has_many association' do
|
54
|
+
user = FactoryGirl.create :user
|
55
|
+
posts = FactoryGirl.create_list :post, 2, author: user
|
56
|
+
user.reload
|
57
|
+
to_json = Oj.load user.to_json(include: :posts)
|
58
|
+
find_json = Oj.load User.find_json(user.id, include: :posts)
|
59
|
+
expect(find_json).to eq(to_json)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'preserves has_many order' do
|
63
|
+
user = FactoryGirl.create :user
|
64
|
+
posts = FactoryGirl.create_list :post, 2, author: user
|
65
|
+
user.reload
|
66
|
+
to_json = Oj.load user.to_json(include: :posts_with_order)
|
67
|
+
find_json = Oj.load User.find_json(user.id, include: :posts_with_order)
|
68
|
+
expect(find_json).to eq(to_json)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'filters by has_many conditions' do
|
72
|
+
user = FactoryGirl.create :user
|
73
|
+
FactoryGirl.create :post, author: user, subject: 'foo'
|
74
|
+
FactoryGirl.create :post, author: user
|
75
|
+
user.reload
|
76
|
+
to_json = Oj.load user.to_json(include: :posts_with_conditions)
|
77
|
+
find_json = Oj.load User.find_json(user.id, include: :posts_with_conditions)
|
78
|
+
expect(find_json).to eq(to_json)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'includes only select columns of has_many association' do
|
82
|
+
user = FactoryGirl.create :user
|
83
|
+
posts = FactoryGirl.create_list :post, 2, author: user
|
84
|
+
user.reload
|
85
|
+
to_json = Oj.load user.to_json(include: {posts: {only: [:id, :subject]}})
|
86
|
+
find_json = Oj.load User.find_json(user.id, include: {posts: {columns: [:id, :subject]}})
|
87
|
+
expect(find_json).to eq(to_json)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'includes empty array for empty has_many association' do
|
91
|
+
user = FactoryGirl.create :user
|
92
|
+
to_json = Oj.load user.to_json(include: :posts)
|
93
|
+
find_json = Oj.load User.find_json(user.id, include: :posts)
|
94
|
+
expect(find_json).to eq(to_json)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'includes entire has_and_belongs_to_many association' do
|
98
|
+
post = FactoryGirl.create :post
|
99
|
+
tag = FactoryGirl.create :tag
|
100
|
+
post.tags << tag
|
101
|
+
to_json = Oj.load post.to_json(include: :tags)
|
102
|
+
find_json = Oj.load Post.find_json(post.id, include: :tags)
|
103
|
+
expect(find_json).to eq(to_json)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'excludes other has_and_belongs_to_many association records' do
|
107
|
+
post = FactoryGirl.create :post
|
108
|
+
tag = FactoryGirl.create :tag
|
109
|
+
post.tags << tag
|
110
|
+
other_post = FactoryGirl.create :post
|
111
|
+
other_tag = FactoryGirl.create :tag
|
112
|
+
other_post.tags << other_tag
|
113
|
+
to_json = Oj.load post.to_json(include: :tags)
|
114
|
+
find_json = Oj.load Post.find_json(post.id, include: :tags)
|
115
|
+
expect(find_json).to eq(to_json)
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'includes empty array for empty has_and_belongs_to_many' do
|
119
|
+
post = FactoryGirl.create :post
|
120
|
+
to_json = Oj.load post.to_json(include: :tags)
|
121
|
+
find_json = Oj.load Post.find_json(post.id, include: :tags)
|
122
|
+
expect(find_json).to eq(to_json)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'includes nested associations' do
|
126
|
+
user = FactoryGirl.create :user
|
127
|
+
post = FactoryGirl.create :post, author: user
|
128
|
+
user.reload
|
129
|
+
to_json = Oj.load user.to_json(include: {posts: {include: :forum}})
|
130
|
+
find_json = Oj.load User.find_json(user.id, include: {posts: {include: :forum}})
|
131
|
+
expect(find_json).to eq(to_json)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'includes nested associations with columns' do
|
135
|
+
user = FactoryGirl.create :user
|
136
|
+
post = FactoryGirl.create :post, author: user
|
137
|
+
user.reload
|
138
|
+
to_json = Oj.load user.to_json(include: {posts: {only: [:id], include: :forum}})
|
139
|
+
find_json = Oj.load User.find_json(user.id, include: {posts: {columns: [:id], include: :forum}})
|
140
|
+
expect(find_json).to eq(to_json)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe 'all_json' do
|
146
|
+
it 'is all rows as array' do
|
147
|
+
users = FactoryGirl.create_list :user, 3
|
148
|
+
to_json = Oj.load users.to_json
|
149
|
+
all_json = Oj.load User.all_json
|
150
|
+
expect(all_json).to eq(to_json)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|