perpetuity-postgres 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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