neo4j-core 4.0.7 → 5.0.0.rc.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.
@@ -12,8 +12,11 @@ module Neo4j
12
12
 
13
13
  class Clause
14
14
  include CypherTranslator
15
+ UNDERSCORE = '_'
16
+ COMMA_SPACE = ', '
17
+ AND = ' AND '
15
18
 
16
- attr_reader :params
19
+ attr_accessor :params, :arg
17
20
 
18
21
  def initialize(arg, options = {})
19
22
  @arg = arg
@@ -54,19 +57,23 @@ module Neo4j
54
57
  label = label_from_key_and_value(key, value, options[:prefer] || :var)
55
58
  attributes = attributes_from_key_and_value(key, value)
56
59
 
57
- "(#{var}#{format_label(label)}#{attributes_string(attributes)})"
60
+ prefix_value = value
61
+ if value.is_a?(Hash)
62
+ prefix_value = if value.values.any? { |v| v.is_a?(Hash) }
63
+ value.keys.join(UNDERSCORE)
64
+ end
65
+ end
66
+
67
+ prefix_array = [key, prefix_value].tap(&:compact!).join(UNDERSCORE)
68
+ formatted_attributes = attributes_string(attributes, "#{prefix_array}#{UNDERSCORE}")
69
+ "(#{var}#{format_label(label)}#{formatted_attributes})"
58
70
  end
59
71
 
60
72
  def var_from_key_and_value(key, value, prefer = :var)
61
73
  case value
62
- when String, Symbol, Class, Module
63
- key
74
+ when String, Symbol, Class, Module, NilClass, Array then key
64
75
  when Hash
65
- if value.values.none? { |v| v.is_a?(Hash) }
66
- key if prefer == :var
67
- else
68
- key
69
- end
76
+ key if _use_key_for_var?(value, prefer)
70
77
  else
71
78
  fail ArgError, value
72
79
  end
@@ -74,22 +81,29 @@ module Neo4j
74
81
 
75
82
  def label_from_key_and_value(key, value, prefer = :var)
76
83
  case value
77
- when String, Symbol
78
- value
79
- when Class, Module
80
- defined?(value::CYPHER_LABEL) ? value::CYPHER_LABEL : value.name
84
+ when String, Symbol, Array, NilClass then value
85
+ when Class, Module then value.name
81
86
  when Hash
82
87
  if value.values.map(&:class) == [Hash]
83
88
  value.first.first
84
89
  else
85
- key if value.values.none? { |v| v.is_a?(Hash) } && prefer == :label
90
+ key if !_use_key_for_var?(value, prefer)
86
91
  end
87
92
  else
88
93
  fail ArgError, value
89
94
  end
90
95
  end
91
96
 
92
- def attributes_from_key_and_value(key, value)
97
+ def _use_key_for_var?(value, prefer)
98
+ _nested_value_hash?(value) || prefer == :var
99
+ end
100
+
101
+ def _nested_value_hash?(value)
102
+ value.values.any? { |v| v.is_a?(Hash) }
103
+ end
104
+
105
+
106
+ def attributes_from_key_and_value(_key, value)
93
107
  return nil unless value.is_a?(Hash)
94
108
 
95
109
  if value.values.map(&:class) == [Hash]
@@ -100,28 +114,41 @@ module Neo4j
100
114
  end
101
115
 
102
116
  class << self
103
- attr_reader :keyword
117
+ def keyword
118
+ self::KEYWORD
119
+ end
120
+
121
+ def keyword_downcase
122
+ keyword.downcase
123
+ end
104
124
 
105
125
  def from_args(args, options = {})
106
- args.flatten.map do |arg|
107
- new(arg, options) if !arg.respond_to?(:empty?) || !arg.empty?
108
- end.compact
126
+ args.flatten!
127
+ args.map { |arg| from_arg(arg, options) }.tap(&:compact!)
128
+ end
129
+
130
+ def from_arg(arg, options = {})
131
+ new(arg, options) if !arg.respond_to?(:empty?) || !arg.empty?
109
132
  end
110
133
 
111
134
  def to_cypher(clauses)
135
+ @question_mark_param_index = 1
136
+
112
137
  string = clause_string(clauses)
113
138
  string.strip!
114
139
 
115
- "#{@keyword} #{string}" if string.size > 0
140
+ "#{keyword} #{string}" if string.size > 0
116
141
  end
117
142
  end
118
143
 
119
144
  private
120
145
 
121
146
  def key_value_string(key, value, previous_keys = [], force_equals = false)
122
- param = (previous_keys << key).join('_')
123
- param.gsub!(/[^a-z0-9]+/i, '_')
147
+ param = (previous_keys << key).join(UNDERSCORE)
148
+ param.tr_s!('^a-zA-Z0-9', UNDERSCORE)
124
149
  param.gsub!(/^_+|_+$/, '')
150
+
151
+ value = value.first if value.is_a?(Array) && value.size == 1
125
152
  @params[param.to_sym] = value
126
153
 
127
154
  if !value.is_a?(Array) || force_equals
@@ -131,34 +158,39 @@ module Neo4j
131
158
  end
132
159
  end
133
160
 
134
- def format_label(label_string)
135
- label_string = label_string.to_s
136
- label_string.strip!
137
- if !label_string.empty? && label_string[0] != ':'
138
- label_string = "`#{label_string}`" unless label_string.match(' ')
139
- label_string = ":#{label_string}"
161
+ def format_label(label_arg)
162
+ if label_arg.is_a?(Array)
163
+ return label_arg.map { |arg| format_label(arg) }.join
140
164
  end
141
- label_string
165
+
166
+ label_arg = label_arg.to_s
167
+ label_arg.strip!
168
+ if !label_arg.empty? && label_arg[0] != ':'
169
+ label_arg = "`#{label_arg}`" unless label_arg.match(' ')
170
+ label_arg = ":#{label_arg}"
171
+ end
172
+ label_arg
142
173
  end
143
174
 
144
- def attributes_string(attributes)
175
+ def attributes_string(attributes, prefix = '')
145
176
  return '' if not attributes
146
177
 
147
178
  attributes_string = attributes.map do |key, value|
148
- v = if value.nil?
149
- 'null'
150
- else
151
- value.to_s.match(/^{.+}$/) ? value : value.inspect
152
- end
153
- "#{key}: #{v}"
154
- end.join(', ')
179
+ if value.to_s.match(/^{.+}$/)
180
+ "#{key}: #{value}"
181
+ else
182
+ param_key = "#{prefix}#{key}".gsub('::', '_')
183
+ @params[param_key.to_sym] = value
184
+ "#{key}: {#{param_key}}"
185
+ end
186
+ end.join(Clause::COMMA_SPACE)
155
187
 
156
188
  " {#{attributes_string}}"
157
189
  end
158
190
  end
159
191
 
160
192
  class StartClause < Clause
161
- @keyword = 'START'
193
+ KEYWORD = 'START'
162
194
 
163
195
  def from_symbol(value)
164
196
  from_string(value.to_s)
@@ -175,33 +207,20 @@ module Neo4j
175
207
 
176
208
  class << self
177
209
  def clause_string(clauses)
178
- clauses.map!(&:value).join(', ')
210
+ clauses.map!(&:value).join(Clause::COMMA_SPACE)
179
211
  end
180
212
  end
181
213
  end
182
214
 
183
215
  class WhereClause < Clause
184
- @keyword = 'WHERE'
216
+ KEYWORD = 'WHERE'
185
217
 
186
218
  def from_key_and_value(key, value, previous_keys = [])
187
219
  case value
188
- when Hash
189
- value.map do |k, v|
190
- if k.to_sym == :neo_id
191
- clause_id = "neo_id_#{v}"
192
- @params[clause_id] = v.to_i
193
- "ID(#{key}) = {#{clause_id}}"
194
- else
195
- "#{key}.#{from_key_and_value(k, v, previous_keys + [key])}"
196
- end
197
- end.join(' AND ')
198
- when NilClass
199
- "#{key} IS NULL"
200
- when Regexp
201
- pattern = (value.casefold? ? '(?i)' : '') + value.source
202
- "#{key} =~ #{escape_value(pattern.gsub(/\\/, '\\\\\\'))}"
203
- when Array
204
- key_value_string(key, value, previous_keys)
220
+ when Hash then hash_key_value_string(key, value, previous_keys)
221
+ when NilClass then "#{key} IS NULL"
222
+ when Regexp then regexp_key_value_string(key, value)
223
+ when Array then key_value_string(key, value, previous_keys)
205
224
  else
206
225
  key_value_string(key, value, previous_keys)
207
226
  end
@@ -209,14 +228,62 @@ module Neo4j
209
228
 
210
229
  class << self
211
230
  def clause_string(clauses)
212
- clauses.map(&:value).flatten.map {|value| "(#{value})" }.join(' AND ')
231
+ clauses.map!(&:value).tap(&:flatten!).map! { |value| "(#{value})" }.join(Clause::AND)
232
+ end
233
+ end
234
+
235
+ private
236
+
237
+ def hash_key_value_string(key, value, previous_keys)
238
+ value.map do |k, v|
239
+ if k.to_sym == :neo_id
240
+ v = Array(v).map { |item| (item.respond_to?(:neo_id) ? item.neo_id : item).to_i }
241
+ key_value_string("ID(#{key})", v)
242
+ else
243
+ "#{key}.#{from_key_and_value(k, v, previous_keys + [key])}"
244
+ end
245
+ end.join(AND)
246
+ end
247
+
248
+ def regexp_key_value_string(key, value)
249
+ pattern = (value.casefold? ? '(?i)' : '') + value.source
250
+ "#{key} =~ #{escape_value(pattern.gsub(/\\/, '\\\\\\'))}"
251
+ end
252
+
253
+ class << self
254
+ ARG_HAS_QUESTION_MARK_REGEX = /(^|\s)\?(\s|$)/
255
+
256
+ def from_args(args, options = {})
257
+ query_string, params = args
258
+
259
+ if query_string.is_a?(String) && (query_string.match(ARG_HAS_QUESTION_MARK_REGEX) || params.is_a?(Hash))
260
+ if !params.is_a?(Hash)
261
+ question_mark_params_param = self.question_mark_params_param
262
+ query_string.gsub!(ARG_HAS_QUESTION_MARK_REGEX, "\\1{#{question_mark_params_param}}\\2")
263
+ params = {question_mark_params_param.to_sym => params}
264
+ end
265
+
266
+ clause = from_arg(query_string, options).tap do |clause|
267
+ clause.params.merge!(params)
268
+ end
269
+
270
+ [clause]
271
+ else
272
+ super
273
+ end
274
+ end
275
+
276
+ def question_mark_params_param
277
+ result = "question_mark_param#{@question_mark_param_index}"
278
+ @question_mark_param_index += 1
279
+ result
213
280
  end
214
281
  end
215
282
  end
216
283
 
217
284
 
218
285
  class MatchClause < Clause
219
- @keyword = 'MATCH'
286
+ KEYWORD = 'MATCH'
220
287
 
221
288
  def from_symbol(value)
222
289
  from_string(value.to_s)
@@ -228,17 +295,17 @@ module Neo4j
228
295
 
229
296
  class << self
230
297
  def clause_string(clauses)
231
- clauses.map!(&:value).join(', ')
298
+ clauses.map!(&:value).join(Clause::COMMA_SPACE)
232
299
  end
233
300
  end
234
301
  end
235
302
 
236
303
  class OptionalMatchClause < MatchClause
237
- @keyword = 'OPTIONAL MATCH'
304
+ KEYWORD = 'OPTIONAL MATCH'
238
305
  end
239
306
 
240
307
  class WithClause < Clause
241
- @keyword = 'WITH'
308
+ KEYWORD = 'WITH'
242
309
 
243
310
  def from_symbol(value)
244
311
  from_string(value.to_s)
@@ -250,23 +317,23 @@ module Neo4j
250
317
 
251
318
  class << self
252
319
  def clause_string(clauses)
253
- clauses.map!(&:value).join(', ')
320
+ clauses.map!(&:value).join(Clause::COMMA_SPACE)
254
321
  end
255
322
  end
256
323
  end
257
324
 
258
325
  class UsingClause < Clause
259
- @keyword = 'USING'
326
+ KEYWORD = 'USING'
260
327
 
261
328
  class << self
262
329
  def clause_string(clauses)
263
- clauses.map!(&:value).join(" #{@keyword} ")
330
+ clauses.map!(&:value).join(" #{keyword} ")
264
331
  end
265
332
  end
266
333
  end
267
334
 
268
335
  class CreateClause < Clause
269
- @keyword = 'CREATE'
336
+ KEYWORD = 'CREATE'
270
337
 
271
338
  def from_string(value)
272
339
  value
@@ -298,15 +365,15 @@ module Neo4j
298
365
  end
299
366
 
300
367
  class CreateUniqueClause < CreateClause
301
- @keyword = 'CREATE UNIQUE'
368
+ KEYWORD = 'CREATE UNIQUE'
302
369
  end
303
370
 
304
371
  class MergeClause < CreateClause
305
- @keyword = 'MERGE'
372
+ KEYWORD = 'MERGE'
306
373
  end
307
374
 
308
375
  class DeleteClause < Clause
309
- @keyword = 'DELETE'
376
+ KEYWORD = 'DELETE'
310
377
 
311
378
  def from_symbol(value)
312
379
  from_string(value.to_s)
@@ -314,13 +381,13 @@ module Neo4j
314
381
 
315
382
  class << self
316
383
  def clause_string(clauses)
317
- clauses.map!(&:value).join(', ')
384
+ clauses.map!(&:value).join(Clause::COMMA_SPACE)
318
385
  end
319
386
  end
320
387
  end
321
388
 
322
389
  class OrderClause < Clause
323
- @keyword = 'ORDER BY'
390
+ KEYWORD = 'ORDER BY'
324
391
 
325
392
  def from_symbol(value)
326
393
  from_string(value.to_s)
@@ -332,38 +399,32 @@ module Neo4j
332
399
  "#{key}.#{value}"
333
400
  when Array
334
401
  value.map do |v|
335
- if v.is_a?(Hash)
336
- from_key_and_value(key, v)
337
- else
338
- "#{key}.#{v}"
339
- end
402
+ v.is_a?(Hash) ? from_key_and_value(key, v) : "#{key}.#{v}"
340
403
  end
341
404
  when Hash
342
- value.map do |k, v|
343
- "#{key}.#{k} #{v.upcase}"
344
- end
405
+ value.map { |k, v| "#{key}.#{k} #{v.upcase}" }
345
406
  end
346
407
  end
347
408
 
348
409
  class << self
349
410
  def clause_string(clauses)
350
- clauses.map!(&:value).join(', ')
411
+ clauses.map!(&:value).join(Clause::COMMA_SPACE)
351
412
  end
352
413
  end
353
414
  end
354
415
 
355
416
  class LimitClause < Clause
356
- @keyword = 'LIMIT'
417
+ KEYWORD = 'LIMIT'
357
418
 
358
419
  def from_string(value)
359
- clause_id = "#{self.class.keyword.downcase}_#{value}"
360
- @params[clause_id] = value.to_i
420
+ clause_id = "#{self.class.keyword_downcase}_#{value}"
421
+ @params[clause_id.to_sym] = value.to_i
361
422
  "{#{clause_id}}"
362
423
  end
363
424
 
364
425
  def from_integer(value)
365
- clause_id = "#{self.class.keyword.downcase}_#{value}"
366
- @params[clause_id] = value
426
+ clause_id = "#{self.class.keyword_downcase}_#{value}"
427
+ @params[clause_id.to_sym] = value
367
428
  "{#{clause_id}}"
368
429
  end
369
430
 
@@ -375,17 +436,17 @@ module Neo4j
375
436
  end
376
437
 
377
438
  class SkipClause < Clause
378
- @keyword = 'SKIP'
439
+ KEYWORD = 'SKIP'
379
440
 
380
441
  def from_string(value)
381
- clause_id = "#{self.class.keyword.downcase}_#{value}"
382
- @params[clause_id] = value.to_i
442
+ clause_id = "#{self.class.keyword_downcase}_#{value}"
443
+ @params[clause_id.to_sym] = value.to_i
383
444
  "{#{clause_id}}"
384
445
  end
385
446
 
386
447
  def from_integer(value)
387
- clause_id = "#{self.class.keyword.downcase}_#{value}"
388
- @params[clause_id] = value
448
+ clause_id = "#{self.class.keyword_downcase}_#{value}"
449
+ @params[clause_id.to_sym] = value
389
450
  "{#{clause_id}}"
390
451
  end
391
452
 
@@ -397,21 +458,20 @@ module Neo4j
397
458
  end
398
459
 
399
460
  class SetClause < Clause
400
- @keyword = 'SET'
461
+ KEYWORD = 'SET'
401
462
 
402
463
  def from_key_and_value(key, value)
403
464
  case value
404
- when String, Symbol
405
- "#{key} = #{value}"
465
+ when String, Symbol then "#{key}:`#{value}`"
406
466
  when Hash
407
467
  if @options[:set_props]
408
- attribute_string = value.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
468
+ attribute_string = value.map { |k, v| "#{k}: #{v.inspect}" }.join(Clause::COMMA_SPACE)
409
469
  "#{key} = {#{attribute_string}}"
410
470
  else
411
- value.map do |k, v|
412
- key_value_string("#{key}.`#{k}`", v, ['setter'], true)
413
- end
471
+ value.map { |k, v| key_value_string("#{key}.`#{k}`", v, ['setter'], true) }
414
472
  end
473
+ when Array then value.map { |v| from_key_and_value(key, v) }
474
+ when NilClass then []
415
475
  else
416
476
  fail ArgError, value
417
477
  end
@@ -419,13 +479,13 @@ module Neo4j
419
479
 
420
480
  class << self
421
481
  def clause_string(clauses)
422
- clauses.map!(&:value).join(', ')
482
+ clauses.map!(&:value).join(Clause::COMMA_SPACE)
423
483
  end
424
484
  end
425
485
  end
426
486
 
427
487
  class OnCreateSetClause < SetClause
428
- @keyword = 'ON CREATE SET'
488
+ KEYWORD = 'ON CREATE SET'
429
489
 
430
490
  def initialize(*args)
431
491
  super
@@ -434,20 +494,24 @@ module Neo4j
434
494
  end
435
495
 
436
496
  class OnMatchSetClause < OnCreateSetClause
437
- @keyword = 'ON MATCH SET'
497
+ KEYWORD = 'ON MATCH SET'
438
498
  end
439
499
 
440
500
  class RemoveClause < Clause
441
- @keyword = 'REMOVE'
501
+ KEYWORD = 'REMOVE'
442
502
 
443
503
  def from_key_and_value(key, value)
444
504
  case value
445
505
  when /^:/
446
- "#{key}:#{value[1..-1]}"
506
+ "#{key}:`#{value[1..-1]}`"
447
507
  when String
448
508
  "#{key}.#{value}"
449
509
  when Symbol
450
- "#{key}:#{value}"
510
+ "#{key}:`#{value}`"
511
+ when Array
512
+ value.map do |v|
513
+ from_key_and_value(key, v)
514
+ end
451
515
  else
452
516
  fail ArgError, value
453
517
  end
@@ -455,13 +519,13 @@ module Neo4j
455
519
 
456
520
  class << self
457
521
  def clause_string(clauses)
458
- clauses.map!(&:value).join(', ')
522
+ clauses.map!(&:value).join(Clause::COMMA_SPACE)
459
523
  end
460
524
  end
461
525
  end
462
526
 
463
527
  class UnwindClause < Clause
464
- @keyword = 'UNWIND'
528
+ KEYWORD = 'UNWIND'
465
529
 
466
530
  def from_key_and_value(key, value)
467
531
  case value
@@ -482,7 +546,7 @@ module Neo4j
482
546
  end
483
547
 
484
548
  class ReturnClause < Clause
485
- @keyword = 'RETURN'
549
+ KEYWORD = 'RETURN'
486
550
 
487
551
  def from_symbol(value)
488
552
  from_string(value.to_s)
@@ -493,9 +557,13 @@ module Neo4j
493
557
  when Array
494
558
  value.map do |v|
495
559
  from_key_and_value(key, v)
496
- end.join(', ')
560
+ end.join(Clause::COMMA_SPACE)
497
561
  when String, Symbol
498
- "#{key}.#{value}"
562
+ if value.to_sym == :neo_id
563
+ "ID(#{key})"
564
+ else
565
+ "#{key}.#{value}"
566
+ end
499
567
  else
500
568
  fail ArgError, value
501
569
  end
@@ -503,7 +571,7 @@ module Neo4j
503
571
 
504
572
  class << self
505
573
  def clause_string(clauses)
506
- clauses.map!(&:value).join(', ')
574
+ clauses.map!(&:value).join(Clause::COMMA_SPACE)
507
575
  end
508
576
  end
509
577
  end
@@ -2,8 +2,7 @@ module Neo4j
2
2
  module Core
3
3
  module QueryFindInBatches
4
4
  def find_in_batches(node_var, prop_var, options = {})
5
- invalid_keys = options.keys.map(&:to_sym) - [:batch_size]
6
- fail ArgumentError, "Invalid keys: #{invalid_keys.join(', ')}" if not invalid_keys.empty?
5
+ validate_find_in_batches_options!(options)
7
6
 
8
7
  batch_size = options.delete(:batch_size) || 1000
9
8
 
@@ -13,15 +12,7 @@ module Neo4j
13
12
 
14
13
  while records.any?
15
14
  records_size = records.size
16
- primary_key_offset = begin
17
- records.last.send(node_var).send(prop_var)
18
- rescue NoMethodError
19
- begin
20
- records.last.send(node_var)[prop_var.to_sym]
21
- rescue NoMethodError
22
- records.last.send("#{node_var}.#{prop_var}") # In case we're explicitly returning it
23
- end
24
- end
15
+ primary_key_offset = primary_key_offset(records.last, node_var, prop_var)
25
16
 
26
17
  yield records
27
18
 
@@ -36,6 +27,23 @@ module Neo4j
36
27
  batch.each { |result| yield result }
37
28
  end
38
29
  end
30
+
31
+ private
32
+
33
+ def validate_find_in_batches_options!(options)
34
+ invalid_keys = options.keys.map(&:to_sym) - [:batch_size]
35
+ fail ArgumentError, "Invalid keys: #{invalid_keys.join(', ')}" if not invalid_keys.empty?
36
+ end
37
+
38
+ def primary_key_offset(last_record, node_var, prop_var)
39
+ last_record.send(node_var).send(prop_var)
40
+ rescue NoMethodError
41
+ begin
42
+ last_record.send(node_var)[prop_var.to_sym]
43
+ rescue NoMethodError
44
+ last_record.send("#{node_var}.#{prop_var}") # In case we're explicitly returning it
45
+ end
46
+ end
39
47
  end
40
48
  end
41
49
  end
@@ -1,5 +1,5 @@
1
1
  module Neo4j
2
2
  module Core
3
- VERSION = '4.0.7'
3
+ VERSION = '5.0.0.rc.1'
4
4
  end
5
5
  end
data/lib/neo4j-core.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require 'ext/kernel'
2
+
3
+ require 'ostruct'
1
4
  require 'forwardable'
2
5
  require 'fileutils'
3
6
 
@@ -12,26 +12,31 @@ module Neo4j
12
12
  include Enumerable
13
13
 
14
14
  # @return the original result from the Neo4j Cypher Engine, once forward read only !
15
- attr_reader :source
15
+ attr_reader :source, :unwrapped
16
16
 
17
- def initialize(source, query)
17
+ def initialize(source, query, unwrapped = nil)
18
18
  @source = source
19
- @struct = Struct.new(*source.columns.to_a.map(&:to_sym))
19
+ @struct = Struct.new(*source.columns.to_a.map!(&:to_sym)) unless source.columns.empty?
20
20
  @unread = true
21
21
  @query = query
22
+ @unwrapped = unwrapped
22
23
  end
23
24
 
24
25
  def to_s
25
26
  @query
26
27
  end
27
28
 
29
+ def unwrapped?
30
+ !!unwrapped
31
+ end
32
+
28
33
  def inspect
29
34
  "Enumerable query: '#{@query}'"
30
35
  end
31
36
 
32
37
  # @return [Array<Symbol>] the columns in the query result
33
38
  def columns
34
- @source.columns.map(&:to_sym)
39
+ @source.columns.map!(&:to_sym)
35
40
  end
36
41
 
37
42
  def each
@@ -40,13 +45,23 @@ module Neo4j
40
45
  if block_given?
41
46
  @source.each do |row|
42
47
  yield(row.each_with_object(@struct.new) do |(column, value), result|
43
- result[column.to_sym] = (value.respond_to?(:wrapper) ? value.wrapper : value)
48
+ result[column.to_sym] = unwrap(value)
44
49
  end)
45
50
  end
46
51
  else
47
52
  Enumerator.new(self)
48
53
  end
49
54
  end
55
+
56
+ private
57
+
58
+ def unwrap(value)
59
+ if !value.nil? && value.respond_to?(:to_a)
60
+ value.map { |v| unwrap(v) }
61
+ else
62
+ (!value.respond_to?(:wrapper) || unwrapped?) ? value : value.wrapper
63
+ end
64
+ end
50
65
  end
51
66
  end
52
67
  end