perpetuity-postgres 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|