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