perpetuity-postgres 0.0.1 → 0.0.2
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.
- 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
|