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.
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