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
@@ -0,0 +1,179 @@
1
+ require 'perpetuity/postgres/sql_value'
2
+ require 'perpetuity/postgres/serialized_data'
3
+ require 'perpetuity/postgres/value_with_attribute'
4
+ require 'perpetuity/postgres/json_array'
5
+ require 'perpetuity/postgres/json_hash'
6
+ require 'perpetuity/data_injectable'
7
+ require 'json'
8
+
9
+ module Perpetuity
10
+ class Postgres
11
+ class Serializer
12
+ include DataInjectable
13
+
14
+ SERIALIZABLE_CLASSES = [Fixnum, Float, String, Time, TrueClass, FalseClass, NilClass]
15
+ attr_reader :mapper, :mapper_registry
16
+
17
+ def initialize mapper
18
+ @mapper = mapper
19
+ @mapper_registry = mapper.mapper_registry if mapper
20
+ end
21
+
22
+ def serialize object
23
+ attrs = mapper.attribute_set.map do |attribute|
24
+ attr_name = attribute.name.to_s
25
+ value = ValueWithAttribute.new(attribute_for(object, attr_name), attribute)
26
+ serialize_attribute(value)
27
+ end
28
+ column_names = mapper.attributes
29
+
30
+ SerializedData.new(column_names, attrs)
31
+ end
32
+
33
+ def unserialize data
34
+ if data.is_a? Array
35
+ data.map do |datum|
36
+ object = mapper.mapped_class.allocate
37
+ datum.each do |attribute_name, value|
38
+ attribute = mapper.attribute_set[attribute_name.to_sym]
39
+ deserialized_value = unserialize_attribute(attribute, value)
40
+ inject_attribute object, attribute_name, deserialized_value
41
+ end
42
+
43
+ object
44
+ end
45
+ else
46
+ unserialize([data]).first
47
+ end
48
+ end
49
+
50
+ def unserialize_attribute attribute, value
51
+ if value
52
+ if possible_json_value?(value)
53
+ value = JSON.parse(value) rescue value
54
+ if value.is_a? Array
55
+ value = value.map { |v| unserialize_attribute(attribute, v) }
56
+ end
57
+ end
58
+ if foreign_object? value
59
+ value = unserialize_foreign_object value
60
+ end
61
+ if attribute
62
+ if attribute.type == Integer
63
+ value = value.to_i
64
+ elsif attribute.type == Time
65
+ value = TimestampValue.from_sql(value).to_time
66
+ end
67
+ end
68
+
69
+ value
70
+ end
71
+ end
72
+
73
+ def possible_json_value? value
74
+ value.is_a?(String) && %w([ {).include?(value[0])
75
+ end
76
+
77
+ def foreign_object? value
78
+ value.is_a?(Hash) && value.has_key?('__metadata__')
79
+ end
80
+
81
+ def unserialize_foreign_object data
82
+ metadata = data.delete('__metadata__')
83
+ klass = Object.const_get(metadata['class'])
84
+ if metadata.has_key? 'id'
85
+ id = metadata['id']
86
+ return unserialize_reference(klass, id)
87
+ end
88
+
89
+ serializer = serializer_for(klass)
90
+ serializer.unserialize(data)
91
+ end
92
+
93
+ def unserialize_reference klass, id
94
+ Reference.new(klass, id)
95
+ end
96
+
97
+ def serializer_for klass
98
+ Serializer.new(mapper_registry[klass])
99
+ end
100
+
101
+ def attribute_for object, attr_name
102
+ object.instance_variable_get("@#{attr_name}")
103
+ end
104
+
105
+ def serialize_attribute object
106
+ value = object.value rescue object
107
+
108
+ if SERIALIZABLE_CLASSES.include? value.class
109
+ SQLValue.new(value)
110
+ elsif value.is_a? Array
111
+ serialize_array(object)
112
+ elsif !object.embedded?
113
+ serialize_reference(value)
114
+ elsif object.embedded?
115
+ serialize_with_foreign_mapper(value)
116
+ end.to_s
117
+ end
118
+
119
+ def serialize_array object
120
+ value = object.value rescue object
121
+ array = value.map do |item|
122
+ if SERIALIZABLE_CLASSES.include? item.class
123
+ item
124
+ elsif object.embedded?
125
+ serialize_with_foreign_mapper item
126
+ else
127
+ serialize_reference item
128
+ end
129
+ end
130
+
131
+ JSONArray.new(array)
132
+ end
133
+
134
+ def serialize_to_hash value
135
+ Hash[
136
+ mapper.attribute_set.map do |attribute|
137
+ attr_name = attribute.name
138
+ attr_value = attribute_for(value, attr_name)
139
+ [attr_name, attr_value]
140
+ end
141
+ ]
142
+ end
143
+
144
+ def serialize_reference value
145
+ klass = if value.is_a? Reference
146
+ value.klass
147
+ else
148
+ value.class
149
+ end
150
+
151
+ unless mapper.persisted? value
152
+ mapper_registry[value.class].insert value
153
+ end
154
+
155
+ json = {
156
+ __metadata__: {
157
+ class: klass,
158
+ id: mapper.id_for(value)
159
+ }
160
+ }
161
+
162
+ JSONHash.new(json)
163
+ end
164
+
165
+ def serialize_with_foreign_mapper value
166
+ mapper = mapper_registry[value.class]
167
+ serializer = Serializer.new(mapper)
168
+ attr = serializer.serialize_to_hash(value)
169
+ attr.merge!('__metadata__' => { 'class' => value.class })
170
+
171
+ JSONHash.new(attr)
172
+ end
173
+
174
+ def serialize_changes modified, original
175
+ serialize(modified) - serialize(original)
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,71 @@
1
+ require 'perpetuity/postgres/table_name'
2
+
3
+ module Perpetuity
4
+ class Postgres
5
+ class SQLSelect
6
+ attr_reader :selection, :table, :where, :group_by, :order, :limit, :offset
7
+
8
+ def initialize *args
9
+ @selection = if args.one?
10
+ '*'
11
+ else
12
+ args.shift
13
+ end
14
+ options = args.first
15
+ @table = options.fetch(:from)
16
+ @where = options[:where]
17
+ @group_by = options[:group]
18
+ @order = options[:order]
19
+ @limit = options[:limit]
20
+ @offset = options[:offset]
21
+ end
22
+
23
+ def to_s
24
+ "SELECT #{selection} FROM #{TableName.new(table)}" << where_clause.to_s <<
25
+ group_by_clause.to_s <<
26
+ order_clause.to_s <<
27
+ limit_clause.to_s <<
28
+ offset_clause.to_s
29
+ end
30
+
31
+ def where_clause
32
+ if where
33
+ " WHERE #{where}"
34
+ end
35
+ end
36
+
37
+ def group_by_clause
38
+ if group_by
39
+ " GROUP BY #{group_by}"
40
+ end
41
+ end
42
+
43
+ def order_clause
44
+ order = Array(self.order)
45
+ order.map! do |(attribute, direction)|
46
+ if direction
47
+ "#{attribute} #{direction.to_s.upcase}"
48
+ else
49
+ attribute
50
+ end
51
+ end
52
+
53
+ unless order.empty?
54
+ " ORDER BY #{order.join(',')}"
55
+ end
56
+ end
57
+
58
+ def limit_clause
59
+ if limit
60
+ " LIMIT #{limit}"
61
+ end
62
+ end
63
+
64
+ def offset_clause
65
+ if offset
66
+ " OFFSET #{offset}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ require 'perpetuity/postgres/table_name'
2
+ require 'perpetuity/postgres/sql_value'
3
+
4
+ module Perpetuity
5
+ class Postgres
6
+ class SQLUpdate
7
+ attr_reader :klass, :id, :attributes
8
+
9
+ def initialize klass, id, attributes
10
+ @class = klass
11
+ @id = id
12
+ @attributes = attributes
13
+ end
14
+
15
+ def to_s
16
+ sql = "UPDATE #{TableName.new(@class)}"
17
+ if attributes.any?
18
+ sql << " SET "
19
+ sql << attributes.map do |name, value|
20
+ value = SQLValue.new(value) if attributes.is_a? Hash
21
+ "#{name} = #{value}"
22
+ end.join(',')
23
+ end
24
+ sql << " WHERE id = #{SQLValue.new(id)}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ require 'perpetuity/postgres/text_value'
2
+ require 'perpetuity/postgres/timestamp_value'
3
+ require 'perpetuity/postgres/numeric_value'
4
+ require 'perpetuity/postgres/null_value'
5
+ require 'perpetuity/postgres/boolean_value'
6
+
7
+ module Perpetuity
8
+ class Postgres
9
+ class SQLValue
10
+ attr_reader :value
11
+
12
+ def initialize value
13
+ @value = case value
14
+ when String, Symbol
15
+ TextValue.new(value)
16
+ when Time
17
+ TimestampValue.new(value)
18
+ when Fixnum, Float
19
+ NumericValue.new(value)
20
+ when nil
21
+ NullValue.new
22
+ when true, false
23
+ BooleanValue.new(value)
24
+ end.to_s
25
+ end
26
+
27
+ def to_s
28
+ value
29
+ end
30
+
31
+ def == other
32
+ if other.is_a? String
33
+ value == other
34
+ else
35
+ value == other.value
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,60 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class Table
4
+ class Attribute
5
+ attr_reader :name, :type, :max_length
6
+
7
+ NoDefaultValue = Module.new
8
+ UUID = Module.new
9
+
10
+ def initialize name, type, options={}
11
+ @name = name
12
+ @type = type
13
+ @max_length = options[:max_length]
14
+ @primary_key = options.fetch(:primary_key) { false }
15
+ @default = options.fetch(:default) { NoDefaultValue }
16
+ end
17
+
18
+ def sql_type
19
+ if type == String
20
+ if max_length
21
+ "VARCHAR(#{max_length})"
22
+ else
23
+ 'TEXT'
24
+ end
25
+ elsif type == Integer
26
+ 'INTEGER'
27
+ elsif type == UUID
28
+ 'UUID'
29
+ elsif type == Time
30
+ 'TIMESTAMPTZ'
31
+ else
32
+ 'JSON'
33
+ end
34
+ end
35
+
36
+ def sql_declaration
37
+ if self.default.is_a? String
38
+ default = "'#{self.default}'"
39
+ else
40
+ default = self.default
41
+ end
42
+
43
+ sql = "#{name} #{sql_type}"
44
+ sql << ' PRIMARY KEY' if primary_key?
45
+ sql << " DEFAULT #{default}" unless self.default == NoDefaultValue
46
+
47
+ sql
48
+ end
49
+
50
+ def primary_key?
51
+ !!@primary_key
52
+ end
53
+
54
+ def default
55
+ @default
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,36 @@
1
+ require 'perpetuity/postgres/table/attribute'
2
+ require 'perpetuity/postgres/expression'
3
+ require 'perpetuity/postgres/table_name'
4
+
5
+ module Perpetuity
6
+ class Postgres
7
+ class Table
8
+ attr_reader :name, :attributes
9
+ def initialize name, attributes
10
+ @name = TableName.new(name)
11
+ @attributes = attributes.to_a
12
+
13
+ generate_id_attribute unless has_id_attribute?
14
+ end
15
+
16
+ def create_table_sql
17
+ sql = "CREATE TABLE IF NOT EXISTS #{name} ("
18
+ sql << attributes.map(&:sql_declaration).join(', ')
19
+ sql << ')'
20
+ end
21
+
22
+ def has_id_attribute?
23
+ attributes.any? { |attr| attr.name.to_s == 'id' }
24
+ end
25
+
26
+ def generate_id_attribute
27
+ id = Attribute.new('id', Attribute::UUID, primary_key: true, default: Expression.new('uuid_generate_v4()'))
28
+ attributes.unshift id
29
+ end
30
+
31
+ def to_s
32
+ name.to_s
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class TableName
4
+ def initialize name
5
+ @name = name.to_s
6
+ end
7
+
8
+ def to_s
9
+ @name.to_s.inspect
10
+ end
11
+
12
+ def == other
13
+ if other.is_a? String
14
+ other == @name
15
+ else
16
+ to_s == other.to_s
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class TextValue
4
+ def initialize value
5
+ @value = value.to_s.gsub("'") { "''" }
6
+ end
7
+
8
+ def to_s
9
+ "'#{@value}'"
10
+ end
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,68 @@
1
+ require 'perpetuity/postgres/text_value'
2
+
3
+ module Perpetuity
4
+ class Postgres
5
+ class TimestampValue
6
+ attr_reader :time
7
+ def initialize time
8
+ @time = time
9
+ end
10
+
11
+ def self.from_sql sql_value
12
+ date, time = sql_value.split(/ /)
13
+ year, month, day = date.split(/-/)
14
+ hour, minute, seconds_with_offset = time.split(/:/)
15
+ second = seconds_with_offset[/\d+\.\d+/].to_f
16
+ offset = seconds_with_offset[/(\+|\-)\d+/] + ':00'
17
+
18
+ new Time.new(year, month, day, hour, minute, second, offset)
19
+ end
20
+
21
+ def to_time
22
+ time
23
+ end
24
+
25
+ def value
26
+ time
27
+ end
28
+
29
+ def year
30
+ time.year
31
+ end
32
+
33
+ def month
34
+ zero_pad(time.month)
35
+ end
36
+
37
+ def day
38
+ zero_pad(time.day)
39
+ end
40
+
41
+ def hour
42
+ zero_pad(time.hour)
43
+ end
44
+
45
+ def minute
46
+ zero_pad(time.min)
47
+ end
48
+
49
+ def second
50
+ '%02d.%06d' % [time.sec, time.usec]
51
+ end
52
+
53
+ def offset
54
+ time.strftime('%z')
55
+ end
56
+
57
+ def to_s
58
+ string = TextValue.new("#{year}-#{month}-#{day} #{hour}:#{minute}:#{second}#{offset}").to_s
59
+ "#{string}::timestamptz"
60
+ end
61
+
62
+ private
63
+ def zero_pad n
64
+ '%02d' % n
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,19 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ class ValueWithAttribute
4
+ attr_reader :value, :attribute
5
+ def initialize value, attribute
6
+ @value = value
7
+ @attribute = attribute
8
+ end
9
+
10
+ def type
11
+ attribute.type
12
+ end
13
+
14
+ def embedded?
15
+ attribute.embedded?
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ module Perpetuity
2
+ class Postgres
3
+ VERSION = "0.0.1"
4
+ end
5
+ end