activerecord-postgres-composite-types 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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