activerecord-postgres-composite-types 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|