perpetuity-postgres 0.0.1

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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +34 -0
  6. data/Rakefile +1 -0
  7. data/lib/perpetuity/postgres/boolean_value.rb +17 -0
  8. data/lib/perpetuity/postgres/connection.rb +78 -0
  9. data/lib/perpetuity/postgres/connection_pool.rb +39 -0
  10. data/lib/perpetuity/postgres/expression.rb +21 -0
  11. data/lib/perpetuity/postgres/json_array.rb +31 -0
  12. data/lib/perpetuity/postgres/json_hash.rb +55 -0
  13. data/lib/perpetuity/postgres/json_string_value.rb +17 -0
  14. data/lib/perpetuity/postgres/negated_query.rb +21 -0
  15. data/lib/perpetuity/postgres/nil_query.rb +9 -0
  16. data/lib/perpetuity/postgres/null_value.rb +9 -0
  17. data/lib/perpetuity/postgres/numeric_value.rb +17 -0
  18. data/lib/perpetuity/postgres/query.rb +34 -0
  19. data/lib/perpetuity/postgres/query_attribute.rb +33 -0
  20. data/lib/perpetuity/postgres/query_expression.rb +90 -0
  21. data/lib/perpetuity/postgres/query_intersection.rb +26 -0
  22. data/lib/perpetuity/postgres/query_union.rb +26 -0
  23. data/lib/perpetuity/postgres/serialized_data.rb +63 -0
  24. data/lib/perpetuity/postgres/serializer.rb +179 -0
  25. data/lib/perpetuity/postgres/sql_select.rb +71 -0
  26. data/lib/perpetuity/postgres/sql_update.rb +28 -0
  27. data/lib/perpetuity/postgres/sql_value.rb +40 -0
  28. data/lib/perpetuity/postgres/table/attribute.rb +60 -0
  29. data/lib/perpetuity/postgres/table.rb +36 -0
  30. data/lib/perpetuity/postgres/table_name.rb +21 -0
  31. data/lib/perpetuity/postgres/text_value.rb +14 -0
  32. data/lib/perpetuity/postgres/timestamp_value.rb +68 -0
  33. data/lib/perpetuity/postgres/value_with_attribute.rb +19 -0
  34. data/lib/perpetuity/postgres/version.rb +5 -0
  35. data/lib/perpetuity/postgres.rb +170 -0
  36. data/perpetuity-postgres.gemspec +26 -0
  37. data/spec/perpetuity/postgres/boolean_value_spec.rb +15 -0
  38. data/spec/perpetuity/postgres/connection_pool_spec.rb +55 -0
  39. data/spec/perpetuity/postgres/connection_spec.rb +30 -0
  40. data/spec/perpetuity/postgres/expression_spec.rb +13 -0
  41. data/spec/perpetuity/postgres/json_array_spec.rb +31 -0
  42. data/spec/perpetuity/postgres/json_hash_spec.rb +39 -0
  43. data/spec/perpetuity/postgres/json_string_value_spec.rb +11 -0
  44. data/spec/perpetuity/postgres/negated_query_spec.rb +39 -0
  45. data/spec/perpetuity/postgres/null_value_spec.rb +11 -0
  46. data/spec/perpetuity/postgres/numeric_value_spec.rb +12 -0
  47. data/spec/perpetuity/postgres/query_attribute_spec.rb +48 -0
  48. data/spec/perpetuity/postgres/query_expression_spec.rb +110 -0
  49. data/spec/perpetuity/postgres/query_intersection_spec.rb +23 -0
  50. data/spec/perpetuity/postgres/query_spec.rb +23 -0
  51. data/spec/perpetuity/postgres/query_union_spec.rb +23 -0
  52. data/spec/perpetuity/postgres/serialized_data_spec.rb +69 -0
  53. data/spec/perpetuity/postgres/serializer_spec.rb +216 -0
  54. data/spec/perpetuity/postgres/sql_select_spec.rb +51 -0
  55. data/spec/perpetuity/postgres/sql_update_spec.rb +22 -0
  56. data/spec/perpetuity/postgres/sql_value_spec.rb +38 -0
  57. data/spec/perpetuity/postgres/table/attribute_spec.rb +82 -0
  58. data/spec/perpetuity/postgres/table_name_spec.rb +15 -0
  59. data/spec/perpetuity/postgres/table_spec.rb +43 -0
  60. data/spec/perpetuity/postgres/text_value_spec.rb +15 -0
  61. data/spec/perpetuity/postgres/timestamp_value_spec.rb +28 -0
  62. data/spec/perpetuity/postgres/value_with_attribute_spec.rb +34 -0
  63. data/spec/perpetuity/postgres_spec.rb +163 -0
  64. data/spec/support/test_classes/book.rb +16 -0
  65. data/spec/support/test_classes/person.rb +11 -0
  66. metadata +207 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ae601e3900e9e24c2bb98c63eb6d8817b6f06b1b
4
+ data.tar.gz: abf781e103cb6292df35a779388bf8a662ecbce5
5
+ SHA512:
6
+ metadata.gz: 8ff4c1bbd74e6baec79e9db3000644eecc08b5270ba5df5b822eec79792e35dedce562e00d7cdcc2e3f30ccd97e7c5e5745dec67dc95bb869ae500df2dfb5811
7
+ data.tar.gz: 429991a3ec80b543567a581e600aa30918fae13b7d7d75a3bc7ee3ebab2bef9ea64ea5d25a8f4db6c845b4bb4ec6bbc6649039f96fd8a3aa792f52e946d3f25c
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jamie Gaskins
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Perpetuity::Postgres
2
+
3
+ This is the PostgreSQL adapter for [Perpetuity](https://github.com/jgaskins/perpetuity), a Data Mapper-pattern persistence gem. The Data Mapper pattern puts persistence logic into mapper objects and keeps it out of your domain models. This keeps your domain models lightweight and focused.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'perpetuity-postgres'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install perpetuity-postgres
18
+
19
+ ## Usage
20
+
21
+ To configure Perpetuity to use your PostgreSQL database, you can use the same parameters as you would with the MongoDB adapter, except substitute `:postgres` in for `:mongodb`:
22
+
23
+ ```ruby
24
+ require 'perpetuity-postgres' # Unnecessary if using Rails
25
+ Perpetuity.data_source :postgres, 'my_perpetuity_database'
26
+ ```
27
+
28
+ ## Contributing
29
+
30
+ 1. Fork it
31
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
32
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
33
+ 4. Push to the branch (`git push origin my-new-feature`)
34
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class BooleanValue
4
+ def initialize value
5
+ @value = value
6
+ end
7
+
8
+ def to_s
9
+ if @value
10
+ 'TRUE'
11
+ else
12
+ 'FALSE'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,78 @@
1
+ require 'pg'
2
+
3
+ module Perpetuity
4
+ class Postgres
5
+ class Connection
6
+ attr_reader :options
7
+
8
+ def initialize options={}
9
+ @options = sanitize_options(options)
10
+ end
11
+
12
+ def db
13
+ options[:dbname]
14
+ end
15
+
16
+ def pg_connection
17
+ @pg_connection ||= connect
18
+ end
19
+
20
+ def connect
21
+ @pg_connection = PG.connect(options)
22
+ use_uuid_extension
23
+
24
+ @pg_connection
25
+ rescue PG::ConnectionBad => e
26
+ tries ||= 0
27
+ conn = PG.connect
28
+ conn.exec "CREATE DATABASE #{db}"
29
+ conn.close
30
+
31
+ if tries.zero?
32
+ retry
33
+ else
34
+ raise e
35
+ end
36
+ end
37
+
38
+ def active?
39
+ !!@pg_connection
40
+ end
41
+
42
+ def execute sql
43
+ pg_connection.exec sql
44
+ end
45
+
46
+ def tables
47
+ sql = "SELECT table_name FROM information_schema.tables "
48
+ sql << "WHERE table_schema = 'public'"
49
+
50
+ result = execute(sql)
51
+ result.to_a.map { |r| r['table_name'] }
52
+ end
53
+
54
+ def sanitize_options options
55
+ options = options.dup
56
+ db = options.delete(:db)
57
+ username = options.delete(:username)
58
+
59
+ if db
60
+ options[:dbname] = db
61
+ end
62
+
63
+ if username
64
+ options[:user] = username
65
+ end
66
+
67
+ options
68
+ end
69
+
70
+ private
71
+ def use_uuid_extension
72
+ @pg_connection.exec 'CREATE EXTENSION "uuid-ossp"'
73
+ rescue PG::DuplicateObject
74
+ # Ignore. It just means the extension's already been loaded.
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,39 @@
1
+ require 'perpetuity/postgres/connection'
2
+ require 'thread'
3
+
4
+ module Perpetuity
5
+ class Postgres
6
+ class ConnectionPool
7
+ attr_reader :connections, :size
8
+
9
+ def initialize options={}
10
+ @connections = Queue.new
11
+ @size = options.delete(:pool_size) { 5 }
12
+ @size.times do
13
+ connections << Connection.new(options)
14
+ end
15
+ end
16
+
17
+ def lend_connection
18
+ if block_given?
19
+ connection = connections.pop
20
+ result = yield connection
21
+ connections << connection
22
+ result
23
+ end
24
+ end
25
+
26
+ def execute sql
27
+ lend_connection do |connection|
28
+ connection.execute sql
29
+ end
30
+ end
31
+
32
+ def tables
33
+ lend_connection do |connection|
34
+ connection.tables
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class Expression
4
+ def initialize string
5
+ @string = string
6
+ end
7
+
8
+ def to_sql
9
+ @string
10
+ end
11
+
12
+ def to_s
13
+ @string
14
+ end
15
+
16
+ def == other
17
+ other.is_a?(self.class) && to_sql == other.to_sql
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ require 'perpetuity/postgres/numeric_value'
2
+ require 'perpetuity/postgres/json_string_value'
3
+ require 'perpetuity/postgres/json_hash'
4
+
5
+ module Perpetuity
6
+ class Postgres
7
+ class JSONArray
8
+ def initialize value
9
+ @value = value
10
+ end
11
+
12
+ def to_s
13
+ "'[#{serialize_elements}]'"
14
+ end
15
+
16
+ def serialize_elements
17
+ @value.map do |element|
18
+ if element.is_a? Numeric
19
+ NumericValue.new(element)
20
+ elsif element.is_a? String
21
+ JSONStringValue.new(element)
22
+ elsif element.is_a? Hash
23
+ JSONHash.new(element, :inner)
24
+ elsif element.is_a? JSONHash
25
+ JSONHash.new(element.to_hash, :inner)
26
+ end
27
+ end.join(',')
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,55 @@
1
+ require 'perpetuity/postgres/json_string_value'
2
+ require 'perpetuity/postgres/numeric_value'
3
+
4
+ module Perpetuity
5
+ class Postgres
6
+ class JSONHash
7
+ def initialize value, location=:outer
8
+ @value = value
9
+ @location = location
10
+ end
11
+
12
+ def to_s
13
+ if @location == :outer
14
+ "'{#{serialize_elements}}'"
15
+ else
16
+ "{#{serialize_elements}}"
17
+ end
18
+ end
19
+
20
+ def to_hash
21
+ @value
22
+ end
23
+
24
+ def serialize_elements
25
+ @value.map do |key, value|
26
+ string = ''
27
+ string << JSONStringValue.new(key) << ':'
28
+
29
+ string << if value.is_a? Numeric
30
+ NumericValue.new(value)
31
+ elsif value.is_a? String
32
+ JSONStringValue.new(value)
33
+ elsif value.is_a? Hash
34
+ JSONHash.new(value, :inner)
35
+ elsif value.is_a? Class
36
+ JSONStringValue.new(value.to_s)
37
+ elsif [true, false].include? value
38
+ value.to_s
39
+ else
40
+ value
41
+ end
42
+ end.join(',')
43
+ end
44
+
45
+ def to_str
46
+ to_s
47
+ end
48
+
49
+ def == other
50
+ other.is_a? self.class and
51
+ other.to_hash == to_hash
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class JSONStringValue
4
+ def initialize value
5
+ @value = value
6
+ end
7
+
8
+ def to_s
9
+ %Q{"#{@value}"}
10
+ end
11
+
12
+ def to_str
13
+ to_s
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ require 'perpetuity/postgres/query'
2
+
3
+ module Perpetuity
4
+ class Postgres
5
+ class NegatedQuery
6
+ attr_reader :query
7
+
8
+ def initialize &block
9
+ @query = Query.new(&block)
10
+ end
11
+
12
+ def to_db
13
+ "NOT (#{query.to_db})"
14
+ end
15
+
16
+ def to_s
17
+ to_db
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class NilQuery
4
+ def to_db
5
+ 'TRUE'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class NullValue
4
+ def to_s
5
+ 'NULL'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class NumericValue
4
+ def initialize value
5
+ @value = value
6
+ end
7
+
8
+ def to_s
9
+ @value.to_s
10
+ end
11
+
12
+ def to_str
13
+ to_s
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ require 'perpetuity/postgres/query_attribute'
2
+ require 'perpetuity/postgres/nil_query'
3
+
4
+ module Perpetuity
5
+ class Postgres
6
+ class Query
7
+ attr_reader :query, :klass
8
+
9
+ def initialize &block
10
+ if block_given?
11
+ @query = block
12
+ else
13
+ @query = proc { NilQuery.new }
14
+ end
15
+ end
16
+
17
+ def to_db
18
+ query.call(self).to_db
19
+ end
20
+
21
+ def to_str
22
+ to_db
23
+ end
24
+
25
+ def to_s
26
+ to_db
27
+ end
28
+
29
+ def method_missing name
30
+ QueryAttribute.new(name)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ require 'perpetuity/postgres/query_expression'
2
+
3
+ module Perpetuity
4
+ class Postgres
5
+ class QueryAttribute
6
+ attr_reader :name
7
+
8
+ def initialize name
9
+ @name = name
10
+ end
11
+
12
+ %w(!= <= < == > >= =~).each do |comparator|
13
+ eval <<METHOD
14
+ def #{comparator} value
15
+ QueryExpression.new self, :#{comparator}, value
16
+ end
17
+ METHOD
18
+ end
19
+
20
+ def in collection
21
+ QueryExpression.new self, :in, collection
22
+ end
23
+
24
+ def nil?
25
+ QueryExpression.new self, :==, nil
26
+ end
27
+
28
+ def to_s
29
+ name.to_s
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,90 @@
1
+ require 'perpetuity/postgres/query_union'
2
+ require 'perpetuity/postgres/query_intersection'
3
+ require 'perpetuity/postgres/sql_value'
4
+
5
+ module Perpetuity
6
+ class Postgres
7
+ class QueryExpression
8
+ attr_accessor :attribute, :comparator, :value
9
+ def initialize attribute, comparator, value
10
+ @attribute = attribute
11
+ @comparator = comparator
12
+ @value = value
13
+ end
14
+
15
+ def to_db
16
+ if value.nil?
17
+ if comparator == :==
18
+ "#{attribute} IS NULL"
19
+ elsif comparator == :!=
20
+ "#{attribute} IS NOT NULL"
21
+ end
22
+ else
23
+ public_send comparator
24
+ end
25
+ end
26
+
27
+ def sql_value value=self.value
28
+ if value.is_a? String or value.is_a? Symbol
29
+ SQLValue.new(value)
30
+ elsif value.is_a? Regexp
31
+ "'#{value.to_s.sub(/\A\(\?i?-mi?x\:/, '').sub(/\)\z/, '')}'"
32
+ elsif value.is_a? Time
33
+ SQLValue.new(value)
34
+ elsif value.is_a? Array
35
+ value.map! do |element|
36
+ sql_value(element)
37
+ end
38
+ "(#{value.join(',')})"
39
+ else
40
+ value
41
+ end
42
+ end
43
+
44
+ def ==
45
+ "#{attribute} = #{sql_value}"
46
+ end
47
+
48
+ def <
49
+ "#{attribute} < #{sql_value}"
50
+ end
51
+
52
+ def <=
53
+ "#{attribute} <= #{sql_value}"
54
+ end
55
+
56
+ def >
57
+ "#{attribute} > #{sql_value}"
58
+ end
59
+
60
+ def >=
61
+ "#{attribute} >= #{sql_value}"
62
+ end
63
+
64
+ def !=
65
+ "#{attribute} != #{sql_value}"
66
+ end
67
+
68
+ def in
69
+ "#{attribute} IN #{sql_value}"
70
+ end
71
+
72
+ def =~
73
+ regexp_comparator = if value.casefold?
74
+ '~*'
75
+ else
76
+ '~'
77
+ end
78
+ "#{attribute} #{regexp_comparator} #{sql_value}"
79
+ end
80
+
81
+ def | other
82
+ QueryUnion.new(self, other)
83
+ end
84
+
85
+ def & other
86
+ QueryIntersection.new(self, other)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,26 @@
1
+ require 'perpetuity/postgres/query_union'
2
+
3
+ module Perpetuity
4
+ class Postgres
5
+ class QueryIntersection
6
+ attr_reader :lhs, :rhs
7
+
8
+ def initialize lhs, rhs
9
+ @lhs = lhs
10
+ @rhs = rhs
11
+ end
12
+
13
+ def to_db
14
+ "(#{lhs.to_db} AND #{rhs.to_db})"
15
+ end
16
+
17
+ def & other
18
+ QueryIntersection.new(self, other)
19
+ end
20
+
21
+ def | other
22
+ QueryUnion.new(self, other)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ require 'perpetuity/postgres/query_intersection'
2
+
3
+ module Perpetuity
4
+ class Postgres
5
+ class QueryUnion
6
+ attr_reader :lhs, :rhs
7
+
8
+ def initialize lhs, rhs
9
+ @lhs = lhs
10
+ @rhs = rhs
11
+ end
12
+
13
+ def to_db
14
+ "(#{lhs.to_db} OR #{rhs.to_db})"
15
+ end
16
+
17
+ def | other
18
+ QueryUnion.new(self, other)
19
+ end
20
+
21
+ def & other
22
+ QueryIntersection.new(self, other)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,63 @@
1
+ require 'perpetuity/postgres/text_value'
2
+
3
+ module Perpetuity
4
+ class Postgres
5
+ class SerializedData
6
+ include Enumerable
7
+ attr_reader :column_names, :values
8
+ def initialize column_names, *values
9
+ @column_names = column_names.map(&:to_s)
10
+ @values = values
11
+ end
12
+
13
+ def to_s
14
+ value_strings = values.map { |data| "(#{data.join(',')})" }.join(',')
15
+ "(#{column_names.join(',')}) VALUES #{value_strings}"
16
+ end
17
+
18
+ def []= column, value
19
+ value = TextValue.new(value)
20
+ if column_names.include? column
21
+ index = column_names.index(column)
22
+ values.first[index] = value
23
+ else
24
+ column_names << column
25
+ values.first << value
26
+ end
27
+ end
28
+
29
+ def + other
30
+ combined = dup
31
+ combined.values << other.values
32
+
33
+ combined
34
+ end
35
+
36
+ def any?
37
+ values.any?
38
+ end
39
+
40
+ def each
41
+ data = values.first
42
+ column_names.each_with_index { |column, index| yield(column, data[index]) }
43
+ self
44
+ end
45
+
46
+ def - other
47
+ values = self.values.first
48
+ modified_values = values - other.values.first
49
+ modified_columns = column_names.select.with_index { |col, index|
50
+ values[index] != other.values.first[index]
51
+ }
52
+
53
+ SerializedData.new(modified_columns, modified_values)
54
+ end
55
+
56
+ def == other
57
+ other.is_a? SerializedData and
58
+ other.column_names == column_names and
59
+ other.values == values
60
+ end
61
+ end
62
+ end
63
+ end