perpetuity-postgres 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +34 -0
  6. data/Rakefile +1 -0
  7. data/lib/perpetuity/postgres/boolean_value.rb +17 -0
  8. data/lib/perpetuity/postgres/connection.rb +78 -0
  9. data/lib/perpetuity/postgres/connection_pool.rb +39 -0
  10. data/lib/perpetuity/postgres/expression.rb +21 -0
  11. data/lib/perpetuity/postgres/json_array.rb +31 -0
  12. data/lib/perpetuity/postgres/json_hash.rb +55 -0
  13. data/lib/perpetuity/postgres/json_string_value.rb +17 -0
  14. data/lib/perpetuity/postgres/negated_query.rb +21 -0
  15. data/lib/perpetuity/postgres/nil_query.rb +9 -0
  16. data/lib/perpetuity/postgres/null_value.rb +9 -0
  17. data/lib/perpetuity/postgres/numeric_value.rb +17 -0
  18. data/lib/perpetuity/postgres/query.rb +34 -0
  19. data/lib/perpetuity/postgres/query_attribute.rb +33 -0
  20. data/lib/perpetuity/postgres/query_expression.rb +90 -0
  21. data/lib/perpetuity/postgres/query_intersection.rb +26 -0
  22. data/lib/perpetuity/postgres/query_union.rb +26 -0
  23. data/lib/perpetuity/postgres/serialized_data.rb +63 -0
  24. data/lib/perpetuity/postgres/serializer.rb +179 -0
  25. data/lib/perpetuity/postgres/sql_select.rb +71 -0
  26. data/lib/perpetuity/postgres/sql_update.rb +28 -0
  27. data/lib/perpetuity/postgres/sql_value.rb +40 -0
  28. data/lib/perpetuity/postgres/table/attribute.rb +60 -0
  29. data/lib/perpetuity/postgres/table.rb +36 -0
  30. data/lib/perpetuity/postgres/table_name.rb +21 -0
  31. data/lib/perpetuity/postgres/text_value.rb +14 -0
  32. data/lib/perpetuity/postgres/timestamp_value.rb +68 -0
  33. data/lib/perpetuity/postgres/value_with_attribute.rb +19 -0
  34. data/lib/perpetuity/postgres/version.rb +5 -0
  35. data/lib/perpetuity/postgres.rb +170 -0
  36. data/perpetuity-postgres.gemspec +26 -0
  37. data/spec/perpetuity/postgres/boolean_value_spec.rb +15 -0
  38. data/spec/perpetuity/postgres/connection_pool_spec.rb +55 -0
  39. data/spec/perpetuity/postgres/connection_spec.rb +30 -0
  40. data/spec/perpetuity/postgres/expression_spec.rb +13 -0
  41. data/spec/perpetuity/postgres/json_array_spec.rb +31 -0
  42. data/spec/perpetuity/postgres/json_hash_spec.rb +39 -0
  43. data/spec/perpetuity/postgres/json_string_value_spec.rb +11 -0
  44. data/spec/perpetuity/postgres/negated_query_spec.rb +39 -0
  45. data/spec/perpetuity/postgres/null_value_spec.rb +11 -0
  46. data/spec/perpetuity/postgres/numeric_value_spec.rb +12 -0
  47. data/spec/perpetuity/postgres/query_attribute_spec.rb +48 -0
  48. data/spec/perpetuity/postgres/query_expression_spec.rb +110 -0
  49. data/spec/perpetuity/postgres/query_intersection_spec.rb +23 -0
  50. data/spec/perpetuity/postgres/query_spec.rb +23 -0
  51. data/spec/perpetuity/postgres/query_union_spec.rb +23 -0
  52. data/spec/perpetuity/postgres/serialized_data_spec.rb +69 -0
  53. data/spec/perpetuity/postgres/serializer_spec.rb +216 -0
  54. data/spec/perpetuity/postgres/sql_select_spec.rb +51 -0
  55. data/spec/perpetuity/postgres/sql_update_spec.rb +22 -0
  56. data/spec/perpetuity/postgres/sql_value_spec.rb +38 -0
  57. data/spec/perpetuity/postgres/table/attribute_spec.rb +82 -0
  58. data/spec/perpetuity/postgres/table_name_spec.rb +15 -0
  59. data/spec/perpetuity/postgres/table_spec.rb +43 -0
  60. data/spec/perpetuity/postgres/text_value_spec.rb +15 -0
  61. data/spec/perpetuity/postgres/timestamp_value_spec.rb +28 -0
  62. data/spec/perpetuity/postgres/value_with_attribute_spec.rb +34 -0
  63. data/spec/perpetuity/postgres_spec.rb +163 -0
  64. data/spec/support/test_classes/book.rb +16 -0
  65. data/spec/support/test_classes/person.rb +11 -0
  66. 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