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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +34 -0
- data/Rakefile +1 -0
- data/lib/perpetuity/postgres/boolean_value.rb +17 -0
- data/lib/perpetuity/postgres/connection.rb +78 -0
- data/lib/perpetuity/postgres/connection_pool.rb +39 -0
- data/lib/perpetuity/postgres/expression.rb +21 -0
- data/lib/perpetuity/postgres/json_array.rb +31 -0
- data/lib/perpetuity/postgres/json_hash.rb +55 -0
- data/lib/perpetuity/postgres/json_string_value.rb +17 -0
- data/lib/perpetuity/postgres/negated_query.rb +21 -0
- data/lib/perpetuity/postgres/nil_query.rb +9 -0
- data/lib/perpetuity/postgres/null_value.rb +9 -0
- data/lib/perpetuity/postgres/numeric_value.rb +17 -0
- data/lib/perpetuity/postgres/query.rb +34 -0
- data/lib/perpetuity/postgres/query_attribute.rb +33 -0
- data/lib/perpetuity/postgres/query_expression.rb +90 -0
- data/lib/perpetuity/postgres/query_intersection.rb +26 -0
- data/lib/perpetuity/postgres/query_union.rb +26 -0
- data/lib/perpetuity/postgres/serialized_data.rb +63 -0
- data/lib/perpetuity/postgres/serializer.rb +179 -0
- data/lib/perpetuity/postgres/sql_select.rb +71 -0
- data/lib/perpetuity/postgres/sql_update.rb +28 -0
- data/lib/perpetuity/postgres/sql_value.rb +40 -0
- data/lib/perpetuity/postgres/table/attribute.rb +60 -0
- data/lib/perpetuity/postgres/table.rb +36 -0
- data/lib/perpetuity/postgres/table_name.rb +21 -0
- data/lib/perpetuity/postgres/text_value.rb +14 -0
- data/lib/perpetuity/postgres/timestamp_value.rb +68 -0
- data/lib/perpetuity/postgres/value_with_attribute.rb +19 -0
- data/lib/perpetuity/postgres/version.rb +5 -0
- data/lib/perpetuity/postgres.rb +170 -0
- data/perpetuity-postgres.gemspec +26 -0
- data/spec/perpetuity/postgres/boolean_value_spec.rb +15 -0
- data/spec/perpetuity/postgres/connection_pool_spec.rb +55 -0
- data/spec/perpetuity/postgres/connection_spec.rb +30 -0
- data/spec/perpetuity/postgres/expression_spec.rb +13 -0
- data/spec/perpetuity/postgres/json_array_spec.rb +31 -0
- data/spec/perpetuity/postgres/json_hash_spec.rb +39 -0
- data/spec/perpetuity/postgres/json_string_value_spec.rb +11 -0
- data/spec/perpetuity/postgres/negated_query_spec.rb +39 -0
- data/spec/perpetuity/postgres/null_value_spec.rb +11 -0
- data/spec/perpetuity/postgres/numeric_value_spec.rb +12 -0
- data/spec/perpetuity/postgres/query_attribute_spec.rb +48 -0
- data/spec/perpetuity/postgres/query_expression_spec.rb +110 -0
- data/spec/perpetuity/postgres/query_intersection_spec.rb +23 -0
- data/spec/perpetuity/postgres/query_spec.rb +23 -0
- data/spec/perpetuity/postgres/query_union_spec.rb +23 -0
- data/spec/perpetuity/postgres/serialized_data_spec.rb +69 -0
- data/spec/perpetuity/postgres/serializer_spec.rb +216 -0
- data/spec/perpetuity/postgres/sql_select_spec.rb +51 -0
- data/spec/perpetuity/postgres/sql_update_spec.rb +22 -0
- data/spec/perpetuity/postgres/sql_value_spec.rb +38 -0
- data/spec/perpetuity/postgres/table/attribute_spec.rb +82 -0
- data/spec/perpetuity/postgres/table_name_spec.rb +15 -0
- data/spec/perpetuity/postgres/table_spec.rb +43 -0
- data/spec/perpetuity/postgres/text_value_spec.rb +15 -0
- data/spec/perpetuity/postgres/timestamp_value_spec.rb +28 -0
- data/spec/perpetuity/postgres/value_with_attribute_spec.rb +34 -0
- data/spec/perpetuity/postgres_spec.rb +163 -0
- data/spec/support/test_classes/book.rb +16 -0
- data/spec/support/test_classes/person.rb +11 -0
- 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,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
|