activerecord-postgres-composite-types 0.2.4 → 0.2.5
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 +4 -4
- data/Gemfile +9 -9
- data/Rakefile +13 -13
- data/VERSION +1 -1
- data/activerecord-postgres-composite-types.gemspec +4 -4
- data/lib/activerecord-postgres-composite-types.rb +5 -5
- data/lib/activerecord-postgres-composite-types/active_record.rb +141 -148
- data/lib/activerecord-postgres-composite-types/active_record_3.rb +86 -86
- data/lib/activerecord-postgres-composite-types/active_record_4.rb +90 -90
- data/lib/activerecord-postgres-composite-types/composite_type_parser.rb +44 -93
- data/lib/activerecord-postgres-composite-types/railtie.rb +6 -6
- data/lib/postgres_composite_type.rb +89 -89
- data/test/composite_types.rb +5 -5
- data/test/helper.rb +4 -4
- data/test/internal/db/schema.rb +16 -16
- data/test/test_composite_type_class.rb +52 -36
- data/test/test_nested_types.rb +34 -16
- data/test/test_postgres_composite_types.rb +44 -44
- metadata +3 -3
@@ -1,108 +1,108 @@
|
|
1
1
|
# ActiveRecord 3.X specific extensions.
|
2
2
|
module ActiveRecord
|
3
3
|
|
4
|
-
|
4
|
+
module ConnectionAdapters
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
class PostgreSQLAdapter
|
7
|
+
module OID
|
8
|
+
class CompositeType < Type
|
9
|
+
def initialize(composite_type_class)
|
10
|
+
@composite_type_class = composite_type_class
|
11
|
+
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
# Casts value (which is a String) to an appropriate instance.
|
14
|
+
def type_cast(value)
|
15
|
+
PostgreSQLColumn.string_to_composite_type(@composite_type_class, value)
|
16
|
+
# @composite_type_class.new(value)
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
19
|
+
# Casts a Ruby value to something appropriate for writing to the database.
|
20
|
+
def type_cast_for_write(value)
|
21
|
+
# Cast Hash and Array to composite type klass
|
22
|
+
if value.is_a?(@composite_type_class)
|
23
|
+
value
|
24
|
+
else
|
25
|
+
@composite_type_class.new(value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
31
|
+
class << self
|
32
|
+
def register_oid_type(klass)
|
33
|
+
OID.register_type klass.type.to_s, OID::CompositeType.new(klass)
|
34
|
+
# Dirty. Only this type should be added to type map
|
35
|
+
klass.connection.send(:reload_type_map) if klass.connected?
|
36
|
+
end
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
def add_composite_type_to_map(type)
|
40
|
+
oid_type = OID::NAMES[type.to_s]
|
41
|
+
raise "OID type: '#{type}' not registered" unless oid_type
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
|
43
|
+
result = execute("SELECT oid, typname, typelem, typdelim, typinput FROM pg_type WHERE typname = '#{type}'", 'SCHEMA')
|
44
|
+
raise "Composite type: '#{type}' not defined in PostgreSQL database" if result.empty?
|
45
|
+
row = result[0]
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
unless type_map.key? row['typelem'].to_i
|
48
|
+
type_map[row['oid'].to_i] = vector
|
49
|
+
end
|
50
|
+
end
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
52
|
+
# Cast a +value+ to a type that the database understands.
|
53
|
+
def type_cast_with_composite_types(value, column, array_member = false)
|
54
|
+
case value
|
55
|
+
when PostgresCompositeType
|
56
|
+
PostgreSQLColumn.composite_type_to_string(value, self)
|
57
|
+
when Array, Hash
|
58
|
+
if klass = column.composite_type_class
|
59
|
+
value = klass.new(value)
|
60
|
+
PostgreSQLColumn.composite_type_to_string(value, self)
|
61
|
+
else
|
62
|
+
type_cast_without_composite_types(value, column, array_member)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
type_cast_without_composite_types(value, column, array_member)
|
66
|
+
end
|
67
|
+
end
|
68
68
|
|
69
|
-
|
70
|
-
|
69
|
+
alias_method_chain :type_cast, :composite_types
|
70
|
+
end
|
71
71
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
72
|
+
class PostgreSQLColumn < Column
|
73
|
+
# Casts value (which is a String) to an appropriate instance.
|
74
|
+
def type_cast_with_composite_types(value)
|
75
|
+
if composite_type_klass = PostgreSQLAdapter.composite_type_classes[type]
|
76
|
+
self.class.string_to_composite_type(composite_type_klass, value)
|
77
|
+
else
|
78
|
+
type_cast_without_composite_types(value)
|
79
|
+
end
|
80
|
+
end
|
81
81
|
|
82
|
-
|
82
|
+
alias_method_chain :type_cast, :composite_types
|
83
83
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
84
|
+
def self.composite_type_to_string(object, adapter)
|
85
|
+
quoted_values = object.class.columns.collect do |column|
|
86
|
+
value = object.send(column.name)
|
87
|
+
if String === value
|
88
|
+
if value == "NULL"
|
89
|
+
"\"#{value}\""
|
90
|
+
else
|
91
|
+
quote_and_escape(adapter.type_cast(value, column, true))
|
92
|
+
end
|
93
|
+
else
|
94
|
+
res = adapter.type_cast(value, column, true)
|
95
|
+
if value.class < PostgresCompositeType
|
96
|
+
quote_and_escape(res)
|
97
|
+
else
|
98
|
+
res
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
"(#{quoted_values.join(',')})"
|
103
|
+
end
|
104
104
|
|
105
105
|
|
106
|
-
|
107
|
-
|
106
|
+
end
|
107
|
+
end
|
108
108
|
end
|
@@ -1,95 +1,46 @@
|
|
1
1
|
module ActiveRecord
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
case token
|
47
|
-
when DOUBLE_QUOTE
|
48
|
-
is_quoted = false
|
49
|
-
was_quoted = true
|
50
|
-
when BACKSLASH
|
51
|
-
is_escaping = true
|
52
|
-
else
|
53
|
-
current_item << token
|
54
|
-
end
|
55
|
-
else
|
56
|
-
case token
|
57
|
-
when BACKSLASH
|
58
|
-
is_escaping = true
|
59
|
-
when COMMA
|
60
|
-
add_item_to_array(array, current_item, was_quoted)
|
61
|
-
current_item = ''
|
62
|
-
was_quoted = false
|
63
|
-
when DOUBLE_QUOTE
|
64
|
-
is_quoted = true
|
65
|
-
when BRACKET_OPEN
|
66
|
-
internal_items = []
|
67
|
-
local_index, internal_items = parse_composite_type_contents(internal_items, string, local_index + 1)
|
68
|
-
array.push(internal_items)
|
69
|
-
when BRACKET_CLOSE
|
70
|
-
add_item_to_array(array, current_item, was_quoted)
|
71
|
-
return local_index, array
|
72
|
-
else
|
73
|
-
current_item << token
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
local_index += 1
|
79
|
-
end
|
80
|
-
return local_index, array
|
81
|
-
end
|
82
|
-
|
83
|
-
def add_item_to_array(array, current_item, quoted)
|
84
|
-
return if !quoted && current_item.length == 0
|
85
|
-
|
86
|
-
if !quoted && current_item == 'NULL'
|
87
|
-
array.push nil
|
88
|
-
else
|
89
|
-
array.push current_item
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
2
|
+
module ConnectionAdapters
|
3
|
+
class PostgreSQLColumn
|
4
|
+
class CompositeTypeParser
|
5
|
+
class Splitter < StringScanner
|
6
|
+
OPEN_PAREN = /\(/.freeze
|
7
|
+
CLOSE_PAREN = /\)/.freeze
|
8
|
+
UNQUOTED_RE = /[^,)]*/.freeze
|
9
|
+
SEP_RE = /[,)]/.freeze
|
10
|
+
QUOTE_RE = /"/.freeze
|
11
|
+
QUOTE_SEP_RE = /"[,)]/.freeze
|
12
|
+
QUOTED_RE = /(\\.|""|[^"])*/.freeze
|
13
|
+
REPLACE_RE = /\\(.)|"(")/.freeze
|
14
|
+
REPLACE_WITH = '\1\2'.freeze
|
15
|
+
|
16
|
+
# Split the stored string into an array of strings, handling
|
17
|
+
# the different types of quoting.
|
18
|
+
def parse
|
19
|
+
return @result if @result
|
20
|
+
values = []
|
21
|
+
skip(OPEN_PAREN)
|
22
|
+
if skip(CLOSE_PAREN)
|
23
|
+
values << nil
|
24
|
+
else
|
25
|
+
until eos?
|
26
|
+
if skip(QUOTE_RE)
|
27
|
+
values << scan(QUOTED_RE).gsub(REPLACE_RE, REPLACE_WITH)
|
28
|
+
skip(QUOTE_SEP_RE)
|
29
|
+
else
|
30
|
+
v = scan(UNQUOTED_RE)
|
31
|
+
values << (v unless v.empty?)
|
32
|
+
skip(SEP_RE)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
values
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.parse_data(string)
|
41
|
+
Splitter.new(string).parse
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
95
46
|
end
|
@@ -6,12 +6,12 @@ require 'rails'
|
|
6
6
|
|
7
7
|
class PostgresCompositeTypesRailtie < Rails::Railtie
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
initializer 'activerecord-postgres-composite-types' do
|
10
|
+
ActiveSupport.on_load :active_record do
|
11
|
+
require "activerecord-postgres-composite-types/active_record"
|
12
|
+
require 'postgres_composite_type'
|
13
|
+
end
|
14
|
+
end
|
15
15
|
|
16
16
|
end
|
17
17
|
|
@@ -1,106 +1,106 @@
|
|
1
1
|
require 'activerecord-postgres-composite-types/active_record'
|
2
2
|
|
3
3
|
class PostgresCompositeType
|
4
|
-
|
4
|
+
include Comparable
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
class << self
|
7
|
+
# The PostgreSQL type name as symbol
|
8
|
+
attr_reader :type
|
9
|
+
# Column definition read from db schema
|
10
|
+
attr_reader :columns
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
12
|
+
# Link PostgreSQL type given by the name with this class.
|
13
|
+
# Usage:
|
14
|
+
#
|
15
|
+
# class ComplexType < PostgresCompositeType
|
16
|
+
# register_type :complex
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @param [Symbol] :type the PostgreSQL type name
|
20
|
+
def register_type(type)
|
21
|
+
@type = type.to_sym
|
22
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.register_composite_type_class(self)
|
23
|
+
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
25
|
+
# Be default the ActiveRecord::Base connection is used when reading type definition.
|
26
|
+
# If you want to use connection linked with another class use this method.
|
27
|
+
# Usage
|
28
|
+
#
|
29
|
+
# class ComplexType < PostgresCompositeType
|
30
|
+
# register_type :complex
|
31
|
+
# use_connection_class MyRecordConnectedToDifferentDB
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# @param [Class] :active_record_class the ActiveRecord model class
|
35
|
+
def use_connection_class(active_record_class)
|
36
|
+
@connection_class = active_record_class
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
# :nodoc:
|
40
|
+
def connection
|
41
|
+
(@connection_class || ActiveRecord::Base).connection
|
42
|
+
end
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
# :nodoc:
|
45
|
+
def connected?
|
46
|
+
(@connection_class || ActiveRecord::Base).connected?
|
47
|
+
end
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
49
|
+
# :nodoc:
|
50
|
+
def initialize_column_definition
|
51
|
+
unless @columns
|
52
|
+
@columns = self.connection.columns(type)
|
53
|
+
attr_accessor *@columns.map(&:name)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
57
|
|
58
|
-
|
59
|
-
|
58
|
+
def initialize(value)
|
59
|
+
self.class.initialize_column_definition
|
60
60
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
61
|
+
case value
|
62
|
+
when String
|
63
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLColumn.string_to_composite_type(self.class, value)
|
64
|
+
when Array
|
65
|
+
set_values value
|
66
|
+
when Hash
|
67
|
+
set_attributes value
|
68
|
+
else
|
69
|
+
raise "Unexpected value: #{value.inspect}"
|
70
|
+
end
|
71
|
+
end
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
73
|
+
def <=>(another)
|
74
|
+
return nil if (self.class <=> another.class) == nil
|
75
|
+
self.class.columns.each do |column|
|
76
|
+
v1 = self.send(column.name)
|
77
|
+
v2 = another.send(column.name)
|
78
|
+
return v1 <=> v2 unless v1 == v2
|
79
|
+
end
|
80
|
+
0
|
81
|
+
end
|
82
82
|
|
83
|
-
|
83
|
+
private
|
84
84
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
85
|
+
def set_attributes(values)
|
86
|
+
values.each do |name, value|
|
87
|
+
if Hash === value || Array === value
|
88
|
+
klass = self.class.columns.find(name).first.try(:composite_type_class)
|
89
|
+
value = klass.new(value) if klass
|
90
|
+
end
|
91
|
+
send "#{name}=", value
|
92
|
+
end
|
93
|
+
end
|
94
94
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
95
|
+
def set_values(values)
|
96
|
+
raise "Invalid values count: #{values.size}, expected: #{self.class.columns.size}" if values.size != self.class.columns.size
|
97
|
+
self.class.columns.each.with_index do |column, i|
|
98
|
+
if Hash === values[i] || Array === values[i]
|
99
|
+
klass = column.composite_type_class
|
100
|
+
values[i] = klass.new(values[i]) if klass
|
101
|
+
end
|
102
|
+
send "#{column.name}=", values[i]
|
103
|
+
end
|
104
|
+
end
|
105
105
|
|
106
106
|
end
|