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