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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae601e3900e9e24c2bb98c63eb6d8817b6f06b1b
4
- data.tar.gz: abf781e103cb6292df35a779388bf8a662ecbce5
3
+ metadata.gz: 45cd2bfa1345abc08e97eda5e929c3ef1c4b7c23
4
+ data.tar.gz: 2cab12a23a5f25a0f114b76494f283793f7213be
5
5
  SHA512:
6
- metadata.gz: 8ff4c1bbd74e6baec79e9db3000644eecc08b5270ba5df5b822eec79792e35dedce562e00d7cdcc2e3f30ccd97e7c5e5745dec67dc95bb869ae500df2dfb5811
7
- data.tar.gz: 429991a3ec80b543567a581e600aa30918fae13b7d7d75a3bc7ee3ebab2bef9ea64ea5d25a8f4db6c845b4bb4ec6bbc6649039f96fd8a3aa792f52e946d3f25c
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
- conn = PG.connect
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
- @pg_connection.exec 'CREATE EXTENSION "uuid-ossp"'
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
@@ -36,6 +36,8 @@ module Perpetuity
36
36
  JSONStringValue.new(value.to_s)
37
37
  elsif [true, false].include? value
38
38
  value.to_s
39
+ elsif value.nil?
40
+ 'null'
39
41
  else
40
42
  value
41
43
  end
@@ -2,7 +2,7 @@ module Perpetuity
2
2
  class Postgres
3
3
  class JSONStringValue
4
4
  def initialize value
5
- @value = value
5
+ @value = value.to_s.gsub('"') { '\\"' }
6
6
  end
7
7
 
8
8
  def to_s
@@ -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
@@ -27,7 +27,7 @@ module Perpetuity
27
27
  end
28
28
 
29
29
  def + other
30
- combined = dup
30
+ combined = self.class.new(column_names.dup, *(values.dup))
31
31
  combined.values << other.values
32
32
 
33
33
  combined
@@ -59,7 +59,7 @@ module Perpetuity
59
59
  value = unserialize_foreign_object value
60
60
  end
61
61
  if attribute
62
- if attribute.type == Integer
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
@@ -0,0 +1,15 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class SQLFunction
4
+ attr_reader :name, :args
5
+ def initialize name, *args
6
+ @name = name
7
+ @args = args
8
+ end
9
+
10
+ def to_s
11
+ "#{name}(#{args.join(',')})"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -22,8 +22,12 @@ module Perpetuity
22
22
  else
23
23
  'TEXT'
24
24
  end
25
- elsif type == Integer
26
- 'INTEGER'
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
@@ -1,5 +1,5 @@
1
1
  module Perpetuity
2
2
  class Postgres
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
@@ -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[/\w{1,2}sc/i]
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
@@ -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.beta"
24
+ spec.add_runtime_dependency "perpetuity", "~> 1.0.0.beta2"
25
25
  spec.add_runtime_dependency "pg"
26
26
  end
@@ -15,6 +15,7 @@ module Perpetuity
15
15
 
16
16
  it 'is only activated when it is used' do
17
17
  connection.should_not be_active
18
+ PG.stub(connect: true)
18
19
  connection.connect
19
20
  connection.should be_active
20
21
  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
- ].reduce(:+)
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 == "(name,age) VALUES ('Jamie',31),('Jessica',23),('Kevin',22)"
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 INTEGER DEFAULT 0'
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 INTEGER)'
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) { [Attribute.new(:name, String)] }
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
- it 'returns the ids of all items saved' do
81
- self.data << Postgres::SerializedData.new([:name], ["'Jessica'"]) <<
82
- Postgres::SerializedData.new([:name], ["'Kevin'"])
83
- ids = postgres.insert('User', data, attributes)
84
- ids.should be_a Array
85
- ids.should have(3).items
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.1
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-15 00:00:00.000000000 Z
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.beta
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.beta
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.6
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