perpetuity-postgres 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +13 -0
- data/CHANGELOG.md +9 -0
- data/Rakefile +5 -0
- data/lib/perpetuity/postgres/index.rb +5 -1
- data/lib/perpetuity/postgres/serialized_data.rb +5 -0
- data/lib/perpetuity/postgres/serializer.rb +2 -0
- data/lib/perpetuity/postgres/table/attribute.rb +14 -19
- data/lib/perpetuity/postgres/version.rb +1 -1
- data/lib/perpetuity/postgres.rb +34 -8
- data/spec/perpetuity/postgres/index_spec.rb +3 -3
- data/spec/perpetuity/postgres/serialized_data_spec.rb +5 -0
- data/spec/perpetuity/postgres/serializer_spec.rb +9 -4
- data/spec/perpetuity/postgres/table/attribute_spec.rb +8 -2
- data/spec/perpetuity/postgres/table_spec.rb +1 -1
- data/spec/perpetuity/postgres_spec.rb +23 -8
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 558b498d177b994d2d6ff89101aba27d040a4fdf
|
4
|
+
data.tar.gz: e69cfe6e5454170c6565c7018eb002f5d2e3e161
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39653126b7754933a22d9ba8fd8dac82436c71340c298954245783a84bd1868c6a566937ead0d5379d300fd98becd7c773c9f143d1db43f9bcf9e8879817647d
|
7
|
+
data.tar.gz: 4bb4d6b025ed091a7b975b379d45bd7514215960771fc1b2f3a5ac4fc9cbd03cfa63e264057d36ea7d64f944ec61cc066dc3c10b798ac2d1d81533c4db7fe821
|
data/.travis.yml
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## Version 0.0.5
|
2
|
+
|
3
|
+
- Name indexes explicitly based on table and attributes. Previously we were counting on the PostgreSQL naming convention, but that could break in future versions of Postgres.
|
4
|
+
- Fixed a bug that was causing duplicate indexes to be built
|
5
|
+
- Deserialize booleans. They should be declared in the mapper as `TrueClass` or `FalseClass`.
|
6
|
+
- Fixed a bug that choked when you tried to save an attribute for an object if that column was not yet added.
|
7
|
+
- Allow deleting an array of objects
|
8
|
+
- Removed VARCHAR conversion. VARCHAR and TEXT are stored exactly the same in PostgreSQL. There's no reason to limit yourself to a VARCHAR unless you're using table constraints, which are far more flexible.
|
9
|
+
|
1
10
|
## Version 0.0.4
|
2
11
|
|
3
12
|
- Set minimum logging from the database to warnings. This silences info messages like those from `DROP TABLE IF NOT EXISTS ...` when the table doesn't exist.
|
data/Rakefile
CHANGED
@@ -49,7 +49,7 @@ module Perpetuity
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def table
|
52
|
-
name.gsub("_#{attributes.map(&:to_s).join('_')}
|
52
|
+
name.gsub("_#{attributes.map(&:to_s).join('_')}_index", '')
|
53
53
|
end
|
54
54
|
|
55
55
|
def == other
|
@@ -62,6 +62,10 @@ module Perpetuity
|
|
62
62
|
def eql? other
|
63
63
|
self == other
|
64
64
|
end
|
65
|
+
|
66
|
+
def hash
|
67
|
+
name.to_s.hash
|
68
|
+
end
|
65
69
|
end
|
66
70
|
end
|
67
71
|
end
|
@@ -15,6 +15,11 @@ module Perpetuity
|
|
15
15
|
"(#{column_names.join(',')}) VALUES #{value_strings}"
|
16
16
|
end
|
17
17
|
|
18
|
+
def [] key
|
19
|
+
index = column_names.index(key.to_s)
|
20
|
+
values.first[index]
|
21
|
+
end
|
22
|
+
|
18
23
|
def []= column, value
|
19
24
|
value = TextValue.new(value)
|
20
25
|
if column_names.include? column
|
@@ -9,6 +9,19 @@ module Perpetuity
|
|
9
9
|
NoDefaultValue = Module.new
|
10
10
|
UUID = Module.new
|
11
11
|
|
12
|
+
SQL_TYPE_MAP = {
|
13
|
+
String => 'TEXT',
|
14
|
+
Integer => 'BIGINT',
|
15
|
+
Fixnum => 'BIGINT',
|
16
|
+
Bignum => 'NUMERIC',
|
17
|
+
BigDecimal => 'NUMERIC',
|
18
|
+
Float => 'FLOAT',
|
19
|
+
UUID => 'UUID',
|
20
|
+
Time => 'TIMESTAMPTZ',
|
21
|
+
TrueClass => 'BOOLEAN',
|
22
|
+
FalseClass => 'BOOLEAN'
|
23
|
+
}.tap{|m| m.default = 'JSON' }
|
24
|
+
|
12
25
|
def initialize name, type, options={}
|
13
26
|
@name = name
|
14
27
|
@type = type
|
@@ -18,25 +31,7 @@ module Perpetuity
|
|
18
31
|
end
|
19
32
|
|
20
33
|
def sql_type
|
21
|
-
|
22
|
-
if max_length
|
23
|
-
"VARCHAR(#{max_length})"
|
24
|
-
else
|
25
|
-
'TEXT'
|
26
|
-
end
|
27
|
-
elsif type == Integer or type == Fixnum
|
28
|
-
'BIGINT'
|
29
|
-
elsif type == Bignum or type == BigDecimal
|
30
|
-
'NUMERIC'
|
31
|
-
elsif type == Float
|
32
|
-
'FLOAT'
|
33
|
-
elsif type == UUID
|
34
|
-
'UUID'
|
35
|
-
elsif type == Time
|
36
|
-
'TIMESTAMPTZ'
|
37
|
-
else
|
38
|
-
'JSON'
|
39
|
-
end
|
34
|
+
SQL_TYPE_MAP[type]
|
40
35
|
end
|
41
36
|
|
42
37
|
def sql_declaration
|
data/lib/perpetuity/postgres.rb
CHANGED
@@ -70,11 +70,23 @@ module Perpetuity
|
|
70
70
|
raise
|
71
71
|
end
|
72
72
|
|
73
|
-
def delete
|
73
|
+
def delete ids, klass
|
74
|
+
ids = Array(ids)
|
74
75
|
table = TableName.new(klass)
|
75
|
-
|
76
|
-
|
77
|
-
|
76
|
+
|
77
|
+
if ids.one?
|
78
|
+
id_string = TextValue.new(ids.first)
|
79
|
+
sql = "DELETE FROM #{table} WHERE id = #{id_string}"
|
80
|
+
|
81
|
+
connection.execute(sql).to_a
|
82
|
+
elsif ids.none?
|
83
|
+
# Do nothing, we weren't given anything to delete
|
84
|
+
else
|
85
|
+
id_string = ids.map { |id| TextValue.new(id) }
|
86
|
+
sql = "DELETE FROM #{table} WHERE id IN (#{id_string.join(',')})"
|
87
|
+
|
88
|
+
connection.execute(sql).to_a
|
89
|
+
end
|
78
90
|
end
|
79
91
|
|
80
92
|
def count klass, query='TRUE', options={}, &block
|
@@ -128,10 +140,19 @@ module Perpetuity
|
|
128
140
|
def update klass, id, attributes
|
129
141
|
sql = SQLUpdate.new(klass, id, attributes).to_s
|
130
142
|
connection.execute(sql).to_a
|
143
|
+
rescue PG::UndefinedColumn => e
|
144
|
+
if e.message =~ /column "(.*)" of relation "(.*)" does not exist/
|
145
|
+
column_name = $1
|
146
|
+
table_name = $2
|
147
|
+
add_column table_name, column_name, [Attribute.new(column_name, attributes[column_name].class)]
|
148
|
+
retry
|
149
|
+
else
|
150
|
+
raise e
|
151
|
+
end
|
131
152
|
end
|
132
153
|
|
133
154
|
def index klass, attributes, options={}
|
134
|
-
name = "#{klass}_#{Array(attributes).map(&:name).join('_')}
|
155
|
+
name = "#{klass}_#{Array(attributes).map(&:name).join('_')}_index"
|
135
156
|
index = Index.new(name: name,
|
136
157
|
attributes: Array(attributes),
|
137
158
|
unique: !!options[:unique],
|
@@ -142,13 +163,18 @@ module Perpetuity
|
|
142
163
|
|
143
164
|
def indexes klass
|
144
165
|
@indexes ||= {}
|
145
|
-
@indexes[klass] ||=
|
166
|
+
@indexes[klass] ||= active_indexes(klass)
|
146
167
|
end
|
147
168
|
|
148
169
|
def activate_index! index
|
170
|
+
return if index.active?
|
171
|
+
|
172
|
+
attribute_names = index.attribute_names.join('_')
|
173
|
+
index_name = "#{index.table}_#{attribute_names}_index"
|
149
174
|
sql = "CREATE "
|
150
175
|
sql << "UNIQUE " if index.unique?
|
151
|
-
sql << "INDEX
|
176
|
+
sql << "INDEX \"#{index_name}\" "
|
177
|
+
sql << "ON #{TableName.new(index.table)} (#{attribute_names})"
|
152
178
|
connection.execute(sql)
|
153
179
|
index.activate!
|
154
180
|
rescue PG::UndefinedTable => e
|
@@ -168,7 +194,7 @@ module Perpetuity
|
|
168
194
|
pg_index.indisready AS active
|
169
195
|
FROM pg_class
|
170
196
|
INNER JOIN pg_index ON pg_class.oid = pg_index.indexrelid
|
171
|
-
WHERE pg_class.relname ~ '^#{table}.*
|
197
|
+
WHERE pg_class.relname ~ '^#{table}.*index$'
|
172
198
|
SQL
|
173
199
|
|
174
200
|
indexes = connection.execute(sql).map do |index|
|
@@ -5,7 +5,7 @@ module Perpetuity
|
|
5
5
|
describe Index do
|
6
6
|
it 'can be generated from SQL results' do
|
7
7
|
index_hash = {
|
8
|
-
"name"=>"
|
8
|
+
"name"=>"Object_id_name_index",
|
9
9
|
"attributes"=>"{id,name}",
|
10
10
|
"unique"=>"t",
|
11
11
|
"active"=>"t"
|
@@ -13,7 +13,7 @@ module Perpetuity
|
|
13
13
|
|
14
14
|
index = Index.from_sql(index_hash)
|
15
15
|
index.attribute_names.should == ['id', 'name']
|
16
|
-
index.name.should == '
|
16
|
+
index.name.should == 'Object_id_name_index'
|
17
17
|
index.table.should == 'Object'
|
18
18
|
index.should be_unique
|
19
19
|
index.should be_active
|
@@ -47,7 +47,7 @@ module Perpetuity
|
|
47
47
|
it 'is not equal to an index with different attributes' do
|
48
48
|
new_index = Index.new(attributes: [:lol],
|
49
49
|
name: name,
|
50
|
-
unique:
|
50
|
+
unique: unique)
|
51
51
|
|
52
52
|
new_index.should_not == index
|
53
53
|
end
|
@@ -55,6 +55,11 @@ module Perpetuity
|
|
55
55
|
[['name', "'Jamie'"], ['age', 31]]
|
56
56
|
end
|
57
57
|
|
58
|
+
it 'accesses values like a hash' do
|
59
|
+
serialized['age'].should == 31
|
60
|
+
serialized[:age].should == 31
|
61
|
+
end
|
62
|
+
|
58
63
|
it 'equals another with the same data' do
|
59
64
|
original = SerializedData.new([:a, :b], [1, 2])
|
60
65
|
duplicate = SerializedData.new([:a, :b], [1, 2])
|
@@ -125,12 +125,13 @@ module Perpetuity
|
|
125
125
|
|
126
126
|
let(:article_class) do
|
127
127
|
Class.new do
|
128
|
-
attr_reader :title, :body, :views, :published_at
|
128
|
+
attr_reader :title, :body, :views, :published_at, :published
|
129
129
|
def initialize attributes={}
|
130
130
|
@title = attributes[:title]
|
131
131
|
@body = attributes[:body]
|
132
132
|
@views = attributes.fetch(:views) { 0 }
|
133
133
|
@published_at = attributes.fetch(:published_at) { Time.now }
|
134
|
+
@published = attributes.fetch(:published) { false }
|
134
135
|
end
|
135
136
|
|
136
137
|
def == other
|
@@ -138,7 +139,8 @@ module Perpetuity
|
|
138
139
|
other.title == title &&
|
139
140
|
other.body == body &&
|
140
141
|
other.views == views &&
|
141
|
-
other.published_at == published_at
|
142
|
+
other.published_at == published_at &&
|
143
|
+
other.published == published
|
142
144
|
end
|
143
145
|
end
|
144
146
|
end
|
@@ -151,6 +153,7 @@ module Perpetuity
|
|
151
153
|
attribute :body, type: String
|
152
154
|
attribute :views, type: Integer
|
153
155
|
attribute :published_at, type: Time
|
156
|
+
attribute :published, type: TrueClass
|
154
157
|
end.new(registry)
|
155
158
|
end
|
156
159
|
|
@@ -161,14 +164,16 @@ module Perpetuity
|
|
161
164
|
'title' => 'Title',
|
162
165
|
'body' => 'Body',
|
163
166
|
'views' => '0',
|
164
|
-
'published_at' => '2013-01-02 03:04:05.123456-05'
|
167
|
+
'published_at' => '2013-01-02 03:04:05.123456-05',
|
168
|
+
'published' => 't'
|
165
169
|
}
|
166
170
|
|
167
171
|
article = article_class.new(
|
168
172
|
title: 'Title',
|
169
173
|
body: 'Body',
|
170
174
|
views: 0,
|
171
|
-
published_at: Time.new(2013, 1, 2, 3, 4, 5.123456, '-05:00')
|
175
|
+
published_at: Time.new(2013, 1, 2, 3, 4, 5.123456, '-05:00'),
|
176
|
+
published: true
|
172
177
|
)
|
173
178
|
serializer.unserialize(serialized_article).should == article
|
174
179
|
end
|
@@ -5,7 +5,7 @@ module Perpetuity
|
|
5
5
|
class Postgres
|
6
6
|
class Table
|
7
7
|
describe Attribute do
|
8
|
-
let(:title) { Attribute.new('title', String
|
8
|
+
let(:title) { Attribute.new('title', String) }
|
9
9
|
|
10
10
|
it 'knows its name' do
|
11
11
|
title.name.should == 'title'
|
@@ -44,7 +44,6 @@ module Perpetuity
|
|
44
44
|
let(:body) { Attribute.new('body', String, default: 'foo') }
|
45
45
|
|
46
46
|
it 'converts to the proper SQL type' do
|
47
|
-
title.sql_type.should == 'VARCHAR(40)'
|
48
47
|
body.sql_type.should == 'TEXT'
|
49
48
|
end
|
50
49
|
|
@@ -81,6 +80,13 @@ module Perpetuity
|
|
81
80
|
end
|
82
81
|
end
|
83
82
|
|
83
|
+
describe 'booleans' do
|
84
|
+
it 'is stored in a BOOLEAN column' do
|
85
|
+
Attribute.new(:true, TrueClass).sql_type.should == 'BOOLEAN'
|
86
|
+
Attribute.new(:false, FalseClass).sql_type.should == 'BOOLEAN'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
84
90
|
describe 'non-serializable types' do
|
85
91
|
let(:author) { Attribute.new('author', Object) }
|
86
92
|
|
@@ -26,7 +26,7 @@ module Perpetuity
|
|
26
26
|
|
27
27
|
it 'generates proper SQL to create itself' do
|
28
28
|
table.create_table_sql.should ==
|
29
|
-
'CREATE TABLE IF NOT EXISTS "Article" (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), title
|
29
|
+
'CREATE TABLE IF NOT EXISTS "Article" (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), title TEXT, body TEXT, author JSON, published_at TIMESTAMPTZ, views BIGINT)'
|
30
30
|
end
|
31
31
|
|
32
32
|
describe 'id column' do
|
@@ -141,12 +141,6 @@ module Perpetuity
|
|
141
141
|
postgres.retrieve('Article', 'TRUE').should == []
|
142
142
|
end
|
143
143
|
|
144
|
-
it 'deletes all records' do
|
145
|
-
postgres.insert 'User', data, attributes
|
146
|
-
postgres.delete_all 'User'
|
147
|
-
postgres.count('User').should == 0
|
148
|
-
end
|
149
|
-
|
150
144
|
it 'updates a specific record' do
|
151
145
|
id = postgres.insert('User', data, attributes).first
|
152
146
|
postgres.update 'User', id, name: 'foo'
|
@@ -155,9 +149,30 @@ module Perpetuity
|
|
155
149
|
retrieved.first['name'].should == 'foo'
|
156
150
|
end
|
157
151
|
|
158
|
-
it '
|
152
|
+
it 'updates a record when a column does not currently exist' do
|
159
153
|
id = postgres.insert('User', data, attributes).first
|
160
|
-
|
154
|
+
postgres.update 'User', id, Postgres::SerializedData.new(['foo'], ["'bar'"])
|
155
|
+
|
156
|
+
retrieved = postgres.retrieve('User', "id = '#{id}'")
|
157
|
+
retrieved.first['foo'].should == 'bar'
|
158
|
+
end
|
159
|
+
|
160
|
+
describe 'deletion' do
|
161
|
+
it 'deletes all records' do
|
162
|
+
postgres.insert 'User', data, attributes
|
163
|
+
postgres.delete_all 'User'
|
164
|
+
postgres.count('User').should == 0
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'deletes a record with a specific id' do
|
168
|
+
id = postgres.insert('User', data, attributes).first
|
169
|
+
expect { postgres.delete id, 'User' }.to change { postgres.count 'User' }.by -1
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'deletes records with specific ids' do
|
173
|
+
ids = Array.new(3) { postgres.insert('User', data, attributes).first }
|
174
|
+
expect { postgres.delete ids.take(2), 'User' }.to change { postgres.count 'User' }.by -2
|
175
|
+
end
|
161
176
|
end
|
162
177
|
|
163
178
|
describe 'incrementing/decrementing' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perpetuity-postgres
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jamie Gaskins
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-04-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -88,6 +88,7 @@ extensions: []
|
|
88
88
|
extra_rdoc_files: []
|
89
89
|
files:
|
90
90
|
- .gitignore
|
91
|
+
- .travis.yml
|
91
92
|
- CHANGELOG.md
|
92
93
|
- Gemfile
|
93
94
|
- LICENSE.txt
|
@@ -178,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
179
|
version: '0'
|
179
180
|
requirements: []
|
180
181
|
rubyforge_project:
|
181
|
-
rubygems_version: 2.
|
182
|
+
rubygems_version: 2.2.2
|
182
183
|
signing_key:
|
183
184
|
specification_version: 4
|
184
185
|
summary: PostgreSQL adapter for Perpetuity
|