activerecord-postgres-composite-types 0.2.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/.document +5 -0
- data/.travis.yml +7 -0
- data/Gemfile +18 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +67 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/activerecord-postgres-composite-types.gemspec +92 -0
- data/activerecord-postgres-custom-types.gemspec +91 -0
- data/lib/activerecord-postgres-composite-types.rb +13 -0
- data/lib/activerecord-postgres-composite-types/active_record.rb +166 -0
- data/lib/activerecord-postgres-composite-types/active_record_3.rb +104 -0
- data/lib/activerecord-postgres-composite-types/active_record_4.rb +103 -0
- data/lib/activerecord-postgres-composite-types/composite_type_parser.rb +95 -0
- data/lib/activerecord-postgres-composite-types/postgres_composite_type.rb +75 -0
- data/lib/activerecord-postgres-composite-types/railties.rb +16 -0
- data/test/composite_types.rb +16 -0
- data/test/helper.rb +44 -0
- data/test/internal/config/database.yml +5 -0
- data/test/internal/config/database.yml.travis +4 -0
- data/test/internal/db/schema.rb +19 -0
- data/test/test_composite_type_class.rb +41 -0
- data/test/test_nested_types.rb +18 -0
- data/test/test_postgres_composite_types.rb +48 -0
- metadata +212 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
# ActiveRecord 3.X specific extensions.
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
module ConnectionAdapters
|
5
|
+
|
6
|
+
class PostgreSQLAdapter
|
7
|
+
# Cast a +value+ to a type that the database understands.
|
8
|
+
def type_cast_with_composite_types(value, column)
|
9
|
+
case value
|
10
|
+
when PostgresCompositeType
|
11
|
+
PostgreSQLColumn.composite_type_to_string(value, self)
|
12
|
+
when Array, Hash
|
13
|
+
if klass = column.composite_type_class
|
14
|
+
value = klass.new(value)
|
15
|
+
PostgreSQLColumn.composite_type_to_string(value, self)
|
16
|
+
else
|
17
|
+
type_cast_without_composite_types(value, column)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
type_cast_without_composite_types(value, column)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
alias_method_chain :type_cast, :composite_types
|
25
|
+
end
|
26
|
+
|
27
|
+
class PostgreSQLColumn < Column
|
28
|
+
# Adds composite type for the column.
|
29
|
+
|
30
|
+
# Casts value (which is a String) to an appropriate instance.
|
31
|
+
def type_cast_with_composite_types(value)
|
32
|
+
if composite_type_klass = PostgreSQLAdapter.composite_type_classes[type]
|
33
|
+
self.class.string_to_composite_type(composite_type_klass, value)
|
34
|
+
else
|
35
|
+
type_cast_without_composite_types(value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
alias_method_chain :type_cast, :composite_types
|
40
|
+
|
41
|
+
# quote_and_escape - Rails 4 code
|
42
|
+
|
43
|
+
ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
|
44
|
+
|
45
|
+
def self.quote_and_escape(value)
|
46
|
+
case value
|
47
|
+
when "NULL", Numeric
|
48
|
+
value
|
49
|
+
else
|
50
|
+
value = value.gsub(/\\/, ARRAY_ESCAPE)
|
51
|
+
value.gsub!(/"/, "\\\"")
|
52
|
+
"\"#{value}\""
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.composite_type_to_string(object, adapter)
|
57
|
+
quoted_values = object.class.columns.collect do |column|
|
58
|
+
value = object.send(column.name)
|
59
|
+
if String === value
|
60
|
+
if value == "NULL"
|
61
|
+
"\"#{value}\""
|
62
|
+
else
|
63
|
+
quote_and_escape(adapter.type_cast(value, column))
|
64
|
+
end
|
65
|
+
else
|
66
|
+
adapter.type_cast(value, column)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
"(#{quoted_values.join(',')})"
|
70
|
+
end
|
71
|
+
|
72
|
+
def type_cast_code_with_composite_types(var_name)
|
73
|
+
if composite_type_klass = PostgreSQLAdapter.composite_type_classes[type]
|
74
|
+
"#{self.class}.string_to_composite_type(#{composite_type_klass}, #{var_name})"
|
75
|
+
else
|
76
|
+
type_cast_code_without_composite_types(value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
alias_method_chain :type_cast_code, :composite_types
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module AttributeMethods
|
85
|
+
module CompositeTypes
|
86
|
+
extend ActiveSupport::Concern
|
87
|
+
|
88
|
+
def type_cast_attribute_for_write(column, value)
|
89
|
+
if klass = column.composite_type_class
|
90
|
+
# Cast Hash and Array to composite type klass
|
91
|
+
if value.is_a?(klass)
|
92
|
+
value
|
93
|
+
else
|
94
|
+
klass.new(value)
|
95
|
+
end
|
96
|
+
else
|
97
|
+
super(column, value)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
Base.send :include, AttributeMethods::CompositeTypes
|
104
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# ActiveRecord 3.X specific extensions.
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
module ConnectionAdapters
|
5
|
+
|
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
|
+
|
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
|
+
|
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
|
+
|
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
|
+
|
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
|
+
|
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
|
+
|
47
|
+
unless type_map.key? row['typelem'].to_i
|
48
|
+
type_map[row['oid'].to_i] = vector
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
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
|
+
|
69
|
+
alias_method_chain :type_cast, :composite_types
|
70
|
+
end
|
71
|
+
|
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
|
+
|
82
|
+
alias_method_chain :type_cast, :composite_types
|
83
|
+
|
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
|
+
adapter.type_cast(value, column, true)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
"(#{quoted_values.join(',')})"
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class PostgreSQLColumn
|
4
|
+
class CompositeTypeParser
|
5
|
+
DOUBLE_QUOTE = '"'
|
6
|
+
BACKSLASH = "\\"
|
7
|
+
COMMA = ','
|
8
|
+
BRACKET_OPEN = '('
|
9
|
+
BRACKET_CLOSE = ')'
|
10
|
+
|
11
|
+
def self.parse_data(string)
|
12
|
+
new.parse_data(string)
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_data(string)
|
16
|
+
local_index = 0
|
17
|
+
array = []
|
18
|
+
while (local_index < string.length)
|
19
|
+
case string[local_index]
|
20
|
+
when BRACKET_OPEN
|
21
|
+
local_index, array = parse_composite_type_contents(array, string, local_index + 1)
|
22
|
+
when BRACKET_CLOSE
|
23
|
+
return array
|
24
|
+
end
|
25
|
+
local_index += 1
|
26
|
+
end
|
27
|
+
|
28
|
+
array
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def parse_composite_type_contents(array, string, index)
|
33
|
+
is_escaping = false
|
34
|
+
is_quoted = false
|
35
|
+
was_quoted = false
|
36
|
+
current_item = ''
|
37
|
+
|
38
|
+
local_index = index
|
39
|
+
while local_index
|
40
|
+
token = string[local_index]
|
41
|
+
if is_escaping
|
42
|
+
current_item << token
|
43
|
+
is_escaping = false
|
44
|
+
else
|
45
|
+
if is_quoted
|
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
|
95
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class PostgresCompositeType
|
2
|
+
include Comparable
|
3
|
+
|
4
|
+
class << self
|
5
|
+
# TODO: doc
|
6
|
+
attr_reader :type
|
7
|
+
# TODO: doc
|
8
|
+
attr_reader :columns
|
9
|
+
|
10
|
+
def register_type(type)
|
11
|
+
@type = type.to_sym
|
12
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.register_composite_type_class(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
def use_connection_class(active_record_class)
|
16
|
+
@connection_class = active_record_class
|
17
|
+
end
|
18
|
+
|
19
|
+
def connection
|
20
|
+
(@connection_class || ActiveRecord::Base).connection
|
21
|
+
end
|
22
|
+
|
23
|
+
def connected?
|
24
|
+
(@connection_class || ActiveRecord::Base).connected?
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize_column_definition
|
28
|
+
unless @columns
|
29
|
+
@columns = self.connection.columns(type)
|
30
|
+
attr_accessor *@columns.map(&:name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(value)
|
36
|
+
self.class.initialize_column_definition
|
37
|
+
|
38
|
+
case value
|
39
|
+
when String
|
40
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLColumn.string_to_composite_type(self.class, value)
|
41
|
+
when Array
|
42
|
+
set_values value
|
43
|
+
when Hash
|
44
|
+
set_attributes value
|
45
|
+
else
|
46
|
+
raise "Unexpected value: #{value.inspect}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def <=>(another)
|
51
|
+
return nil if (self.class <=> another.class) == nil
|
52
|
+
self.class.columns.each do |column|
|
53
|
+
v1 = self.send(column.name)
|
54
|
+
v2 = another.send(column.name)
|
55
|
+
return v1 <=> v2 unless v1 == v2
|
56
|
+
end
|
57
|
+
0
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def set_attributes(values)
|
63
|
+
values.each do |name, value|
|
64
|
+
send "#{name}=", value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_values(values)
|
69
|
+
raise "Invalid values count: #{values.size}, expected: #{self.class.columns.size}" if values.size != self.class.columns.size
|
70
|
+
self.class.columns.each.with_index do |column, i|
|
71
|
+
send "#{column.name}=", values[i]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
# = Postgres Composite Types Railtie
|
4
|
+
#
|
5
|
+
# Creates a new railtie to initialize ActiveRecord properly
|
6
|
+
|
7
|
+
class PostgresCompositeTypesRailtie < Rails::Railtie
|
8
|
+
|
9
|
+
initializer 'activerecord-postgres-composite-types' do
|
10
|
+
ActiveSupport.on_load :active_record do
|
11
|
+
require "activerecord-postgres-composite-types/active_record"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
if ActiveRecord::VERSION::MAJOR > 3
|
3
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID.alias_type 'rgb_color', 'text'
|
4
|
+
end
|
5
|
+
|
6
|
+
class Compfoo < PostgresCompositeType
|
7
|
+
register_type :compfoo
|
8
|
+
end
|
9
|
+
|
10
|
+
class MyType < PostgresCompositeType
|
11
|
+
register_type :my_type
|
12
|
+
end
|
13
|
+
|
14
|
+
class NestedType < PostgresCompositeType
|
15
|
+
register_type :nested_type
|
16
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
module SimpleCov::Configuration
|
4
|
+
def clean_filters
|
5
|
+
@filters = []
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
SimpleCov.configure do
|
10
|
+
clean_filters
|
11
|
+
load_profile 'test_frameworks'
|
12
|
+
end
|
13
|
+
|
14
|
+
ENV["COVERAGE"] && SimpleCov.start do
|
15
|
+
add_filter "/.rvm/"
|
16
|
+
end
|
17
|
+
require 'rubygems'
|
18
|
+
require 'bundler'
|
19
|
+
begin
|
20
|
+
Bundler.require(:default, :development)
|
21
|
+
rescue Bundler::BundlerError => e
|
22
|
+
$stderr.puts e.message
|
23
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
24
|
+
exit e.status_code
|
25
|
+
end
|
26
|
+
|
27
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
28
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
29
|
+
|
30
|
+
require 'activerecord-postgres-composite-types'
|
31
|
+
|
32
|
+
ActiveSupport.on_load :active_record do
|
33
|
+
require 'activerecord-postgres-composite-types/active_record'
|
34
|
+
require_relative 'composite_types.rb'
|
35
|
+
end
|
36
|
+
|
37
|
+
Combustion.path = 'test/internal'
|
38
|
+
Combustion.initialize! :active_record
|
39
|
+
|
40
|
+
class Test::Unit::TestCase
|
41
|
+
def connection
|
42
|
+
ActiveRecord::Base.connection
|
43
|
+
end
|
44
|
+
end
|