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.
@@ -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