ar_jdbc_pg_array 0.1.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +48 -0
- data/Rakefile +7 -0
- data/ar_jdbc_pg_array.gemspec +24 -0
- data/lib/ar_jdbc_pg_array.rb +12 -0
- data/lib/ar_jdbc_pg_array/allways_save.rb +38 -0
- data/lib/ar_jdbc_pg_array/parser.rb +174 -0
- data/lib/ar_jdbc_pg_array/querying.rb +72 -0
- data/lib/ar_jdbc_pg_array/querying_arel.rb +172 -0
- data/lib/ar_jdbc_pg_array/references_by.rb +154 -0
- data/lib/ar_jdbc_pg_array/schema.rb +248 -0
- data/lib/ar_jdbc_pg_array/schema_arel.rb +47 -0
- data/lib/ar_jdbc_pg_array/schema_cacheable.rb +6 -0
- data/lib/ar_jdbc_pg_array/schema_fix_will_change.rb +13 -0
- data/spec/fixtures/bulk.rb +5 -0
- data/spec/fixtures/bulks.yml +36 -0
- data/spec/fixtures/item.rb +4 -0
- data/spec/fixtures/items.yml +22 -0
- data/spec/fixtures/schema.rb +30 -0
- data/spec/fixtures/tag.rb +2 -0
- data/spec/fixtures/tags.yml +15 -0
- data/spec/fixtures/unrelated.rb +21 -0
- data/spec/fixtures/unrelateds.yml +3 -0
- data/spec/pg_array_spec.rb +380 -0
- data/spec/spec_helper.rb +55 -0
- metadata +186 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
module PGArrays
|
2
|
+
module ReferencesBy
|
3
|
+
if defined? ::Arel
|
4
|
+
HAS_AREL=true
|
5
|
+
NAMED_SCOPE='scope'
|
6
|
+
else
|
7
|
+
NAMED_SCOPE='named_scope'
|
8
|
+
if defined? ::FakeArel
|
9
|
+
HAS_AREL= true
|
10
|
+
else
|
11
|
+
HAS_AREL=false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class RelationHolder < Struct.new(:relation, :field, :klass)
|
16
|
+
|
17
|
+
def referenced(obj)
|
18
|
+
ids = (obj[field] || []).map{|i| i.to_i}
|
19
|
+
objs = klass.find_all_by_id( ids.sort )
|
20
|
+
if ids.size < 20
|
21
|
+
objs.sort_by{|o| ids.index(o.id)}
|
22
|
+
else
|
23
|
+
to_ind = ids.each_with_index.inject({}){|h, (v,i)| h[v]=i; h}
|
24
|
+
objs.sort_by{|o| to_ind[o.id]}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_referenced(obj, value)
|
29
|
+
obj[field] = map_to_ids(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
if HAS_AREL
|
33
|
+
def validate(obj)
|
34
|
+
has_ids = klass.where(:id=>obj.read_attribute(field)).
|
35
|
+
select('id').all.map(&:id)
|
36
|
+
unless has_ids.sort == obj.read_attribute(field).sort
|
37
|
+
obj.errors.add(relation, :wrong_array_reference)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def for_referenced(obj_klass)
|
42
|
+
myself = self
|
43
|
+
obj_klass.class_exec do
|
44
|
+
puts "named_scope #{NAMED_SCOPE} #{myself.relation}_include"
|
45
|
+
self.send NAMED_SCOPE, "#{myself.relation}_include", lambda{|*objs|
|
46
|
+
objs = myself.map_to_ids objs.flatten
|
47
|
+
where(myself.field.to_sym => objs.search_all)
|
48
|
+
}
|
49
|
+
|
50
|
+
self.send NAMED_SCOPE, "#{myself.relation}_have_all", lambda{|*objs|
|
51
|
+
objs = myself.map_to_ids objs.flatten
|
52
|
+
where(myself.field.to_sym => objs.search_all)
|
53
|
+
}
|
54
|
+
|
55
|
+
self.send NAMED_SCOPE, "#{myself.relation}_have_any", lambda{|*objs|
|
56
|
+
objs = myself.map_to_ids objs.flatten
|
57
|
+
where(myself.field.to_sym => objs.search_any)
|
58
|
+
}
|
59
|
+
|
60
|
+
self.send NAMED_SCOPE, "#{myself.relation}_included_into", lambda{|*objs|
|
61
|
+
objs = myself.map_to_ids objs.flatten
|
62
|
+
where(myself.field.to_sym => objs.search_subarray)
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
else
|
67
|
+
def validate(obj)
|
68
|
+
has_ids = klass.find(:all,
|
69
|
+
:select=>'id',
|
70
|
+
:conditions=>{:id=>obj.read_attribute(field)}
|
71
|
+
).map(&:id)
|
72
|
+
unless has_ids.sort == obj.read_attribute(field).sort
|
73
|
+
obj.errors.add(relation, :wrong_array_reference)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def for_referenced(obj_klass)
|
78
|
+
myself = self
|
79
|
+
obj_klass.class_exec do
|
80
|
+
named_scope "#{myself.relation}_include", lambda{|*objs|
|
81
|
+
objs = myself.map_to_ids objs.flatten
|
82
|
+
{ :conditions=>{ myself.field.to_sym => objs.search_all } }
|
83
|
+
}
|
84
|
+
|
85
|
+
named_scope "#{myself.relation}_have_all", lambda{|*objs|
|
86
|
+
objs = myself.map_to_ids objs.flatten
|
87
|
+
{ :conditions=>{ myself.field.to_sym => objs.search_all } }
|
88
|
+
}
|
89
|
+
|
90
|
+
named_scope "#{myself.relation}_have_any", lambda{|*objs|
|
91
|
+
objs = myself.map_to_ids objs.flatten
|
92
|
+
{ :conditions=>{ myself.field.to_sym => objs.search_any } }
|
93
|
+
}
|
94
|
+
|
95
|
+
named_scope "#{myself.relation}_included_into", lambda{|*objs|
|
96
|
+
objs = myself.map_to_ids objs.flatten
|
97
|
+
{ :conditions=>{ myself.field.to_sym => objs.search_subarray } }
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def val_to_id(val)
|
104
|
+
case val
|
105
|
+
when ActiveRecord::Base then val.id
|
106
|
+
when nil then nil
|
107
|
+
else val.to_i
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def map_to_ids(vals)
|
112
|
+
(r = vals.map{|v| val_to_id(v)}).compact!
|
113
|
+
r
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
module ClassMethods
|
118
|
+
def references_by_array( relation, options = {} )
|
119
|
+
unless ActiveSupport::Memoizable === self
|
120
|
+
extend ActiveSupport::Memoizable
|
121
|
+
end
|
122
|
+
|
123
|
+
relation = relation.to_s.pluralize
|
124
|
+
field = "#{relation.singularize}_ids"
|
125
|
+
klass_name = (options[:class_name] || relation).to_s.singularize.camelize
|
126
|
+
klass = klass_name.constantize
|
127
|
+
holder = RelationHolder.new(relation, field, klass )
|
128
|
+
|
129
|
+
meths = Module.new do
|
130
|
+
define_method(relation) do
|
131
|
+
holder.referenced(self)
|
132
|
+
end
|
133
|
+
|
134
|
+
define_method("#{relation}=") do |value|
|
135
|
+
flush_cache(relation)
|
136
|
+
holder.set_referenced(self, value)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
include meths
|
140
|
+
|
141
|
+
memoize relation
|
142
|
+
|
143
|
+
if options[:validate]
|
144
|
+
validate {|o| holder.validate(o)}
|
145
|
+
end
|
146
|
+
|
147
|
+
holder.for_referenced(self)
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
ActiveRecord::Base.extend PGArrays::ReferencesBy::ClassMethods
|
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'ar_jdbc_pg_array/parser'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
class PostgreSQLColumn
|
6
|
+
include PgArrayParser
|
7
|
+
extend PgArrayParser
|
8
|
+
|
9
|
+
BASE_TYPE_COLUMNS = Hash.new { |h, base_type|
|
10
|
+
base_column = new(nil, nil, base_type.to_s, true)
|
11
|
+
h[base_type] = h[base_column.type] = base_column
|
12
|
+
}
|
13
|
+
attr_reader :base_column
|
14
|
+
|
15
|
+
def initialize(name, default, sql_type = nil, null = true)
|
16
|
+
if sql_type =~ /^(.+)\[\]$/
|
17
|
+
@base_sql_type = $1
|
18
|
+
@base_column = BASE_TYPE_COLUMNS[@base_sql_type]
|
19
|
+
end
|
20
|
+
|
21
|
+
if Hash === name
|
22
|
+
super
|
23
|
+
else
|
24
|
+
super(nil, name, default, sql_type, null)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def simplified_type_with_postgresql_arrays(field_type)
|
29
|
+
if field_type =~ /^(.+)\[\]$/
|
30
|
+
:"#{simplified_type_without_postgresql_arrays($1)}_array"
|
31
|
+
else
|
32
|
+
simplified_type_without_postgresql_arrays(field_type)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
alias_method_chain :simplified_type, :postgresql_arrays
|
36
|
+
|
37
|
+
def klass
|
38
|
+
if type.to_s =~ /_array$/
|
39
|
+
Array
|
40
|
+
else
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def type_cast(value)
|
46
|
+
return nil if value.nil?
|
47
|
+
case type
|
48
|
+
when :integer_array, :float_array
|
49
|
+
string_to_num_array(value)
|
50
|
+
when :decimal_array, :date_array, :boolean_array
|
51
|
+
safe_string_to_array(value)
|
52
|
+
when :timestamp_array, :time_array, :datetime_array, :binary_array
|
53
|
+
string_to_array(value)
|
54
|
+
when :text_array, :string_array
|
55
|
+
string_to_text_array(value)
|
56
|
+
else super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def type_cast_code(var_name)
|
61
|
+
case type
|
62
|
+
when :integer_array, :float_array
|
63
|
+
"#{self.class.name}.string_to_num_array(#{var_name})"
|
64
|
+
when :decimal_array, :date_array, :boolean_array
|
65
|
+
"#{self.class.name}.safe_string_to_array(#{var_name}, #{@base_sql_type.inspect})"
|
66
|
+
when :timestamp_array, :time_array, :datetime_array, :binary_array
|
67
|
+
"#{self.class.name}.string_to_array(#{var_name}, #{@base_sql_type.inspect})"
|
68
|
+
when :text_array, :string_array
|
69
|
+
"#{self.class.name}.string_to_text_array(#{var_name})"
|
70
|
+
else super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def default
|
75
|
+
res = super
|
76
|
+
Array === res ? res.dup : res
|
77
|
+
end
|
78
|
+
|
79
|
+
def self._string_to_array(string)
|
80
|
+
return string unless string.is_a? String
|
81
|
+
return nil if string.empty?
|
82
|
+
|
83
|
+
yield
|
84
|
+
end
|
85
|
+
|
86
|
+
def _string_to_array(string)
|
87
|
+
return string unless string.is_a? String
|
88
|
+
return nil if string.empty?
|
89
|
+
|
90
|
+
yield
|
91
|
+
end
|
92
|
+
|
93
|
+
def safe_string_to_array(string)
|
94
|
+
_string_to_array(string) do
|
95
|
+
parse_safe_pgarray(string){|v| @base_column.type_cast(v)}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.safe_string_to_array(string, sql_type)
|
100
|
+
_string_to_array(string) do
|
101
|
+
base_column = BASE_TYPE_COLUMNS[sql_type]
|
102
|
+
parse_safe_pgarray(string){|v| base_column.type_cast(v)}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def string_to_array(string)
|
107
|
+
_string_to_array(string) do
|
108
|
+
parse_pgarray(string){|v| @base_column.type_cast(v)}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.string_to_array(string, sql_type)
|
113
|
+
_string_to_array(string) do
|
114
|
+
base_column = BASE_TYPE_COLUMNS[sql_type]
|
115
|
+
parse_pgarray(string){|v| base_column.type_cast(v)}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def string_to_num_array(string)
|
120
|
+
_string_to_array(string) do
|
121
|
+
parse_numeric_pgarray(string)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.string_to_num_array(string)
|
126
|
+
_string_to_array(string) do
|
127
|
+
parse_numeric_pgarray(string)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def string_to_text_array(string)
|
132
|
+
_string_to_array(string) do
|
133
|
+
parse_pgarray(string){|v| v}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.string_to_text_array(string)
|
138
|
+
_string_to_array(string) do
|
139
|
+
parse_pgarray(string){|v| v}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class PostgreSQLAdapter #:nodoc:
|
145
|
+
include PgArrayParser
|
146
|
+
def quote_with_postgresql_arrays(value, column = nil)
|
147
|
+
if Array === value && column && "#{column.type}" =~ /^(.+)_array$/
|
148
|
+
quote_array_by_base_type(value, $1, column)
|
149
|
+
else
|
150
|
+
quote_without_postgresql_arrays(value, column)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
alias_method_chain :quote, :postgresql_arrays
|
154
|
+
|
155
|
+
def quote_array_by_base_type(value, base_type, column = nil)
|
156
|
+
case base_type.to_sym
|
157
|
+
when :integer, :float, :decimal, :boolean, :date, :safe, :datetime, :timestamp, :time
|
158
|
+
"'#{ prepare_array_for_arel_by_base_type(value, base_type) }'"
|
159
|
+
when :string, :text, :other
|
160
|
+
pa = prepare_array_for_arel_by_base_type(value, base_type)
|
161
|
+
"'#{ quote_string(pa) }'"
|
162
|
+
else
|
163
|
+
"'#{ prepare_pg_string_array(value, base_type, column) }'"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def prepare_array_for_arel_by_base_type(value, base_type)
|
168
|
+
case base_type.to_sym
|
169
|
+
when :integer
|
170
|
+
prepare_pg_integer_array(value)
|
171
|
+
when :float
|
172
|
+
prepare_pg_float_array(value)
|
173
|
+
when :string, :text, :other
|
174
|
+
prepare_pg_text_array(value)
|
175
|
+
when :datetime, :timestamp, :time
|
176
|
+
prepare_pg_string_array(value, base_type)
|
177
|
+
when :decimal, :boolean, :date, :safe
|
178
|
+
prepare_pg_safe_array(value)
|
179
|
+
else
|
180
|
+
raise "Unsupported array base type #{base_type} for arel"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def prepare_pg_string_array(value, base_type, column = nil)
|
185
|
+
base_column = if column
|
186
|
+
column.base_column
|
187
|
+
else
|
188
|
+
PostgreSQLColumn::BASE_TYPE_COLUMNS[base_type.to_sym]
|
189
|
+
end
|
190
|
+
_prepare_pg_string_array(value) { |v| quote_without_postgresql_arrays(v, base_column) }
|
191
|
+
end
|
192
|
+
|
193
|
+
NATIVE_DATABASE_TYPES.keys.each do |key|
|
194
|
+
unless key == :primary_key
|
195
|
+
base = NATIVE_DATABASE_TYPES[key].dup
|
196
|
+
base[:name] = base[:name] + '[]'
|
197
|
+
NATIVE_DATABASE_TYPES[:"#{key}_array"] = base
|
198
|
+
TableDefinition.class_eval <<-EOV
|
199
|
+
def #{key}_array(*args) # def string_array(*args)
|
200
|
+
options = args.extract_options! # options = args.extract_options!
|
201
|
+
column_names = args # column_names = args
|
202
|
+
#
|
203
|
+
column_names.each { |name| column(name, :'#{key}_array', options) } # column_names.each { |name| column(name, :string_array, options) }
|
204
|
+
end # end
|
205
|
+
EOV
|
206
|
+
Table.class_eval <<-EOV
|
207
|
+
def #{key}_array(*args) # def string_array(*args)
|
208
|
+
options = args.extract_options! # options = args.extract_options!
|
209
|
+
column_names = args # column_names = args
|
210
|
+
#
|
211
|
+
column_names.each { |name| # column_names.each { |name|
|
212
|
+
@base.add_column(@table_name, name, :'#{key}_array', options) # @base.add_column(@table_name, name, :string_array, options) }
|
213
|
+
} # }
|
214
|
+
end # end
|
215
|
+
EOV
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def add_column_with_postgresql_arrays(table, column, type, options = {})
|
220
|
+
if type.to_s =~ /^(.+)_array$/ && options[:default].is_a?(Array)
|
221
|
+
options = options.merge(:default => prepare_array_for_arel_by_base_type(options[:default], $1))
|
222
|
+
end
|
223
|
+
add_column_without_postgresql_arrays(table, column, type, options)
|
224
|
+
end
|
225
|
+
alias_method_chain :add_column, :postgresql_arrays
|
226
|
+
|
227
|
+
def type_to_sql_with_postgresql_arrays(type, limit = nil, precision = nil, scale = nil)
|
228
|
+
if type.to_s =~ /^(.+)_array$/
|
229
|
+
type_to_sql_without_postgresql_arrays($1.to_sym, limit, precision, scale) + '[]'
|
230
|
+
else
|
231
|
+
type_to_sql_without_postgresql_arrays(type, limit, precision, scale)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
alias_method_chain :type_to_sql, :postgresql_arrays
|
235
|
+
|
236
|
+
if method_defined?(:type_cast)
|
237
|
+
def type_cast_with_postgresql_arrays(value, column)
|
238
|
+
if Array === value && column && "#{column.type}" =~ /^(.+)_array$/
|
239
|
+
prepare_array_for_arel_by_base_type(value, $1)
|
240
|
+
else
|
241
|
+
type_cast_without_postgresql_arrays(value, column)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
alias_method_chain :type_cast, :postgresql_arrays
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Arel
|
2
|
+
module Attributes
|
3
|
+
%w{Integer Float Decimal Boolean String Time}.each do |basetype|
|
4
|
+
module_eval <<-"END"
|
5
|
+
class #{basetype}Array < Attribute
|
6
|
+
end
|
7
|
+
END
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Attributes
|
12
|
+
class << self
|
13
|
+
def for_with_postgresql_arrays(column)
|
14
|
+
if column.type.to_s =~ /^(.+)_array$/
|
15
|
+
('Arel::Attributes::' + for_without_postgresql_arrays(column.base_column).name.split('::').last + 'Array').constantize
|
16
|
+
else
|
17
|
+
for_without_postgresql_arrays(column)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
alias_method_chain :for, :postgresql_arrays
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ActiveRecord
|
26
|
+
class << Base
|
27
|
+
if method_defined?(:column_defaults)
|
28
|
+
alias column_defaults_without_extradup column_defaults
|
29
|
+
def column_defaults_with_extradup
|
30
|
+
res = {}
|
31
|
+
column_defaults_without_extradup.each{|k, v|
|
32
|
+
res[k] = Array === v ? v.dup : v
|
33
|
+
}
|
34
|
+
res
|
35
|
+
end
|
36
|
+
def column_defaults
|
37
|
+
defaults = column_defaults_without_extradup
|
38
|
+
if defaults.values.grep(Array).empty?
|
39
|
+
alias column_defaults column_defaults_without_extradup
|
40
|
+
else
|
41
|
+
alias column_defaults column_defaults_with_extradup
|
42
|
+
end
|
43
|
+
column_defaults
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|