perpetuity-postgres 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +34 -0
- data/Rakefile +1 -0
- data/lib/perpetuity/postgres/boolean_value.rb +17 -0
- data/lib/perpetuity/postgres/connection.rb +78 -0
- data/lib/perpetuity/postgres/connection_pool.rb +39 -0
- data/lib/perpetuity/postgres/expression.rb +21 -0
- data/lib/perpetuity/postgres/json_array.rb +31 -0
- data/lib/perpetuity/postgres/json_hash.rb +55 -0
- data/lib/perpetuity/postgres/json_string_value.rb +17 -0
- data/lib/perpetuity/postgres/negated_query.rb +21 -0
- data/lib/perpetuity/postgres/nil_query.rb +9 -0
- data/lib/perpetuity/postgres/null_value.rb +9 -0
- data/lib/perpetuity/postgres/numeric_value.rb +17 -0
- data/lib/perpetuity/postgres/query.rb +34 -0
- data/lib/perpetuity/postgres/query_attribute.rb +33 -0
- data/lib/perpetuity/postgres/query_expression.rb +90 -0
- data/lib/perpetuity/postgres/query_intersection.rb +26 -0
- data/lib/perpetuity/postgres/query_union.rb +26 -0
- data/lib/perpetuity/postgres/serialized_data.rb +63 -0
- data/lib/perpetuity/postgres/serializer.rb +179 -0
- data/lib/perpetuity/postgres/sql_select.rb +71 -0
- data/lib/perpetuity/postgres/sql_update.rb +28 -0
- data/lib/perpetuity/postgres/sql_value.rb +40 -0
- data/lib/perpetuity/postgres/table/attribute.rb +60 -0
- data/lib/perpetuity/postgres/table.rb +36 -0
- data/lib/perpetuity/postgres/table_name.rb +21 -0
- data/lib/perpetuity/postgres/text_value.rb +14 -0
- data/lib/perpetuity/postgres/timestamp_value.rb +68 -0
- data/lib/perpetuity/postgres/value_with_attribute.rb +19 -0
- data/lib/perpetuity/postgres/version.rb +5 -0
- data/lib/perpetuity/postgres.rb +170 -0
- data/perpetuity-postgres.gemspec +26 -0
- data/spec/perpetuity/postgres/boolean_value_spec.rb +15 -0
- data/spec/perpetuity/postgres/connection_pool_spec.rb +55 -0
- data/spec/perpetuity/postgres/connection_spec.rb +30 -0
- data/spec/perpetuity/postgres/expression_spec.rb +13 -0
- data/spec/perpetuity/postgres/json_array_spec.rb +31 -0
- data/spec/perpetuity/postgres/json_hash_spec.rb +39 -0
- data/spec/perpetuity/postgres/json_string_value_spec.rb +11 -0
- data/spec/perpetuity/postgres/negated_query_spec.rb +39 -0
- data/spec/perpetuity/postgres/null_value_spec.rb +11 -0
- data/spec/perpetuity/postgres/numeric_value_spec.rb +12 -0
- data/spec/perpetuity/postgres/query_attribute_spec.rb +48 -0
- data/spec/perpetuity/postgres/query_expression_spec.rb +110 -0
- data/spec/perpetuity/postgres/query_intersection_spec.rb +23 -0
- data/spec/perpetuity/postgres/query_spec.rb +23 -0
- data/spec/perpetuity/postgres/query_union_spec.rb +23 -0
- data/spec/perpetuity/postgres/serialized_data_spec.rb +69 -0
- data/spec/perpetuity/postgres/serializer_spec.rb +216 -0
- data/spec/perpetuity/postgres/sql_select_spec.rb +51 -0
- data/spec/perpetuity/postgres/sql_update_spec.rb +22 -0
- data/spec/perpetuity/postgres/sql_value_spec.rb +38 -0
- data/spec/perpetuity/postgres/table/attribute_spec.rb +82 -0
- data/spec/perpetuity/postgres/table_name_spec.rb +15 -0
- data/spec/perpetuity/postgres/table_spec.rb +43 -0
- data/spec/perpetuity/postgres/text_value_spec.rb +15 -0
- data/spec/perpetuity/postgres/timestamp_value_spec.rb +28 -0
- data/spec/perpetuity/postgres/value_with_attribute_spec.rb +34 -0
- data/spec/perpetuity/postgres_spec.rb +163 -0
- data/spec/support/test_classes/book.rb +16 -0
- data/spec/support/test_classes/person.rb +11 -0
- metadata +207 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'perpetuity/postgres/query_union'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe QueryUnion do
|
6
|
+
let(:lhs) { double('LHS', to_db: 'left = 1') }
|
7
|
+
let(:rhs) { double('RHS', to_db: 'right = 2') }
|
8
|
+
let(:union) { QueryUnion.new(lhs, rhs) }
|
9
|
+
|
10
|
+
it 'converts to a SQL "OR" expression' do
|
11
|
+
union.to_db.should == '(left = 1 OR right = 2)'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'allows unions to have other unions' do
|
15
|
+
(union|union).to_db.should == '((left = 1 OR right = 2) OR (left = 1 OR right = 2))'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'allows unions to have intersections' do
|
19
|
+
(union&union).to_db.should == '((left = 1 OR right = 2) AND (left = 1 OR right = 2))'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'perpetuity/postgres/serialized_data'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe SerializedData do
|
6
|
+
let(:columns) { [:name, :age] }
|
7
|
+
let(:data) { ["'Jamie'", 31] }
|
8
|
+
let(:serialized) { SerializedData.new(columns, data) }
|
9
|
+
|
10
|
+
it 'matches a SQL string' do
|
11
|
+
serialized.to_s.should == "(name,age) VALUES ('Jamie',31)"
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'adding values' do
|
15
|
+
it 'adds a value' do
|
16
|
+
serialized['id'] = 'abc'
|
17
|
+
serialized.to_s.should == "(name,age,id) VALUES ('Jamie',31,'abc')"
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'replaces an existing value' do
|
21
|
+
serialized['id'] = 'abc'
|
22
|
+
serialized['id'] = 'xyz'
|
23
|
+
serialized.to_s.should == "(name,age,id) VALUES ('Jamie',31,'xyz')"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with multiple serialized objects' do
|
28
|
+
let(:serialized_multiple) do
|
29
|
+
[ SerializedData.new(columns, ["'Jamie'", 31]),
|
30
|
+
SerializedData.new(columns, ["'Jessica'", 23]),
|
31
|
+
SerializedData.new(columns, ["'Kevin'", 22]),
|
32
|
+
].reduce(:+)
|
33
|
+
end
|
34
|
+
let(:serialized_multiple) { serialized + SerializedData.new(columns, ["'Jessica'", 23]) +
|
35
|
+
SerializedData.new(columns, ["'Kevin'",22])}
|
36
|
+
|
37
|
+
it 'matches a SQL string' do
|
38
|
+
serialized_multiple.to_s.should == "(name,age) VALUES ('Jamie',31),('Jessica',23),('Kevin',22)"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'checks whether there are any objects' do
|
43
|
+
serialized.any?.should be_true
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'iterates like a hash' do
|
47
|
+
serialized.map { |attr, value| [attr, value] }.should ==
|
48
|
+
[['name', "'Jamie'"], ['age', 31]]
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'equals another with the same data' do
|
52
|
+
original = SerializedData.new([:a, :b], [1, 2])
|
53
|
+
duplicate = SerializedData.new([:a, :b], [1, 2])
|
54
|
+
modified = SerializedData.new([:a, :b], [0, 2])
|
55
|
+
original.should == duplicate
|
56
|
+
original.should_not == modified
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'returns a new SerializedData with the complement of values' do
|
60
|
+
columns = [:name, :age, :foo, :bar]
|
61
|
+
original = SerializedData.new(columns, ["'Jamie'", 31, nil, nil])
|
62
|
+
new_name = SerializedData.new(columns, ["'Foo'", 31, nil, nil])
|
63
|
+
new_age = SerializedData.new(columns, ["'Jamie'", 32, nil, nil])
|
64
|
+
(new_name - original).should == SerializedData.new([:name], ["'Foo'"])
|
65
|
+
(new_age - original).should == SerializedData.new([:age], [32])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'perpetuity/postgres/serializer'
|
2
|
+
require 'perpetuity/mapper'
|
3
|
+
require 'perpetuity/mapper_registry'
|
4
|
+
require 'support/test_classes/book'
|
5
|
+
require 'support/test_classes/person'
|
6
|
+
|
7
|
+
module Perpetuity
|
8
|
+
class Postgres
|
9
|
+
describe Serializer do
|
10
|
+
let(:registry) { MapperRegistry.new }
|
11
|
+
let!(:book_mapper) do
|
12
|
+
registry = self.registry
|
13
|
+
Class.new(Mapper) do
|
14
|
+
map Book, registry
|
15
|
+
attribute :title, type: String
|
16
|
+
attribute :authors, type: Array[Person], embedded: true
|
17
|
+
attribute :main_character, type: Person
|
18
|
+
end.new(registry)
|
19
|
+
end
|
20
|
+
let!(:person_mapper) do
|
21
|
+
registry = self.registry
|
22
|
+
Class.new(Mapper) do
|
23
|
+
map Person, registry
|
24
|
+
attribute :name, type: String
|
25
|
+
end.new(registry)
|
26
|
+
end
|
27
|
+
let(:serializer) { Serializer.new(book_mapper) }
|
28
|
+
|
29
|
+
it 'serializes simple objects' do
|
30
|
+
serializer.serialize(Book.new('Foo')).to_s.should ==
|
31
|
+
%q{(title,authors,main_character) VALUES ('Foo','[]',NULL)}
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'serializing complex objects' do
|
35
|
+
let(:jamie) { Person.new('Jamie') }
|
36
|
+
let(:jamie_json) { '{"name":"Jamie","__metadata__":{"class":"Person"}}' }
|
37
|
+
let(:character) { Person.new('Character') }
|
38
|
+
let(:character_json) { '{"__metadata__":{"class":"Person","id":1}}' }
|
39
|
+
|
40
|
+
before { character.instance_variable_set :@id, 1 }
|
41
|
+
|
42
|
+
context 'with nested objects' do
|
43
|
+
let(:book) { Book.new('Foo', jamie, character) }
|
44
|
+
it 'converts objects into JSON' do
|
45
|
+
serializer.serialize(book).to_s.should ==
|
46
|
+
%Q{(title,authors,main_character) VALUES ('Foo','#{jamie_json}','#{character_json}')}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'with arrays of nested objects' do
|
51
|
+
let(:book) { Book.new('Foo', [jamie], [character]) }
|
52
|
+
|
53
|
+
it 'adds the JSON array' do
|
54
|
+
serializer.serialize(book).to_s.should ==
|
55
|
+
%Q{(title,authors,main_character) VALUES ('Foo','[#{jamie_json}]','[#{character_json}]')}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with natively serializable values' do
|
61
|
+
it 'serializes strings' do
|
62
|
+
serializer.serialize_attribute('string').should == "'string'"
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'serializes numbers' do
|
66
|
+
serializer.serialize_attribute(1).should == '1'
|
67
|
+
serializer.serialize_attribute(1.5).should == '1.5'
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'serializes nil' do
|
71
|
+
serializer.serialize_attribute(nil).should == 'NULL'
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'serializes booleans' do
|
75
|
+
serializer.serialize_attribute(true).should == 'TRUE'
|
76
|
+
serializer.serialize_attribute(false).should == 'FALSE'
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'serializes Time objects' do
|
80
|
+
time = Time.new(2000, 1, 2, 3, 4, 5.123456, '-04:00')
|
81
|
+
serializer.serialize_attribute(time).should == "'2000-01-02 03:04:05.123456-0400'::timestamptz"
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'serializes an array as JSON' do
|
85
|
+
serializer.serialize_attribute([1, 'foo']).should == %q{'[1,"foo"]'}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe 'unserialization AKA deserialization' do
|
90
|
+
let(:author) { Person.new('Me') }
|
91
|
+
let(:serialized_book) do
|
92
|
+
{
|
93
|
+
'id' => 'id-id-id',
|
94
|
+
'title' => 'My Book',
|
95
|
+
'authors' => [
|
96
|
+
].to_json,
|
97
|
+
'main_character' => nil
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'deserializes an object that embeds another object' do
|
102
|
+
serialized_authors = [{
|
103
|
+
'__metadata__' => { 'class' => 'Person' },
|
104
|
+
'name' => 'Me'
|
105
|
+
}].to_json
|
106
|
+
serialized_book['authors'] = serialized_authors
|
107
|
+
book = Book.new('My Book', [author])
|
108
|
+
serializer.unserialize(serialized_book).should == book
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'deserializes an object which references another object' do
|
112
|
+
serialized_book['main_character'] = {
|
113
|
+
'__metadata__' => {
|
114
|
+
'class' => 'Person',
|
115
|
+
'id' => 'id-id-id'
|
116
|
+
}
|
117
|
+
}.to_json
|
118
|
+
deserialized_book = Book.new('My Book', [], Reference.new(Person, 'id-id-id'))
|
119
|
+
serializer.unserialize(serialized_book).should == deserialized_book
|
120
|
+
end
|
121
|
+
|
122
|
+
let(:article_class) do
|
123
|
+
Class.new do
|
124
|
+
attr_reader :title, :body, :views, :published_at
|
125
|
+
def initialize attributes={}
|
126
|
+
@title = attributes[:title]
|
127
|
+
@body = attributes[:body]
|
128
|
+
@views = attributes.fetch(:views) { 0 }
|
129
|
+
@published_at = attributes.fetch(:published_at) { Time.now }
|
130
|
+
end
|
131
|
+
|
132
|
+
def == other
|
133
|
+
other.is_a?(self.class) &&
|
134
|
+
other.title == title &&
|
135
|
+
other.body == body &&
|
136
|
+
other.views == views &&
|
137
|
+
other.published_at == published_at
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
let(:article_mapper) do
|
142
|
+
article_class = self.article_class
|
143
|
+
registry = self.registry
|
144
|
+
Class.new(Mapper) do
|
145
|
+
map article_class, registry
|
146
|
+
attribute :title, type: String
|
147
|
+
attribute :body, type: String
|
148
|
+
attribute :views, type: Integer
|
149
|
+
attribute :published_at, type: Time
|
150
|
+
end.new(registry)
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'deserializes non-string attributes to their proper types' do
|
154
|
+
serializer = Serializer.new(article_mapper)
|
155
|
+
serialized_article = {
|
156
|
+
'id' => 'id-id-id',
|
157
|
+
'title' => 'Title',
|
158
|
+
'body' => 'Body',
|
159
|
+
'views' => '0',
|
160
|
+
'published_at' => '2013-01-02 03:04:05.123456-05'
|
161
|
+
}
|
162
|
+
|
163
|
+
article = article_class.new(
|
164
|
+
title: 'Title',
|
165
|
+
body: 'Body',
|
166
|
+
views: 0,
|
167
|
+
published_at: Time.new(2013, 1, 2, 3, 4, 5.123456, '-05:00')
|
168
|
+
)
|
169
|
+
serializer.unserialize(serialized_article).should == article
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe 'identifying embedded/referenced objects as foreign' do
|
174
|
+
it 'sees hashes with metadata keys as foreign objects' do
|
175
|
+
serializer.foreign_object?({'__metadata__' => 'lol'}).should be_true
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'sees hashes without metadata keys as simple hashes' do
|
179
|
+
serializer.foreign_object?({ 'name' => 'foo' }).should be_false
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe 'identifying possible JSON strings' do
|
184
|
+
it 'identifies JSON objects' do
|
185
|
+
serializer.possible_json_value?('{"name":"foo"}').should be_true
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'identifies JSON arrays' do
|
189
|
+
serializer.possible_json_value?('[{"name":"foo"}]').should be_true
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'rejects things it does not detect as either of the above' do
|
193
|
+
serializer.possible_json_value?('foo is my name').should be_false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'serializes changes between two objects' do
|
198
|
+
original = Book.new('Old title')
|
199
|
+
modified = original.dup
|
200
|
+
modified.title = 'New title'
|
201
|
+
serializer.serialize_changes(modified, original).should ==
|
202
|
+
SerializedData.new([:title], ["'New title'"])
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'serializes a reference as its referenced class' do
|
206
|
+
reference = Reference.new(Object, 123)
|
207
|
+
serializer.serialize_reference(reference).should == JSONHash.new(
|
208
|
+
__metadata__: {
|
209
|
+
class: Object,
|
210
|
+
id: 123
|
211
|
+
}
|
212
|
+
)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'perpetuity/postgres/sql_select'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe SQLSelect do
|
6
|
+
let(:query) { SQLSelect.new(from: 'foo',
|
7
|
+
where: "name = 'foo'",
|
8
|
+
limit: 4) }
|
9
|
+
subject { query }
|
10
|
+
|
11
|
+
its(:table) { should == 'foo' }
|
12
|
+
its(:where) { should == "name = 'foo'" }
|
13
|
+
its(:limit) { should == 4 }
|
14
|
+
|
15
|
+
it 'generates a SQL query' do
|
16
|
+
query.to_s.should == %Q{SELECT * FROM "foo" WHERE name = 'foo' LIMIT 4}
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'generates a query with no clauses' do
|
20
|
+
sql = SQLSelect.new(from: 'foo').to_s
|
21
|
+
sql.should == %Q{SELECT * FROM "foo"}
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'generates a count query' do
|
25
|
+
sql = SQLSelect.new('COUNT(*)', from: 'foo').to_s
|
26
|
+
sql.should == %Q{SELECT COUNT(*) FROM "foo"}
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'generates a query with an ORDER BY clause' do
|
30
|
+
sql = SQLSelect.new(from: 'foo', order: 'name').to_s
|
31
|
+
sql.should == %Q{SELECT * FROM "foo" ORDER BY name}
|
32
|
+
|
33
|
+
sql = SQLSelect.new(from: 'foo', order: { name: :asc }).to_s
|
34
|
+
sql.should == %Q{SELECT * FROM "foo" ORDER BY name ASC}
|
35
|
+
|
36
|
+
sql = SQLSelect.new(from: 'foo', order: { name: :asc, age: :desc }).to_s
|
37
|
+
sql.should == %Q{SELECT * FROM "foo" ORDER BY name ASC,age DESC}
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'generates a query with an OFFSET clause' do
|
41
|
+
sql = SQLSelect.new(from: 'foo', offset: 12).to_s
|
42
|
+
sql.should == %Q{SELECT * FROM "foo" OFFSET 12}
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'generates a query with a GROUP BY clause' do
|
46
|
+
sql = SQLSelect.new(from: 'foo', group: :id).to_s
|
47
|
+
sql.should == %Q{SELECT * FROM "foo" GROUP BY id}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'perpetuity/postgres/sql_update'
|
2
|
+
require 'perpetuity/postgres/serialized_data'
|
3
|
+
|
4
|
+
module Perpetuity
|
5
|
+
class Postgres
|
6
|
+
describe SQLUpdate do
|
7
|
+
context 'when given a SerializedData' do
|
8
|
+
it 'generates the SQL to update an object' do
|
9
|
+
update = SQLUpdate.new('User', 'abc123', SerializedData.new([:foo, :baz], ["'bar'", "'quux'"]))
|
10
|
+
update.to_s.should == %Q{UPDATE "User" SET foo = 'bar',baz = 'quux' WHERE id = 'abc123'}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'when given a hash' do
|
15
|
+
it 'sanitizes the data into SQLValues' do
|
16
|
+
update = SQLUpdate.new('User', 'abc123', foo: 'bar', baz: 'quux')
|
17
|
+
update.to_s.should == %Q{UPDATE "User" SET foo = 'bar',baz = 'quux' WHERE id = 'abc123'}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'perpetuity/postgres/sql_value'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe SQLValue do
|
6
|
+
it 'converts strings' do
|
7
|
+
SQLValue.new('Foo').should == "'Foo'"
|
8
|
+
SQLValue.new("Jamie's House").should == "'Jamie''s House'"
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'converts symbols' do
|
12
|
+
SQLValue.new(:foo).should == "'foo'"
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'converts integers' do
|
16
|
+
SQLValue.new(1).should == "1"
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'converts floats' do
|
20
|
+
SQLValue.new(1.5).should == "1.5"
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'converts nil' do
|
24
|
+
SQLValue.new(nil).should == "NULL"
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'converts booleans' do
|
28
|
+
SQLValue.new(true).should == "TRUE"
|
29
|
+
SQLValue.new(false).should == "FALSE"
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'converts Time objects' do
|
33
|
+
time = Time.new(2013, 1, 2, 3, 4, 5.1234567, '+05:30')
|
34
|
+
SQLValue.new(time).should == "'2013-01-02 03:04:05.123456+0530'::timestamptz"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'perpetuity/postgres/table/attribute'
|
2
|
+
require 'perpetuity/postgres/expression'
|
3
|
+
|
4
|
+
module Perpetuity
|
5
|
+
class Postgres
|
6
|
+
class Table
|
7
|
+
describe Attribute do
|
8
|
+
let(:title) { Attribute.new('title', String, max_length: 40) }
|
9
|
+
|
10
|
+
it 'knows its name' do
|
11
|
+
title.name.should == 'title'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'knows its type' do
|
15
|
+
title.type.should == String
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'id' do
|
19
|
+
let(:id) do
|
20
|
+
Attribute.new('id', Attribute::UUID,
|
21
|
+
primary_key: true,
|
22
|
+
default: Expression.new('uuid_generate_v4()')
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'is a UUID type' do
|
27
|
+
id.sql_type.should == 'UUID'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'is a primary key' do
|
31
|
+
id.should be_primary_key
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'can have a specified default' do
|
35
|
+
id.default.should == Expression.new('uuid_generate_v4()')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'generates the proper SQL' do
|
39
|
+
id.sql_declaration.should == 'id UUID PRIMARY KEY DEFAULT uuid_generate_v4()'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'strings' do
|
44
|
+
let(:body) { Attribute.new('body', String, default: 'foo') }
|
45
|
+
|
46
|
+
it 'converts to the proper SQL type' do
|
47
|
+
title.sql_type.should == 'VARCHAR(40)'
|
48
|
+
body.sql_type.should == 'TEXT'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'generates the proper SQL' do
|
52
|
+
body.sql_declaration.should == "body TEXT DEFAULT 'foo'"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'integers' do
|
57
|
+
let(:page_views) { Attribute.new('page_views', Integer, default: 0) }
|
58
|
+
|
59
|
+
it 'generates the proper SQL' do
|
60
|
+
page_views.sql_declaration.should == 'page_views INTEGER DEFAULT 0'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'times' do
|
65
|
+
let(:timestamp) { Attribute.new('timestamp', Time) }
|
66
|
+
|
67
|
+
it 'converts to the SQL TIMESTAMPTZ type' do
|
68
|
+
timestamp.sql_type.should == 'TIMESTAMPTZ'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'non-serializable types' do
|
73
|
+
let(:author) { Attribute.new('author', Object) }
|
74
|
+
|
75
|
+
it 'has an SQL type of JSON' do
|
76
|
+
author.sql_type.should == 'JSON'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'perpetuity/postgres/table_name'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe TableName do
|
6
|
+
it 'converts to a SQL-string table name' do
|
7
|
+
TableName.new('Person').to_s.should == '"Person"'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'compares equally to its string representation' do
|
11
|
+
TableName.new('Person').should == 'Person'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'perpetuity/postgres/table'
|
2
|
+
require 'perpetuity/postgres/table/attribute'
|
3
|
+
|
4
|
+
module Perpetuity
|
5
|
+
class Postgres
|
6
|
+
describe Table do
|
7
|
+
let(:title) { Table::Attribute.new('title', String, max_length: 40) }
|
8
|
+
let(:body) { Table::Attribute.new('body', String) }
|
9
|
+
let(:author) { Table::Attribute.new('author', Object) }
|
10
|
+
let(:published_at) { Table::Attribute.new('published_at', Time) }
|
11
|
+
let(:views) { Table::Attribute.new('views', Integer) }
|
12
|
+
let(:attributes) { [title, body, author, published_at, views] }
|
13
|
+
let(:table) { Table.new('Article', attributes) }
|
14
|
+
|
15
|
+
it 'knows its name' do
|
16
|
+
table.name.should == 'Article'
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'knows its attributes' do
|
20
|
+
table.attributes.should == attributes
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'converts to a string for SQL' do
|
24
|
+
table.to_s.should == '"Article"'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'generates proper SQL to create itself' do
|
28
|
+
table.create_table_sql.should ==
|
29
|
+
'CREATE TABLE IF NOT EXISTS "Article" (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), title VARCHAR(40), body TEXT, author JSON, published_at TIMESTAMPTZ, views INTEGER)'
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'id column' do
|
33
|
+
context 'when there is an id attribute' do
|
34
|
+
it 'uses the attribute type for the column type' do
|
35
|
+
attributes = [Table::Attribute.new(:id, String, primary_key: true), Table::Attribute.new(:name, String)]
|
36
|
+
table = Table.new('User', attributes)
|
37
|
+
table.create_table_sql.should == 'CREATE TABLE IF NOT EXISTS "User" (id TEXT PRIMARY KEY, name TEXT)'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'perpetuity/postgres/text_value'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe TextValue do
|
6
|
+
it 'serializes into a Postgres-compatible string' do
|
7
|
+
TextValue.new('Jamie').to_s.should == "'Jamie'"
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'escapes single quotes' do
|
11
|
+
TextValue.new("Jamie's house").to_s.should == "'Jamie''s house'"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'perpetuity/postgres/timestamp_value'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe TimestampValue do
|
6
|
+
it 'converts to a SQL string' do
|
7
|
+
time = Time.new(2000, 1, 2, 3, 4, 5.0123456, '-04:00')
|
8
|
+
TimestampValue.new(time).to_s.should == "'2000-01-02 03:04:05.012345-0400'::timestamptz"
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'conversion from a SQL value string' do
|
12
|
+
it 'converts GMT-X times' do
|
13
|
+
timestamp = TimestampValue.from_sql('2013-12-01 15:31:23.838367-05')
|
14
|
+
timestamp.to_time.should == Time.new(2013, 12, 1, 15, 31, 23.838367, '-05:00')
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'converts GMT+X times' do
|
18
|
+
timestamp = TimestampValue.from_sql('1982-08-25 22:19:10.123456+08')
|
19
|
+
timestamp.to_time.should == Time.new(1982, 8, 25, 10, 19, 10.123456, '-04:00')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns its wrapped value' do
|
24
|
+
TimestampValue.new(:foo).value.should == :foo
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'perpetuity/postgres/value_with_attribute'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe ValueWithAttribute do
|
6
|
+
let(:attribute) { OpenStruct.new(name: :name, type: String) }
|
7
|
+
let(:serialized) { ValueWithAttribute.new('foo', attribute) }
|
8
|
+
|
9
|
+
it 'contains a value and an attribute' do
|
10
|
+
serialized.value.should == 'foo'
|
11
|
+
serialized.attribute.should == attribute
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'knows its type' do
|
15
|
+
serialized.type.should be String
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when attribute is embedded' do
|
19
|
+
let(:attribute) { OpenStruct.new(embedded?: true) }
|
20
|
+
it 'is embedded' do
|
21
|
+
serialized.should be_embedded
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when attribute is not embedded' do
|
26
|
+
let(:attribute) { OpenStruct.new(embedded?: false) }
|
27
|
+
|
28
|
+
it 'is not embedded' do
|
29
|
+
serialized.should_not be_embedded
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|