perpetuity-postgres 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/lib/perpetuity/postgres/connection.rb +12 -5
- data/lib/perpetuity/postgres/index.rb +67 -0
- data/lib/perpetuity/postgres/index_collection.rb +53 -0
- data/lib/perpetuity/postgres/json_hash.rb +2 -0
- data/lib/perpetuity/postgres/json_string_value.rb +1 -1
- data/lib/perpetuity/postgres/query_attribute.rb +13 -0
- data/lib/perpetuity/postgres/serialized_data.rb +1 -1
- data/lib/perpetuity/postgres/serializer.rb +1 -1
- data/lib/perpetuity/postgres/sql_function.rb +15 -0
- data/lib/perpetuity/postgres/table/attribute.rb +6 -2
- data/lib/perpetuity/postgres/table_name.rb +2 -0
- data/lib/perpetuity/postgres/version.rb +1 -1
- data/lib/perpetuity/postgres.rb +100 -2
- data/perpetuity-postgres.gemspec +1 -1
- data/spec/perpetuity/postgres/connection_spec.rb +1 -0
- data/spec/perpetuity/postgres/index_collection_spec.rb +29 -0
- data/spec/perpetuity/postgres/index_spec.rb +83 -0
- data/spec/perpetuity/postgres/json_hash_spec.rb +4 -0
- data/spec/perpetuity/postgres/json_string_value_spec.rb +9 -0
- data/spec/perpetuity/postgres/query_attribute_spec.rb +8 -0
- data/spec/perpetuity/postgres/serialized_data_spec.rb +9 -4
- data/spec/perpetuity/postgres/sql_function_spec.rb +17 -0
- data/spec/perpetuity/postgres/table/attribute_spec.rb +13 -1
- data/spec/perpetuity/postgres/table_name_spec.rb +4 -0
- data/spec/perpetuity/postgres/table_spec.rb +1 -1
- data/spec/perpetuity/postgres_spec.rb +111 -7
- metadata +15 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45cd2bfa1345abc08e97eda5e929c3ef1c4b7c23
|
4
|
+
data.tar.gz: 2cab12a23a5f25a0f114b76494f283793f7213be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79578054a8adc0164fd5e5197f6b52f0ed337c8af13b6910dfef79060dedea6c6e08f8310c9ad26b01fc70e756896996820ea695bf2ee9a70a1a16037fba2ba8
|
7
|
+
data.tar.gz: a0d72ba46bff515cc6d7c222fd68e63ea780aee5fcf2f534a4c9f8963f1afa54f15739c5335aa8e15591d43fe8f52e8a2e1d76d351c421e1e85ba119901b4c44
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
## Version 0.0.2
|
2
|
+
|
3
|
+
- Make DB creation call out to remote DB to create it instead of calling on localhost.
|
4
|
+
- Add index support
|
5
|
+
- Only load UUID extension on UUID failure. This keeps the DB log from getting flooded every time the app connects to it.
|
6
|
+
- Add support for any?/none? on query attributes for queries like `mapper.select { |user| user.friends.any? }`
|
7
|
+
- Serialize nil values in JSONHash
|
8
|
+
- Add support for in-place increment/decrement. This allows you to use a mapper's `#increment`/`#decrement` methods to modify the object without pulling it down from the DB first.
|
9
|
+
- Keep first value when inserting multiple records. This fixes a bug where a `SerializedData` value (the data structure holding what's being inserted) would modify itself in place on `each` calls.
|
10
|
+
- Cast numeric ids to numeric types on insertion. Previously, if you had an `Integer` id column, it was being returned as a string.
|
11
|
+
- Don't allow double quotes in table names. Postgres doesn't allow it, so this catches it before we have to make the DB call.
|
12
|
+
- Add support for more numeric types
|
13
|
+
- Escape double quotes in JSON strings
|
14
|
+
- Add table columns automatically if they don't exist. This way, you can simply add more attributes in your mapper and Perpetuity::Postgres will take care of it on its own.
|
15
|
+
|
16
|
+
## Version 0.0.1
|
17
|
+
|
18
|
+
- Initial release.
|
19
|
+
- Most Perpetuity CRUD features enabled.
|
@@ -19,12 +19,12 @@ module Perpetuity
|
|
19
19
|
|
20
20
|
def connect
|
21
21
|
@pg_connection = PG.connect(options)
|
22
|
-
use_uuid_extension
|
23
|
-
|
24
|
-
@pg_connection
|
25
22
|
rescue PG::ConnectionBad => e
|
26
23
|
tries ||= 0
|
27
|
-
|
24
|
+
connect_options = options.dup
|
25
|
+
connect_options.delete :dbname
|
26
|
+
|
27
|
+
conn = PG.connect connect_options
|
28
28
|
conn.exec "CREATE DATABASE #{db}"
|
29
29
|
conn.close
|
30
30
|
|
@@ -41,6 +41,13 @@ module Perpetuity
|
|
41
41
|
|
42
42
|
def execute sql
|
43
43
|
pg_connection.exec sql
|
44
|
+
rescue PG::UndefinedFunction => e
|
45
|
+
if e.message =~ /uuid_generate/
|
46
|
+
use_uuid_extension
|
47
|
+
retry
|
48
|
+
else
|
49
|
+
raise
|
50
|
+
end
|
44
51
|
end
|
45
52
|
|
46
53
|
def tables
|
@@ -69,7 +76,7 @@ module Perpetuity
|
|
69
76
|
|
70
77
|
private
|
71
78
|
def use_uuid_extension
|
72
|
-
|
79
|
+
execute 'CREATE EXTENSION "uuid-ossp"'
|
73
80
|
rescue PG::DuplicateObject
|
74
81
|
# Ignore. It just means the extension's already been loaded.
|
75
82
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'perpetuity/attribute'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
class Index
|
6
|
+
attr_reader :attributes, :name
|
7
|
+
|
8
|
+
def initialize options={}
|
9
|
+
@attributes = options.fetch(:attributes)
|
10
|
+
@name = options.fetch(:name)
|
11
|
+
@unique = options.fetch(:unique) { false }
|
12
|
+
@active = options.fetch(:active) { false }
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_sql sql_result
|
16
|
+
attributes = sql_result['attributes'].gsub(/[{}]/, '').split(',').map do |attr|
|
17
|
+
Attribute.new(attr)
|
18
|
+
end
|
19
|
+
unique = sql_result['unique'] == 't'
|
20
|
+
active = sql_result['active'] == 't'
|
21
|
+
new(name: sql_result['name'],
|
22
|
+
attributes: attributes,
|
23
|
+
unique: unique,
|
24
|
+
active: active)
|
25
|
+
end
|
26
|
+
|
27
|
+
def attribute
|
28
|
+
attributes.first
|
29
|
+
end
|
30
|
+
|
31
|
+
def attribute_names
|
32
|
+
attributes.map { |attr| attr.name.to_s }
|
33
|
+
end
|
34
|
+
|
35
|
+
def unique?
|
36
|
+
!!@unique
|
37
|
+
end
|
38
|
+
|
39
|
+
def active?
|
40
|
+
!!@active
|
41
|
+
end
|
42
|
+
|
43
|
+
def inactive?
|
44
|
+
!active?
|
45
|
+
end
|
46
|
+
|
47
|
+
def activate!
|
48
|
+
@active = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def table
|
52
|
+
name.gsub("_#{attributes.map(&:to_s).join('_')}_idx", '')
|
53
|
+
end
|
54
|
+
|
55
|
+
def == other
|
56
|
+
other.is_a?(self.class) &&
|
57
|
+
attributes.map(&:to_s) == other.attributes.map(&:to_s) &&
|
58
|
+
name.to_s == other.name.to_s &&
|
59
|
+
unique? == other.unique?
|
60
|
+
end
|
61
|
+
|
62
|
+
def eql? other
|
63
|
+
self == other
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
class IndexCollection
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_reader :table
|
9
|
+
|
10
|
+
def initialize table, *indexes
|
11
|
+
@table = table.to_s
|
12
|
+
@indexes = indexes.flatten.to_set
|
13
|
+
end
|
14
|
+
|
15
|
+
def << index
|
16
|
+
@indexes << index
|
17
|
+
end
|
18
|
+
|
19
|
+
def each
|
20
|
+
@indexes.each { |index| yield index }
|
21
|
+
end
|
22
|
+
|
23
|
+
def reject! &block
|
24
|
+
@indexes.reject!(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_a
|
28
|
+
@indexes.to_a
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_ary
|
32
|
+
to_a
|
33
|
+
end
|
34
|
+
|
35
|
+
def - other
|
36
|
+
difference = self.class.new(table)
|
37
|
+
each do |index|
|
38
|
+
unless other.include? index
|
39
|
+
difference << index
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
difference
|
44
|
+
end
|
45
|
+
|
46
|
+
def == other
|
47
|
+
table == other.table &&
|
48
|
+
count == other.count &&
|
49
|
+
(self - other).empty?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'perpetuity/postgres/query_expression'
|
2
|
+
require 'perpetuity/postgres/sql_function'
|
2
3
|
|
3
4
|
module Perpetuity
|
4
5
|
class Postgres
|
@@ -25,6 +26,18 @@ METHOD
|
|
25
26
|
QueryExpression.new self, :==, nil
|
26
27
|
end
|
27
28
|
|
29
|
+
def count
|
30
|
+
SQLFunction.new('json_array_length', self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def any?
|
34
|
+
QueryExpression.new count, :>, 0
|
35
|
+
end
|
36
|
+
|
37
|
+
def none?
|
38
|
+
QueryExpression.new count, :==, 0
|
39
|
+
end
|
40
|
+
|
28
41
|
def to_s
|
29
42
|
name.to_s
|
30
43
|
end
|
@@ -59,7 +59,7 @@ module Perpetuity
|
|
59
59
|
value = unserialize_foreign_object value
|
60
60
|
end
|
61
61
|
if attribute
|
62
|
-
if attribute.type
|
62
|
+
if [Fixnum, Bignum, Integer].include? attribute.type
|
63
63
|
value = value.to_i
|
64
64
|
elsif attribute.type == Time
|
65
65
|
value = TimestampValue.from_sql(value).to_time
|
@@ -22,8 +22,12 @@ module Perpetuity
|
|
22
22
|
else
|
23
23
|
'TEXT'
|
24
24
|
end
|
25
|
-
elsif type == Integer
|
26
|
-
'
|
25
|
+
elsif type == Integer or type == Fixnum
|
26
|
+
'BIGINT'
|
27
|
+
elsif type == Bignum or type == BigDecimal
|
28
|
+
'NUMERIC'
|
29
|
+
elsif type == Float
|
30
|
+
'FLOAT'
|
27
31
|
elsif type == UUID
|
28
32
|
'UUID'
|
29
33
|
elsif type == Time
|
@@ -1,8 +1,10 @@
|
|
1
1
|
module Perpetuity
|
2
2
|
class Postgres
|
3
|
+
InvalidTableName = Class.new(StandardError)
|
3
4
|
class TableName
|
4
5
|
def initialize name
|
5
6
|
@name = name.to_s
|
7
|
+
raise InvalidTableName, "PostgreSQL table name cannot contain double quotes" if @name.include? '"'
|
6
8
|
end
|
7
9
|
|
8
10
|
def to_s
|
data/lib/perpetuity/postgres.rb
CHANGED
@@ -8,6 +8,8 @@ require 'perpetuity/postgres/table'
|
|
8
8
|
require 'perpetuity/postgres/table/attribute'
|
9
9
|
require 'perpetuity/postgres/sql_select'
|
10
10
|
require 'perpetuity/postgres/sql_update'
|
11
|
+
require 'perpetuity/postgres/index_collection'
|
12
|
+
require 'perpetuity/postgres/index'
|
11
13
|
|
12
14
|
module Perpetuity
|
13
15
|
class Postgres
|
@@ -38,7 +40,7 @@ module Perpetuity
|
|
38
40
|
sql = "INSERT INTO #{table} #{data} RETURNING id"
|
39
41
|
|
40
42
|
results = connection.execute(sql).to_a
|
41
|
-
ids = results.map { |result| result['id'] }
|
43
|
+
ids = results.map { |result| cast_id(result['id'], attributes[:id]) }
|
42
44
|
|
43
45
|
ids
|
44
46
|
rescue PG::UndefinedTable => e # Table doesn't exist, so we create it.
|
@@ -47,6 +49,25 @@ module Perpetuity
|
|
47
49
|
create_table_with_attributes klass, attributes
|
48
50
|
retry unless retries > 1
|
49
51
|
raise e
|
52
|
+
rescue PG::UndefinedColumn => e
|
53
|
+
retries ||= 0
|
54
|
+
retries += 1
|
55
|
+
error ||= nil
|
56
|
+
|
57
|
+
if retries > 1 && e.message == error
|
58
|
+
# We've retried more than once and we're getting the same error
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
|
62
|
+
error = e.message
|
63
|
+
if error =~ /column "(.+)" of relation "(.+)" does not exist/
|
64
|
+
column_name = $1
|
65
|
+
table_name = $2
|
66
|
+
add_column table_name, column_name, attributes
|
67
|
+
retry
|
68
|
+
end
|
69
|
+
|
70
|
+
raise
|
50
71
|
end
|
51
72
|
|
52
73
|
def delete id, klass
|
@@ -109,12 +130,64 @@ module Perpetuity
|
|
109
130
|
connection.execute(sql).to_a
|
110
131
|
end
|
111
132
|
|
133
|
+
def index klass, attributes, options={}
|
134
|
+
name = "#{klass}_#{Array(attributes).map(&:name).join('_')}_idx"
|
135
|
+
index = Index.new(name: name,
|
136
|
+
attributes: Array(attributes),
|
137
|
+
unique: !!options[:unique],
|
138
|
+
active: false)
|
139
|
+
indexes(klass) << index
|
140
|
+
index
|
141
|
+
end
|
142
|
+
|
143
|
+
def indexes klass
|
144
|
+
@indexes ||= {}
|
145
|
+
@indexes[klass] ||= IndexCollection.new(klass)
|
146
|
+
end
|
147
|
+
|
148
|
+
def activate_index! index
|
149
|
+
sql = "CREATE "
|
150
|
+
sql << "UNIQUE " if index.unique?
|
151
|
+
sql << "INDEX ON #{TableName.new(index.table)} (#{index.attribute_names.join(',')})"
|
152
|
+
connection.execute(sql)
|
153
|
+
index.activate!
|
154
|
+
rescue PG::UndefinedTable => e
|
155
|
+
create_table_with_attributes index.table, index.attributes
|
156
|
+
retry
|
157
|
+
end
|
158
|
+
|
159
|
+
def active_indexes table
|
160
|
+
sql = <<-SQL
|
161
|
+
SELECT pg_class.relname AS name,
|
162
|
+
ARRAY(
|
163
|
+
SELECT pg_get_indexdef(pg_index.indexrelid, k + 1, true)
|
164
|
+
FROM generate_subscripts(pg_index.indkey, 1) AS k
|
165
|
+
ORDER BY k
|
166
|
+
) AS attributes,
|
167
|
+
pg_index.indisunique AS unique,
|
168
|
+
pg_index.indisready AS active
|
169
|
+
FROM pg_class
|
170
|
+
INNER JOIN pg_index ON pg_class.oid = pg_index.indexrelid
|
171
|
+
WHERE pg_class.relname ~ '^#{table}.*idx$'
|
172
|
+
SQL
|
173
|
+
|
174
|
+
indexes = connection.execute(sql).map do |index|
|
175
|
+
Index.from_sql(index)
|
176
|
+
end
|
177
|
+
IndexCollection.new(table, indexes)
|
178
|
+
end
|
179
|
+
|
180
|
+
def remove_index index
|
181
|
+
sql = %Q{DROP INDEX IF EXISTS #{TableName.new(index.name)}}
|
182
|
+
connection.execute(sql)
|
183
|
+
end
|
184
|
+
|
112
185
|
def translate_options options
|
113
186
|
options = options.dup
|
114
187
|
if options[:attribute]
|
115
188
|
options[:order] = options.delete(:attribute)
|
116
189
|
if direction = options.delete(:direction)
|
117
|
-
direction = direction.to_s[
|
190
|
+
direction = direction.to_s[/(asc|desc)/i]
|
118
191
|
options[:order] = { options[:order] => direction }
|
119
192
|
end
|
120
193
|
end
|
@@ -132,6 +205,7 @@ module Perpetuity
|
|
132
205
|
def drop_table name
|
133
206
|
connection.execute "DROP TABLE IF EXISTS #{table_name(name)}"
|
134
207
|
end
|
208
|
+
alias :drop_collection :drop_table
|
135
209
|
|
136
210
|
def create_table name, attributes
|
137
211
|
connection.execute Table.new(name, attributes).create_table_sql
|
@@ -153,10 +227,26 @@ module Perpetuity
|
|
153
227
|
Serializer.new(mapper).serialize_changes object, original
|
154
228
|
end
|
155
229
|
|
230
|
+
def cast_id id, id_attribute
|
231
|
+
return id if id_attribute.nil?
|
232
|
+
|
233
|
+
if [Bignum, Fixnum, Integer].include? id_attribute.type
|
234
|
+
id.to_i
|
235
|
+
else
|
236
|
+
id
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
156
240
|
def unserialize data, mapper
|
157
241
|
Serializer.new(mapper).unserialize data
|
158
242
|
end
|
159
243
|
|
244
|
+
def increment klass, id, attribute, count=1
|
245
|
+
table = TableName.new(klass)
|
246
|
+
sql = %Q{UPDATE #{table} SET #{attribute} = #{attribute} + #{count} WHERE id = #{SQLValue.new(id)} RETURNING #{attribute}}
|
247
|
+
connection.execute(sql).to_a
|
248
|
+
end
|
249
|
+
|
160
250
|
def create_table_with_attributes klass, attributes
|
161
251
|
table_attributes = attributes.map do |attr|
|
162
252
|
name = attr.name
|
@@ -166,5 +256,13 @@ module Perpetuity
|
|
166
256
|
end
|
167
257
|
create_table klass.to_s, table_attributes
|
168
258
|
end
|
259
|
+
|
260
|
+
def add_column table_name, column_name, attributes
|
261
|
+
attr = attributes.detect { |a| a.name.to_s == column_name.to_s }
|
262
|
+
column = Table::Attribute.new(attr.name, attr.type, attr.options)
|
263
|
+
|
264
|
+
sql = %Q(ALTER TABLE "#{table_name}" ADD #{column.sql_declaration})
|
265
|
+
connection.execute sql
|
266
|
+
end
|
169
267
|
end
|
170
268
|
end
|
data/perpetuity-postgres.gemspec
CHANGED
@@ -21,6 +21,6 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.3"
|
22
22
|
spec.add_development_dependency "rspec"
|
23
23
|
spec.add_development_dependency "rake"
|
24
|
-
spec.add_runtime_dependency "perpetuity", "~> 1.0.0.
|
24
|
+
spec.add_runtime_dependency "perpetuity", "~> 1.0.0.beta2"
|
25
25
|
spec.add_runtime_dependency "pg"
|
26
26
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'perpetuity/postgres/index_collection'
|
2
|
+
require 'perpetuity/attribute'
|
3
|
+
|
4
|
+
module Perpetuity
|
5
|
+
class Postgres
|
6
|
+
describe IndexCollection do
|
7
|
+
let(:indexes) { IndexCollection.new(Object) }
|
8
|
+
|
9
|
+
it 'knows which table it is indexing' do
|
10
|
+
indexes.table.should == 'Object'
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'iterates over its indexes' do
|
14
|
+
indexes << 1
|
15
|
+
indexes.map { |index| index.to_s }.should include '1'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'converts to an array' do
|
19
|
+
indexes.to_ary.should == []
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'removes indexes based on a block' do
|
23
|
+
indexes << double('Index', name: 'lol')
|
24
|
+
indexes.reject! { |index| index.name == 'lol' }
|
25
|
+
indexes.map(&:name).should_not include 'lol'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'perpetuity/postgres/index'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe Index do
|
6
|
+
it 'can be generated from SQL results' do
|
7
|
+
index_hash = {
|
8
|
+
"name"=>"Object_id_name_idx",
|
9
|
+
"attributes"=>"{id,name}",
|
10
|
+
"unique"=>"t",
|
11
|
+
"active"=>"t"
|
12
|
+
}
|
13
|
+
|
14
|
+
index = Index.from_sql(index_hash)
|
15
|
+
index.attribute_names.should == ['id', 'name']
|
16
|
+
index.name.should == 'Object_id_name_idx'
|
17
|
+
index.table.should == 'Object'
|
18
|
+
index.should be_unique
|
19
|
+
index.should be_active
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'sets itself as active' do
|
23
|
+
index = Index.new(attributes: double('Attributes'),
|
24
|
+
name: 'Table',
|
25
|
+
unique: false)
|
26
|
+
|
27
|
+
index.should_not be_active
|
28
|
+
index.activate!
|
29
|
+
index.should be_active
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'equality' do
|
33
|
+
let(:attributes) { [:id, :name] }
|
34
|
+
let(:name) { 'Object' }
|
35
|
+
let(:unique) { true }
|
36
|
+
let(:index) do
|
37
|
+
Index.new(attributes: [:id, :name],
|
38
|
+
name: name,
|
39
|
+
unique: true)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'is equal to an index with identical state' do
|
43
|
+
new_index = index.dup
|
44
|
+
new_index.should == index
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'is not equal to an index with different attributes' do
|
48
|
+
new_index = Index.new(attributes: [:lol],
|
49
|
+
name: name,
|
50
|
+
unique: false)
|
51
|
+
|
52
|
+
new_index.should_not == index
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'is equal to an index with stringified attributes' do
|
56
|
+
new_index = Index.new(attributes: attributes.map(&:to_s),
|
57
|
+
name: name,
|
58
|
+
unique: unique)
|
59
|
+
new_index.should == index
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'is not equal to an index with another name' do
|
63
|
+
new_index = Index.new(attributes: attributes,
|
64
|
+
name: 'NotObject',
|
65
|
+
unique: unique)
|
66
|
+
|
67
|
+
new_index.should_not == index
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'is not equal to an index with opposite uniqueness' do
|
71
|
+
new_index = Index.new(attributes: attributes,
|
72
|
+
name: name,
|
73
|
+
unique: !unique)
|
74
|
+
new_index.should_not == index
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'is not equal to things that are not indexes' do
|
78
|
+
index.should_not == 'lol'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -19,6 +19,10 @@ module Perpetuity
|
|
19
19
|
JSONHash.new({a: true, b: false}).to_s.should == %q('{"a":true,"b":false}')
|
20
20
|
end
|
21
21
|
|
22
|
+
it 'serializes nil values' do
|
23
|
+
JSONHash.new({a: nil}).to_s.should == %q('{"a":null}')
|
24
|
+
end
|
25
|
+
|
22
26
|
it 'does not surround the an inner serialized value with quotes' do
|
23
27
|
JSONHash.new({a: 1}, :inner).to_s.should == %q[{"a":1}]
|
24
28
|
end
|
@@ -6,6 +6,15 @@ module Perpetuity
|
|
6
6
|
it 'serializes into a JSON string value' do
|
7
7
|
JSONStringValue.new('Jamie').to_s.should == '"Jamie"'
|
8
8
|
end
|
9
|
+
|
10
|
+
it 'converts symbols into strings' do
|
11
|
+
JSONStringValue.new(:foo).to_s.should == '"foo"'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'escapes quotes' do
|
15
|
+
JSONStringValue.new('Anakin "Darth Vader" Skywalker').to_s.should ==
|
16
|
+
'"Anakin \\"Darth Vader\\" Skywalker"'
|
17
|
+
end
|
9
18
|
end
|
10
19
|
end
|
11
20
|
end
|
@@ -40,6 +40,14 @@ module Perpetuity
|
|
40
40
|
(attribute.in [1, 2, 3]).should be_a QueryExpression
|
41
41
|
end
|
42
42
|
|
43
|
+
it 'checks for existence' do
|
44
|
+
(attribute.any?).to_db.should == 'json_array_length(attribute_name) > 0'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'checks for no existence' do
|
48
|
+
(attribute.none?).to_db.should == 'json_array_length(attribute_name) = 0'
|
49
|
+
end
|
50
|
+
|
43
51
|
it 'checks for nil' do
|
44
52
|
attribute.nil?.should be_a QueryExpression
|
45
53
|
end
|
@@ -29,13 +29,18 @@ module Perpetuity
|
|
29
29
|
[ SerializedData.new(columns, ["'Jamie'", 31]),
|
30
30
|
SerializedData.new(columns, ["'Jessica'", 23]),
|
31
31
|
SerializedData.new(columns, ["'Kevin'", 22]),
|
32
|
-
]
|
32
|
+
]
|
33
33
|
end
|
34
|
-
let(:serialized_multiple) { serialized + SerializedData.new(columns, ["'Jessica'", 23]) +
|
35
|
-
SerializedData.new(columns, ["'Kevin'",22])}
|
36
34
|
|
37
35
|
it 'matches a SQL string' do
|
38
|
-
serialized_multiple.to_s.should ==
|
36
|
+
serialized_multiple.reduce(:+).to_s.should ==
|
37
|
+
"(name,age) VALUES ('Jamie',31),('Jessica',23),('Kevin',22)"
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'does not modify the first value' do
|
41
|
+
jamie_values = serialized_multiple.first.values.dup
|
42
|
+
serialized_multiple.reduce(:+)
|
43
|
+
serialized_multiple.first.values.should == jamie_values
|
39
44
|
end
|
40
45
|
end
|
41
46
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'perpetuity/postgres/sql_function'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class Postgres
|
5
|
+
describe SQLFunction do
|
6
|
+
it 'converts to a SQL function call' do
|
7
|
+
function = SQLFunction.new('json_array_length', :comments)
|
8
|
+
function.to_s.should == 'json_array_length(comments)'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'takes multiple arguments' do
|
12
|
+
function = SQLFunction.new('compare', :a, :b)
|
13
|
+
function.to_s.should == 'compare(a,b)'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -55,9 +55,21 @@ module Perpetuity
|
|
55
55
|
|
56
56
|
describe 'integers' do
|
57
57
|
let(:page_views) { Attribute.new('page_views', Integer, default: 0) }
|
58
|
+
let(:public_key) { Attribute.new('public_key', Bignum) }
|
58
59
|
|
59
60
|
it 'generates the proper SQL' do
|
60
|
-
page_views.sql_declaration.should == 'page_views
|
61
|
+
page_views.sql_declaration.should == 'page_views BIGINT DEFAULT 0'
|
62
|
+
public_key.sql_declaration.should == 'public_key NUMERIC'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'floating-point numbers' do
|
67
|
+
let(:pi) { Attribute.new('pi', Float) }
|
68
|
+
let(:precise_pi) { Attribute.new('precise_pi', BigDecimal) }
|
69
|
+
|
70
|
+
it 'generates the proper SQL' do
|
71
|
+
pi.sql_declaration.should == 'pi FLOAT'
|
72
|
+
precise_pi.sql_declaration.should == 'precise_pi NUMERIC'
|
61
73
|
end
|
62
74
|
end
|
63
75
|
|
@@ -7,6 +7,10 @@ module Perpetuity
|
|
7
7
|
TableName.new('Person').to_s.should == '"Person"'
|
8
8
|
end
|
9
9
|
|
10
|
+
it 'cannot contain double quotes' do
|
11
|
+
expect { TableName.new('Foo "Bar"') }.to raise_error InvalidTableName
|
12
|
+
end
|
13
|
+
|
10
14
|
it 'compares equally to its string representation' do
|
11
15
|
TableName.new('Person').should == 'Person'
|
12
16
|
end
|
@@ -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 VARCHAR(40), body TEXT, author JSON, published_at TIMESTAMPTZ, views
|
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 BIGINT)'
|
30
30
|
end
|
31
31
|
|
32
32
|
describe 'id column' do
|
@@ -59,6 +59,27 @@ module Perpetuity
|
|
59
59
|
postgres.should_not have_table 'Article'
|
60
60
|
end
|
61
61
|
|
62
|
+
it 'adds columns automatically if they are not there' do
|
63
|
+
attributes = AttributeSet.new
|
64
|
+
attributes << Attribute.new('title', String, max_length: 40)
|
65
|
+
attributes << Attribute.new('body', String)
|
66
|
+
attributes << Attribute.new('author', Object)
|
67
|
+
|
68
|
+
postgres.drop_table 'Article'
|
69
|
+
postgres.create_table 'Article', attributes.map { |attr|
|
70
|
+
Postgres::Table::Attribute.new(attr.name, attr.type, attr.options)
|
71
|
+
}
|
72
|
+
|
73
|
+
attributes << Attribute.new('timestamp', Time)
|
74
|
+
data = [Postgres::SerializedData.new([:title, :timestamp],
|
75
|
+
["'Jamie'", "'2013-1-1'"])]
|
76
|
+
id = postgres.insert('Article', data, attributes).first
|
77
|
+
|
78
|
+
postgres.find('Article', id)['timestamp'].should =~ /2013/
|
79
|
+
|
80
|
+
postgres.drop_table 'Article' # Cleanup
|
81
|
+
end
|
82
|
+
|
62
83
|
it 'converts values into something that works with the DB' do
|
63
84
|
postgres.postgresify("string").should == "'string'"
|
64
85
|
postgres.postgresify(1).should == '1'
|
@@ -66,9 +87,13 @@ module Perpetuity
|
|
66
87
|
end
|
67
88
|
|
68
89
|
describe 'working with data' do
|
69
|
-
let(:attributes) {
|
90
|
+
let(:attributes) { AttributeSet.new }
|
70
91
|
let(:data) { [Postgres::SerializedData.new([:name], ["'Jamie'"])] }
|
71
92
|
|
93
|
+
before do
|
94
|
+
attributes << Attribute.new(:name, String)
|
95
|
+
end
|
96
|
+
|
72
97
|
it 'inserts data and finds by id' do
|
73
98
|
id = postgres.insert('User', data, attributes).first
|
74
99
|
result = postgres.find('User', id)
|
@@ -77,12 +102,23 @@ module Perpetuity
|
|
77
102
|
result['name'].should == 'Jamie'
|
78
103
|
end
|
79
104
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
105
|
+
describe 'returning ids' do
|
106
|
+
it 'returns the ids of all items saved' do
|
107
|
+
data << Postgres::SerializedData.new([:name], ["'Jessica'"]) <<
|
108
|
+
Postgres::SerializedData.new([:name], ["'Kevin'"])
|
109
|
+
ids = postgres.insert('User', data, attributes)
|
110
|
+
ids.should be_a Array
|
111
|
+
ids.should have(3).items
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'returns numeric ids when numeric ids are specified' do
|
115
|
+
postgres.drop_table 'User'
|
116
|
+
attributes << Attribute.new(:id, Integer)
|
117
|
+
data.first[:id] = 1234
|
118
|
+
ids = postgres.insert 'User', data, attributes
|
119
|
+
ids.first.should == 1234
|
120
|
+
postgres.drop_table 'User'
|
121
|
+
end
|
86
122
|
end
|
87
123
|
|
88
124
|
it 'counts objects' do
|
@@ -123,6 +159,21 @@ module Perpetuity
|
|
123
159
|
id = postgres.insert('User', data, attributes).first
|
124
160
|
expect { postgres.delete id, 'User' }.to change { postgres.count 'User' }.by -1
|
125
161
|
end
|
162
|
+
|
163
|
+
describe 'incrementing/decrementing' do
|
164
|
+
let(:attributes) { AttributeSet.new }
|
165
|
+
let(:data) { [Postgres::SerializedData.new([:n], [1])] }
|
166
|
+
|
167
|
+
before do
|
168
|
+
attributes << Attribute.new(:n, Fixnum)
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'increments a value for a record' do
|
172
|
+
id = postgres.insert('Increment', data, attributes).first
|
173
|
+
postgres.increment 'Increment', id, :n, 10
|
174
|
+
postgres.find('Increment', id)['n'].should == '11'
|
175
|
+
end
|
176
|
+
end
|
126
177
|
end
|
127
178
|
|
128
179
|
describe 'query generation' do
|
@@ -159,5 +210,58 @@ module Perpetuity
|
|
159
210
|
end
|
160
211
|
end
|
161
212
|
end
|
213
|
+
|
214
|
+
describe 'indexes' do
|
215
|
+
let(:title) { Postgres::Table::Attribute.new('title', String) }
|
216
|
+
|
217
|
+
before do
|
218
|
+
postgres.drop_table Object
|
219
|
+
postgres.create_table Object, [title]
|
220
|
+
end
|
221
|
+
|
222
|
+
after do
|
223
|
+
postgres.drop_table Object
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'retrieves the active indexes from the database' do
|
227
|
+
index = postgres.index(Object, Attribute.new(:title, String), unique: true)
|
228
|
+
postgres.activate_index! index
|
229
|
+
|
230
|
+
active_indexes = postgres.active_indexes(Object)
|
231
|
+
index = active_indexes.find { |i| i.attribute_names == ['title'] }
|
232
|
+
index.attribute_names.should == ['title']
|
233
|
+
index.table.should == 'Object'
|
234
|
+
index.should be_unique
|
235
|
+
index.should be_active
|
236
|
+
end
|
237
|
+
|
238
|
+
describe 'adding indexes to the database' do
|
239
|
+
it 'adds an inactive index to the database' do
|
240
|
+
title_attribute = Attribute.new(:title, String)
|
241
|
+
postgres.index Object, title_attribute
|
242
|
+
index = postgres.indexes(Object).find { |i| i.attributes.map(&:name) == [:title] }
|
243
|
+
index.attribute_names.should == ['title']
|
244
|
+
index.table.should == 'Object'
|
245
|
+
index.should_not be_unique
|
246
|
+
index.should_not be_active
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'activates the specified index' do
|
250
|
+
title_attribute = Attribute.new(:title, String)
|
251
|
+
index = postgres.index Object, title_attribute
|
252
|
+
postgres.activate_index! index
|
253
|
+
postgres.active_indexes(Object).map(&:attribute_names).should include ['title']
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
describe 'removing indexes' do
|
258
|
+
it 'removes the specified index' do
|
259
|
+
index = postgres.index(Object, Attribute.new(:title, String))
|
260
|
+
postgres.activate_index! index
|
261
|
+
postgres.remove_index index
|
262
|
+
postgres.active_indexes(Object).map(&:attribute_names).should_not include ['title']
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
162
266
|
end
|
163
267
|
end
|
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.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jamie Gaskins
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-12-
|
11
|
+
date: 2013-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ~>
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 1.0.0.
|
61
|
+
version: 1.0.0.beta2
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ~>
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 1.0.0.
|
68
|
+
version: 1.0.0.beta2
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: pg
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,6 +88,7 @@ extensions: []
|
|
88
88
|
extra_rdoc_files: []
|
89
89
|
files:
|
90
90
|
- .gitignore
|
91
|
+
- CHANGELOG.md
|
91
92
|
- Gemfile
|
92
93
|
- LICENSE.txt
|
93
94
|
- README.md
|
@@ -97,6 +98,8 @@ files:
|
|
97
98
|
- lib/perpetuity/postgres/connection.rb
|
98
99
|
- lib/perpetuity/postgres/connection_pool.rb
|
99
100
|
- lib/perpetuity/postgres/expression.rb
|
101
|
+
- lib/perpetuity/postgres/index.rb
|
102
|
+
- lib/perpetuity/postgres/index_collection.rb
|
100
103
|
- lib/perpetuity/postgres/json_array.rb
|
101
104
|
- lib/perpetuity/postgres/json_hash.rb
|
102
105
|
- lib/perpetuity/postgres/json_string_value.rb
|
@@ -111,6 +114,7 @@ files:
|
|
111
114
|
- lib/perpetuity/postgres/query_union.rb
|
112
115
|
- lib/perpetuity/postgres/serialized_data.rb
|
113
116
|
- lib/perpetuity/postgres/serializer.rb
|
117
|
+
- lib/perpetuity/postgres/sql_function.rb
|
114
118
|
- lib/perpetuity/postgres/sql_select.rb
|
115
119
|
- lib/perpetuity/postgres/sql_update.rb
|
116
120
|
- lib/perpetuity/postgres/sql_value.rb
|
@@ -126,6 +130,8 @@ files:
|
|
126
130
|
- spec/perpetuity/postgres/connection_pool_spec.rb
|
127
131
|
- spec/perpetuity/postgres/connection_spec.rb
|
128
132
|
- spec/perpetuity/postgres/expression_spec.rb
|
133
|
+
- spec/perpetuity/postgres/index_collection_spec.rb
|
134
|
+
- spec/perpetuity/postgres/index_spec.rb
|
129
135
|
- spec/perpetuity/postgres/json_array_spec.rb
|
130
136
|
- spec/perpetuity/postgres/json_hash_spec.rb
|
131
137
|
- spec/perpetuity/postgres/json_string_value_spec.rb
|
@@ -139,6 +145,7 @@ files:
|
|
139
145
|
- spec/perpetuity/postgres/query_union_spec.rb
|
140
146
|
- spec/perpetuity/postgres/serialized_data_spec.rb
|
141
147
|
- spec/perpetuity/postgres/serializer_spec.rb
|
148
|
+
- spec/perpetuity/postgres/sql_function_spec.rb
|
142
149
|
- spec/perpetuity/postgres/sql_select_spec.rb
|
143
150
|
- spec/perpetuity/postgres/sql_update_spec.rb
|
144
151
|
- spec/perpetuity/postgres/sql_value_spec.rb
|
@@ -171,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
171
178
|
version: '0'
|
172
179
|
requirements: []
|
173
180
|
rubyforge_project:
|
174
|
-
rubygems_version: 2.0.
|
181
|
+
rubygems_version: 2.0.3
|
175
182
|
signing_key:
|
176
183
|
specification_version: 4
|
177
184
|
summary: PostgreSQL adapter for Perpetuity
|
@@ -180,6 +187,8 @@ test_files:
|
|
180
187
|
- spec/perpetuity/postgres/connection_pool_spec.rb
|
181
188
|
- spec/perpetuity/postgres/connection_spec.rb
|
182
189
|
- spec/perpetuity/postgres/expression_spec.rb
|
190
|
+
- spec/perpetuity/postgres/index_collection_spec.rb
|
191
|
+
- spec/perpetuity/postgres/index_spec.rb
|
183
192
|
- spec/perpetuity/postgres/json_array_spec.rb
|
184
193
|
- spec/perpetuity/postgres/json_hash_spec.rb
|
185
194
|
- spec/perpetuity/postgres/json_string_value_spec.rb
|
@@ -193,6 +202,7 @@ test_files:
|
|
193
202
|
- spec/perpetuity/postgres/query_union_spec.rb
|
194
203
|
- spec/perpetuity/postgres/serialized_data_spec.rb
|
195
204
|
- spec/perpetuity/postgres/serializer_spec.rb
|
205
|
+
- spec/perpetuity/postgres/sql_function_spec.rb
|
196
206
|
- spec/perpetuity/postgres/sql_select_spec.rb
|
197
207
|
- spec/perpetuity/postgres/sql_update_spec.rb
|
198
208
|
- spec/perpetuity/postgres/sql_value_spec.rb
|