dm-core 0.10.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/.autotest +29 -0
  2. data/.document +5 -0
  3. data/.gitignore +27 -0
  4. data/LICENSE +20 -0
  5. data/{README.txt → README.rdoc} +14 -3
  6. data/Rakefile +23 -22
  7. data/VERSION +1 -0
  8. data/dm-core.gemspec +201 -10
  9. data/lib/dm-core.rb +32 -23
  10. data/lib/dm-core/adapters.rb +0 -1
  11. data/lib/dm-core/adapters/data_objects_adapter.rb +230 -151
  12. data/lib/dm-core/adapters/mysql_adapter.rb +7 -8
  13. data/lib/dm-core/adapters/oracle_adapter.rb +39 -59
  14. data/lib/dm-core/adapters/postgres_adapter.rb +0 -1
  15. data/lib/dm-core/adapters/sqlite3_adapter.rb +5 -0
  16. data/lib/dm-core/adapters/sqlserver_adapter.rb +114 -0
  17. data/lib/dm-core/adapters/yaml_adapter.rb +0 -5
  18. data/lib/dm-core/associations/many_to_many.rb +118 -56
  19. data/lib/dm-core/associations/many_to_one.rb +48 -21
  20. data/lib/dm-core/associations/one_to_many.rb +8 -30
  21. data/lib/dm-core/associations/one_to_one.rb +1 -5
  22. data/lib/dm-core/associations/relationship.rb +89 -97
  23. data/lib/dm-core/collection.rb +299 -184
  24. data/lib/dm-core/core_ext/enumerable.rb +28 -0
  25. data/lib/dm-core/core_ext/kernel.rb +0 -2
  26. data/lib/dm-core/migrations.rb +314 -170
  27. data/lib/dm-core/model.rb +97 -66
  28. data/lib/dm-core/model/descendant_set.rb +1 -1
  29. data/lib/dm-core/model/hook.rb +0 -3
  30. data/lib/dm-core/model/property.rb +7 -10
  31. data/lib/dm-core/model/relationship.rb +79 -26
  32. data/lib/dm-core/model/scope.rb +3 -4
  33. data/lib/dm-core/property.rb +152 -90
  34. data/lib/dm-core/property_set.rb +18 -37
  35. data/lib/dm-core/query.rb +452 -153
  36. data/lib/dm-core/query/conditions/comparison.rb +266 -173
  37. data/lib/dm-core/query/conditions/operation.rb +499 -57
  38. data/lib/dm-core/query/direction.rb +0 -3
  39. data/lib/dm-core/query/operator.rb +0 -4
  40. data/lib/dm-core/query/path.rb +10 -12
  41. data/lib/dm-core/query/sort.rb +4 -10
  42. data/lib/dm-core/repository.rb +10 -6
  43. data/lib/dm-core/resource.rb +343 -148
  44. data/lib/dm-core/spec/adapter_shared_spec.rb +17 -1
  45. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +277 -17
  46. data/lib/dm-core/support/chainable.rb +0 -2
  47. data/lib/dm-core/support/equalizer.rb +27 -3
  48. data/lib/dm-core/transaction.rb +75 -75
  49. data/lib/dm-core/type.rb +19 -5
  50. data/lib/dm-core/types/discriminator.rb +4 -4
  51. data/lib/dm-core/types/object.rb +2 -7
  52. data/lib/dm-core/types/paranoid_boolean.rb +8 -2
  53. data/lib/dm-core/types/paranoid_datetime.rb +8 -2
  54. data/lib/dm-core/version.rb +1 -1
  55. data/script/performance.rb +7 -7
  56. data/script/profile.rb +6 -6
  57. data/spec/lib/collection_helpers.rb +2 -2
  58. data/spec/lib/pending_helpers.rb +22 -3
  59. data/spec/lib/rspec_immediate_feedback_formatter.rb +1 -0
  60. data/spec/public/associations/many_to_many_spec.rb +6 -4
  61. data/spec/public/associations/many_to_one_spec.rb +10 -1
  62. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +39 -0
  63. data/spec/public/associations/one_to_many_spec.rb +4 -3
  64. data/spec/public/associations/one_to_one_spec.rb +19 -1
  65. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +45 -0
  66. data/spec/public/collection_spec.rb +4 -3
  67. data/spec/public/migrations_spec.rb +144 -0
  68. data/spec/public/model/relationship_spec.rb +115 -55
  69. data/spec/public/model_spec.rb +13 -13
  70. data/spec/public/property/object_spec.rb +106 -0
  71. data/spec/public/property_spec.rb +18 -14
  72. data/spec/public/resource_spec.rb +10 -1
  73. data/spec/public/sel_spec.rb +16 -49
  74. data/spec/public/setup_spec.rb +1 -1
  75. data/spec/public/shared/association_collection_shared_spec.rb +6 -14
  76. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  77. data/spec/public/shared/collection_shared_spec.rb +214 -217
  78. data/spec/public/shared/finder_shared_spec.rb +259 -365
  79. data/spec/public/shared/resource_shared_spec.rb +524 -248
  80. data/spec/public/transaction_spec.rb +27 -3
  81. data/spec/public/types/discriminator_spec.rb +1 -1
  82. data/spec/rcov.opts +6 -0
  83. data/spec/semipublic/adapters/sqlserver_adapter_spec.rb +17 -0
  84. data/spec/semipublic/associations/many_to_one_spec.rb +3 -20
  85. data/spec/semipublic/associations_spec.rb +2 -2
  86. data/spec/semipublic/collection_spec.rb +0 -32
  87. data/spec/semipublic/model_spec.rb +96 -0
  88. data/spec/semipublic/property_spec.rb +3 -3
  89. data/spec/semipublic/query/conditions/comparison_spec.rb +1719 -0
  90. data/spec/semipublic/query/conditions/operation_spec.rb +1292 -0
  91. data/spec/semipublic/query_spec.rb +1285 -144
  92. data/spec/semipublic/resource_spec.rb +0 -24
  93. data/spec/semipublic/shared/resource_shared_spec.rb +103 -38
  94. data/spec/spec.opts +1 -1
  95. data/spec/spec_helper.rb +15 -6
  96. data/tasks/ci.rake +1 -0
  97. data/tasks/metrics.rake +37 -0
  98. data/tasks/spec.rake +41 -0
  99. data/tasks/yard.rake +9 -0
  100. data/tasks/yardstick.rake +19 -0
  101. metadata +99 -29
  102. data/CONTRIBUTING +0 -51
  103. data/FAQ +0 -93
  104. data/History.txt +0 -27
  105. data/MIT-LICENSE +0 -22
  106. data/Manifest.txt +0 -121
  107. data/QUICKLINKS +0 -11
  108. data/SPECS +0 -35
  109. data/TODO +0 -1
  110. data/spec/semipublic/query/conditions_spec.rb +0 -528
  111. data/tasks/ci.rb +0 -24
  112. data/tasks/dm.rb +0 -58
  113. data/tasks/doc.rb +0 -17
  114. data/tasks/gemspec.rb +0 -23
  115. data/tasks/hoe.rb +0 -45
  116. data/tasks/install.rb +0 -18
@@ -10,75 +10,67 @@ module DataMapper
10
10
  deprecate :slice, :values_at
11
11
  deprecate :add, :<<
12
12
 
13
- # TODO: document
14
13
  # @api semipublic
15
14
  def [](name)
16
15
  @properties[name]
17
16
  end
18
17
 
19
- alias super_slice []=
18
+ alias superclass_slice []=
19
+ private :superclass_slice
20
20
 
21
- # TODO: document
22
21
  # @api semipublic
23
22
  def []=(name, property)
24
- if named?(name)
25
- add_property(property)
26
- super_slice(index(property), property)
27
- else
28
- self << property
29
- end
23
+ self << property
30
24
  end
31
25
 
32
- # TODO: document
33
26
  # @api semipublic
34
27
  def named?(name)
35
28
  @properties.key?(name)
36
29
  end
37
30
 
38
- # TODO: document
39
31
  # @api semipublic
40
32
  def values_at(*names)
41
33
  @properties.values_at(*names)
42
34
  end
43
35
 
44
- # TODO: document
45
36
  # @api semipublic
46
37
  def <<(property)
47
- if named?(property.name)
48
- add_property(property)
49
- super_slice(index(property), property)
38
+ found = named?(property.name)
39
+ add_property(property)
40
+
41
+ if found
42
+ superclass_slice(index(property), property)
50
43
  else
51
- add_property(property)
52
44
  super
53
45
  end
54
46
  end
55
47
 
56
- # TODO: document
57
48
  # @api semipublic
58
49
  def include?(property)
59
50
  named?(property.name)
60
51
  end
61
52
 
53
+ # @api semipublic
54
+ def index(property)
55
+ each_index { |index| break index if at(index).name == property.name }
56
+ end
57
+
62
58
  # TODO: make PropertySet#reject return a PropertySet instance
63
- # TODO: document
64
59
  # @api semipublic
65
60
  def defaults
66
61
  @defaults ||= self.class.new(key | [ discriminator ].compact | reject { |property| property.lazy? }).freeze
67
62
  end
68
63
 
69
- # TODO: document
70
64
  # @api semipublic
71
65
  def key
72
66
  @key ||= self.class.new(select { |property| property.key? }).freeze
73
67
  end
74
68
 
75
- # TODO: document
76
69
  # @api semipublic
77
70
  def discriminator
78
71
  @discriminator ||= detect { |property| property.type == Types::Discriminator }
79
72
  end
80
73
 
81
- # TODO: document
82
74
  # @api semipublic
83
75
  def indexes
84
76
  index_hash = {}
@@ -86,7 +78,6 @@ module DataMapper
86
78
  index_hash
87
79
  end
88
80
 
89
- # TODO: document
90
81
  # @api semipublic
91
82
  def unique_indexes
92
83
  index_hash = {}
@@ -94,43 +85,41 @@ module DataMapper
94
85
  index_hash
95
86
  end
96
87
 
97
- # TODO: document
98
88
  # @api semipublic
99
89
  def get(resource)
100
90
  map { |property| property.get(resource) }
101
91
  end
102
92
 
103
- # TODO: document
104
93
  # @api semipublic
105
94
  def get!(resource)
106
95
  map { |property| property.get!(resource) }
107
96
  end
108
97
 
109
- # TODO: document
110
98
  # @api semipublic
111
99
  def set(resource, values)
112
100
  zip(values) { |property, value| property.set(resource, value) }
113
101
  end
114
102
 
115
- # TODO: document
116
103
  # @api semipublic
117
104
  def set!(resource, values)
118
105
  zip(values) { |property, value| property.set!(resource, value) }
119
106
  end
120
107
 
121
- # TODO: document
122
108
  # @api semipublic
123
109
  def loaded?(resource)
124
110
  all? { |property| property.loaded?(resource) }
125
111
  end
126
112
 
127
- # TODO: document
113
+ # @api semipublic
114
+ def valid?(values)
115
+ zip(values.nil? ? [] : values).all? { |property, value| property.valid?(value) }
116
+ end
117
+
128
118
  # @api semipublic
129
119
  def typecast(values)
130
120
  zip(values.nil? ? [] : values).map { |property, value| property.typecast(value) }
131
121
  end
132
122
 
133
- # TODO: document
134
123
  # @api private
135
124
  def property_contexts(property)
136
125
  contexts = []
@@ -140,13 +129,11 @@ module DataMapper
140
129
  contexts
141
130
  end
142
131
 
143
- # TODO: document
144
132
  # @api private
145
133
  def lazy_context(context)
146
134
  lazy_contexts[context] ||= []
147
135
  end
148
136
 
149
- # TODO: document
150
137
  # @api private
151
138
  def in_context(properties)
152
139
  properties_in_context = properties.map do |property|
@@ -162,40 +149,34 @@ module DataMapper
162
149
 
163
150
  private
164
151
 
165
- # TODO: document
166
152
  # @api semipublic
167
153
  def initialize(*)
168
154
  super
169
155
  @properties = map { |property| [ property.name, property ] }.to_mash
170
156
  end
171
157
 
172
- # TODO: document
173
158
  # @api private
174
159
  def initialize_copy(*)
175
160
  super
176
161
  @properties = @properties.dup
177
162
  end
178
163
 
179
- # TODO: document
180
164
  # @api private
181
165
  def add_property(property)
182
166
  clear_cache
183
167
  @properties[property.name] = property
184
168
  end
185
169
 
186
- # TODO: document
187
170
  # @api private
188
171
  def clear_cache
189
172
  @defaults, @key, @discriminator = nil
190
173
  end
191
174
 
192
- # TODO: document
193
175
  # @api private
194
176
  def lazy_contexts
195
177
  @lazy_contexts ||= {}
196
178
  end
197
179
 
198
- # TODO: document
199
180
  # @api private
200
181
  def parse_index(index, property, index_hash)
201
182
  case index
data/lib/dm-core/query.rb CHANGED
@@ -47,20 +47,23 @@ module DataMapper
47
47
  #
48
48
  # @api private
49
49
  def self.target_conditions(source, source_key, target_key)
50
- source_values = []
50
+ target_key_size = target_key.size
51
+ source_values = []
51
52
 
52
53
  if source.nil?
53
- source_values << [ nil ] * target_key.size
54
+ source_values << [ nil ] * target_key_size
54
55
  else
55
56
  Array(source).each do |resource|
56
57
  next unless source_key.loaded?(resource)
57
- source_values << source_key.get!(resource)
58
+ source_value = source_key.get!(resource)
59
+ next unless target_key.valid?(source_value)
60
+ source_values << source_value
58
61
  end
59
62
  end
60
63
 
61
64
  source_values.uniq!
62
65
 
63
- if target_key.size == 1
66
+ if target_key_size == 1
64
67
  target_key = target_key.first
65
68
  source_values.flatten!
66
69
 
@@ -86,6 +89,29 @@ module DataMapper
86
89
  end
87
90
  end
88
91
 
92
+ # @param [Repository] repository
93
+ # the default repository to scope the query within
94
+ # @param [Model] model
95
+ # the default model for the query
96
+ # @param [#query, Enumerable] source
97
+ # the source to generate the query with
98
+ #
99
+ # @return [Query]
100
+ # the query to match the resources with
101
+ #
102
+ # @api private
103
+ def self.target_query(repository, model, source)
104
+ if source.respond_to?(:query)
105
+ source.query
106
+ elsif source.kind_of?(Enumerable)
107
+ key = model.key(repository.name)
108
+ conditions = Query.target_conditions(source, key, key)
109
+ Query.new(repository, model, :conditions => conditions)
110
+ else
111
+ raise ArgumentError, "+source+ must respond to #query or be an Enumerable, but was #{source.class}"
112
+ end
113
+ end
114
+
89
115
  # Returns the repository query should be
90
116
  # executed in
91
117
  #
@@ -173,7 +199,7 @@ module DataMapper
173
199
  #
174
200
  # Document.all(:limit => 10)
175
201
  #
176
- # @return [Integer, NilClass]
202
+ # @return [Integer, nil]
177
203
  # the maximum number of results
178
204
  #
179
205
  # @api semipublic
@@ -325,24 +351,26 @@ module DataMapper
325
351
  def update(other)
326
352
  assert_kind_of 'other', other, self.class, Hash
327
353
 
328
- other_options = if other.kind_of? self.class
329
- if self.eql?(other)
330
- return self
331
- end
354
+ other_options = if kind_of?(other.class)
355
+ return self if self.eql?(other)
332
356
  assert_valid_other(other)
333
357
  other.options
334
358
  else
359
+ return self if other.empty?
335
360
  other
336
361
  end
337
362
 
338
- unless other_options.empty?
339
- options = @options.merge(other_options)
340
- if @options[:conditions] and other_options[:conditions]
341
- options[:conditions] = @options[:conditions].dup << other_options[:conditions]
342
- end
343
- initialize(repository, model, options)
363
+ @options = @options.merge(other_options).freeze
364
+ assert_valid_options(@options)
365
+
366
+ normalize = other_options.only(*OPTIONS - [ :conditions ]).map do |attribute, value|
367
+ instance_variable_set("@#{attribute}", value.try_dup)
368
+ attribute
344
369
  end
345
370
 
371
+ merge_conditions([ other_options.except(*OPTIONS), other_options[:conditions] ])
372
+ normalize_options(normalize | [ :links, :unique ])
373
+
346
374
  self
347
375
  end
348
376
 
@@ -375,22 +403,76 @@ module DataMapper
375
403
  def relative(options)
376
404
  assert_kind_of 'options', options, Hash
377
405
 
378
- options = options.dup
379
-
380
- repository = options.delete(:repository) || self.repository
406
+ offset = nil
407
+ limit = self.limit
381
408
 
382
- if repository.kind_of?(Symbol)
383
- repository = DataMapper.repository(repository)
409
+ if options.key?(:offset) && (options.key?(:limit) || limit)
410
+ options = options.dup
411
+ offset = options.delete(:offset)
412
+ limit = options.delete(:limit) || limit - offset
384
413
  end
385
414
 
386
- if options.key?(:offset) && (options.key?(:limit) || self.limit)
387
- offset = options.delete(:offset)
388
- limit = options.delete(:limit) || self.limit - offset
415
+ query = merge(options)
416
+ query = query.slice!(offset, limit) if offset
417
+ query
418
+ end
389
419
 
390
- self.class.new(repository, model, @options.merge(options)).slice!(offset, limit)
391
- else
392
- self.class.new(repository, model, @options.merge(options))
393
- end
420
+ # Return the union with another query
421
+ #
422
+ # @param [Query] other
423
+ # the other query
424
+ #
425
+ # @return [Query]
426
+ # the union of the query and other
427
+ #
428
+ # @api semipublic
429
+ def union(other)
430
+ return dup if self == other
431
+ set_operation(:union, other)
432
+ end
433
+
434
+ alias | union
435
+ alias + union
436
+
437
+ # Return the intersection with another query
438
+ #
439
+ # @param [Query] other
440
+ # the other query
441
+ #
442
+ # @return [Query]
443
+ # the intersection of the query and other
444
+ #
445
+ # @api semipublic
446
+ def intersection(other)
447
+ return dup if self == other
448
+ set_operation(:intersection, other)
449
+ end
450
+
451
+ alias & intersection
452
+
453
+ # Return the difference with another query
454
+ #
455
+ # @param [Query] other
456
+ # the other query
457
+ #
458
+ # @return [Query]
459
+ # the difference of the query and other
460
+ #
461
+ # @api semipublic
462
+ def difference(other)
463
+ set_operation(:difference, other)
464
+ end
465
+
466
+ alias - difference
467
+
468
+ # Clear conditions
469
+ #
470
+ # @return [self]
471
+ #
472
+ # @api semipublic
473
+ def clear
474
+ @conditions = Conditions::Operation.new(:null)
475
+ self
394
476
  end
395
477
 
396
478
  # Takes an Enumerable of records, and destructively filters it.
@@ -422,10 +504,9 @@ module DataMapper
422
504
  #
423
505
  # @api semipublic
424
506
  def match_records(records)
507
+ conditions = self.conditions
425
508
  return records if conditions.nil?
426
- records.select do |record|
427
- conditions.matches?(record)
428
- end
509
+ records.select { |record| conditions.matches?(record) }
429
510
  end
430
511
 
431
512
  # Sorts a list of Records by the order
@@ -457,7 +538,9 @@ module DataMapper
457
538
  #
458
539
  # @api semipublic
459
540
  def limit_records(records)
460
- size = records.size
541
+ offset = self.offset
542
+ limit = self.limit
543
+ size = records.size
461
544
 
462
545
  if offset > size - 1
463
546
  []
@@ -547,7 +630,9 @@ module DataMapper
547
630
  properties = Set.new
548
631
 
549
632
  each_comparison do |comparison|
550
- properties << comparison.subject if comparison.subject.kind_of?(Property)
633
+ next unless comparison.respond_to?(:subject)
634
+ subject = comparison.subject
635
+ properties << subject if subject.kind_of?(Property)
551
636
  end
552
637
 
553
638
  properties
@@ -563,6 +648,52 @@ module DataMapper
563
648
  fields.sort_by { |property| property.hash }
564
649
  end
565
650
 
651
+ # Transform Query into subquery conditions
652
+ #
653
+ # @return [AndOperation]
654
+ # a subquery for the Query
655
+ #
656
+ # @api private
657
+ def to_subquery
658
+ collection = model.all(merge(:fields => model_key))
659
+ Conditions::Operation.new(:and, Conditions::Comparison.new(:in, self_relationship, collection))
660
+ end
661
+
662
+ # Hash representation of a Query
663
+ #
664
+ # @return [Hash]
665
+ # Hash representation of a Query
666
+ #
667
+ # @api private
668
+ def to_hash
669
+ {
670
+ :repository => repository.name,
671
+ :model => model.name,
672
+ :fields => fields,
673
+ :links => links,
674
+ :conditions => conditions,
675
+ :offset => offset,
676
+ :limit => limit,
677
+ :order => order,
678
+ :unique => unique?,
679
+ :add_reversed => add_reversed?,
680
+ :reload => reload?,
681
+ }
682
+ end
683
+
684
+ # Extract options from a Query
685
+ #
686
+ # @param [Query] query
687
+ # the query to extract options from
688
+ #
689
+ # @return [Hash]
690
+ # the options to use to initialize the new query
691
+ #
692
+ # @api private
693
+ def to_relative_hash
694
+ to_hash.only(:fields, :order, :unique, :add_reversed, :reload)
695
+ end
696
+
566
697
  private
567
698
 
568
699
  # Initializes a Query instance
@@ -598,7 +729,7 @@ module DataMapper
598
729
  assert_valid_options(@options)
599
730
 
600
731
  @fields = @options.fetch :fields, @properties.defaults
601
- @links = @options.fetch :links, []
732
+ @links = @options.key?(:links) ? @options[:links].dup : []
602
733
  @conditions = Conditions::Operation.new(:null)
603
734
  @offset = @options.fetch :offset, 0
604
735
  @limit = @options.fetch :limit, nil
@@ -608,35 +739,18 @@ module DataMapper
608
739
  @reload = @options.fetch :reload, false
609
740
  @raw = false
610
741
 
611
- @links = @links.dup
612
-
613
- # treat all non-options as conditions
614
- @options.except(*OPTIONS).each { |kv| append_condition(*kv) }
615
-
616
- # parse @options[:conditions] differently
617
- case conditions = @options[:conditions]
618
- when Conditions::AbstractOperation, Conditions::AbstractComparison
619
- add_condition(conditions)
620
-
621
- when Hash
622
- conditions.each { |kv| append_condition(*kv) }
623
-
624
- when Array
625
- statement, *bind_values = *conditions
626
- add_condition([ statement, bind_values ])
627
- @raw = true
628
- end
629
-
630
- normalize_order
631
- normalize_fields
632
- normalize_links
742
+ merge_conditions([ @options.except(*OPTIONS), @options[:conditions] ])
743
+ normalize_options
633
744
  end
634
745
 
635
746
  # Copying contructor, called for Query#dup
636
747
  #
637
748
  # @api semipublic
638
- def initialize_copy(original)
639
- initialize(original.repository, original.model, original.options)
749
+ def initialize_copy(*)
750
+ @fields = @fields.dup
751
+ @links = @links.dup
752
+ @conditions = @conditions.dup
753
+ @order = @order.try_dup
640
754
  end
641
755
 
642
756
  # Validate the options
@@ -673,15 +787,15 @@ module DataMapper
673
787
  def assert_valid_fields(fields, unique)
674
788
  assert_kind_of 'options[:fields]', fields, Array
675
789
 
676
- if fields.empty? && unique == false
677
- raise ArgumentError, '+options[:fields]+ should not be empty if +options[:unique]+ is false'
678
- end
790
+ model = self.model
679
791
 
680
792
  fields.each do |field|
793
+ inspect = field.inspect
794
+
681
795
  case field
682
796
  when Symbol, String
683
797
  unless @properties.named?(field)
684
- raise ArgumentError, "+options[:fields]+ entry #{field.inspect} does not map to a property in #{model}"
798
+ raise ArgumentError, "+options[:fields]+ entry #{inspect} does not map to a property in #{model}"
685
799
  end
686
800
 
687
801
  when Property
@@ -690,7 +804,7 @@ module DataMapper
690
804
  end
691
805
 
692
806
  else
693
- raise ArgumentError, "+options[:fields]+ entry #{field.inspect} of an unsupported object #{field.class}"
807
+ raise ArgumentError, "+options[:fields]+ entry #{inspect} of an unsupported object #{field.class}"
694
808
  end
695
809
  end
696
810
  end
@@ -707,10 +821,12 @@ module DataMapper
707
821
  end
708
822
 
709
823
  links.each do |link|
824
+ inspect = link.inspect
825
+
710
826
  case link
711
827
  when Symbol, String
712
828
  unless @relationships.key?(link.to_sym)
713
- raise ArgumentError, "+options[:links]+ entry #{link.inspect} does not map to a relationship in #{model}"
829
+ raise ArgumentError, "+options[:links]+ entry #{inspect} does not map to a relationship in #{model}"
714
830
  end
715
831
 
716
832
  when Associations::Relationship
@@ -720,7 +836,7 @@ module DataMapper
720
836
  #end
721
837
 
722
838
  else
723
- raise ArgumentError, "+options[:links]+ entry #{link.inspect} of an unsupported object #{link.class}"
839
+ raise ArgumentError, "+options[:links]+ entry #{inspect} of an unsupported object #{link.class}"
724
840
  end
725
841
  end
726
842
  end
@@ -735,23 +851,23 @@ module DataMapper
735
851
  case conditions
736
852
  when Hash
737
853
  conditions.each do |subject, bind_value|
854
+ inspect = subject.inspect
855
+
738
856
  case subject
739
857
  when Symbol, String
740
858
  unless subject.to_s.include?('.') || @properties.named?(subject) || @relationships.key?(subject)
741
- raise ArgumentError, "condition #{subject.inspect} does not map to a property or relationship in #{model}"
859
+ raise ArgumentError, "condition #{inspect} does not map to a property or relationship in #{model}"
742
860
  end
743
861
 
744
862
  when Operator
745
- unless (Conditions::Comparison.slugs | [ :not ]).include?(subject.operator)
746
- raise ArgumentError, "condition #{subject.inspect} used an invalid operator #{subject.operator}"
863
+ operator = subject.operator
864
+
865
+ unless (Conditions::Comparison.slugs | [ :not ]).include?(operator)
866
+ raise ArgumentError, "condition #{inspect} used an invalid operator #{operator}"
747
867
  end
748
868
 
749
869
  assert_valid_conditions(subject.target => bind_value)
750
870
 
751
- if subject.operator == :not && bind_value.kind_of?(Array) && bind_value.empty?
752
- raise ArgumentError, "Cannot use 'not' operator with a bind value that is an empty Array for #{subject.inspect}"
753
- end
754
-
755
871
  when Path
756
872
  assert_valid_links(subject.relationships)
757
873
 
@@ -763,7 +879,7 @@ module DataMapper
763
879
  #end
764
880
 
765
881
  else
766
- raise ArgumentError, "condition #{subject.inspect} of an unsupported object #{subject.class}"
882
+ raise ArgumentError, "condition #{inspect} of an unsupported object #{subject.class}"
767
883
  end
768
884
  end
769
885
 
@@ -772,7 +888,9 @@ module DataMapper
772
888
  raise ArgumentError, '+options[:conditions]+ should not be empty'
773
889
  end
774
890
 
775
- unless conditions.first.kind_of?(String) && !conditions.first.blank?
891
+ first_condition = conditions.first
892
+
893
+ unless first_condition.kind_of?(String) && !first_condition.blank?
776
894
  raise ArgumentError, '+options[:conditions]+ should have a statement for the first entry'
777
895
  end
778
896
  end
@@ -813,17 +931,20 @@ module DataMapper
813
931
  def assert_valid_order(order, fields)
814
932
  return if order.nil?
815
933
 
816
- assert_kind_of 'options[:order]', order, Array
817
-
934
+ order = Array(order)
818
935
  if order.empty? && fields && fields.any? { |property| !property.kind_of?(Operator) }
819
936
  raise ArgumentError, '+options[:order]+ should not be empty if +options[:fields] contains a non-operator'
820
937
  end
821
938
 
939
+ model = self.model
940
+
822
941
  order.each do |order_entry|
942
+ inspect = order_entry.inspect
943
+
823
944
  case order_entry
824
945
  when Symbol, String
825
946
  unless @properties.named?(order_entry)
826
- raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} does not map to a property in #{model}"
947
+ raise ArgumentError, "+options[:order]+ entry #{inspect} does not map to a property in #{model}"
827
948
  end
828
949
 
829
950
  when Property
@@ -832,14 +953,16 @@ module DataMapper
832
953
  end
833
954
 
834
955
  when Operator, Direction
835
- unless order_entry.operator == :asc || order_entry.operator == :desc
836
- raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} used an invalid operator #{order_entry.operator}"
956
+ operator = order_entry.operator
957
+
958
+ unless operator == :asc || operator == :desc
959
+ raise ArgumentError, "+options[:order]+ entry #{inspect} used an invalid operator #{operator}"
837
960
  end
838
961
 
839
962
  assert_valid_order([ order_entry.target ], fields)
840
963
 
841
964
  else
842
- raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} of an unsupported object #{order_entry.class}"
965
+ raise ArgumentError, "+options[:order]+ entry #{inspect} of an unsupported object #{order_entry.class}"
843
966
  end
844
967
  end
845
968
  end
@@ -857,18 +980,67 @@ module DataMapper
857
980
  #
858
981
  # @api private
859
982
  def assert_valid_other(other)
860
- unless other.repository == repository
861
- raise ArgumentError, "+other+ #{other.class} must be for the #{repository.name} repository, not #{other.repository.name}"
983
+ other_repository = other.repository
984
+ repository = self.repository
985
+ other_class = other.class
986
+
987
+ unless other_repository == repository
988
+ raise ArgumentError, "+other+ #{other_class} must be for the #{repository.name} repository, not #{other_repository.name}"
862
989
  end
863
990
 
864
- unless other.model >= model
865
- raise ArgumentError, "+other+ #{other.class} must be for the #{model.name} model, not #{other.model.name}"
991
+ other_model = other.model
992
+ model = self.model
993
+
994
+ unless other_model >= model
995
+ raise ArgumentError, "+other+ #{other_class} must be for the #{model.name} model, not #{other_model.name}"
866
996
  end
867
997
  end
868
998
 
869
- # Normalize order elements to Query::Direction instances
999
+ # Handle all the conditions options provided
870
1000
  #
871
- # TODO: needs example
1001
+ # @param [Array<Conditions::AbstractOperation, Conditions::AbstractComparison, Hash, Array>]
1002
+ # a list of conditions
1003
+ #
1004
+ # @return [undefined]
1005
+ #
1006
+ # @api private
1007
+ def merge_conditions(conditions)
1008
+ @conditions = Conditions::Operation.new(:and) << @conditions unless @conditions.nil?
1009
+
1010
+ conditions.compact!
1011
+ conditions.each do |condition|
1012
+ case condition
1013
+ when Conditions::AbstractOperation, Conditions::AbstractComparison
1014
+ add_condition(condition)
1015
+
1016
+ when Hash
1017
+ condition.each { |kv| append_condition(*kv) }
1018
+
1019
+ when Array
1020
+ statement, *bind_values = *condition
1021
+ raw_condition = [ statement ]
1022
+ raw_condition << bind_values if bind_values.size > 0
1023
+ add_condition(raw_condition)
1024
+ @raw = true
1025
+ end
1026
+ end
1027
+ end
1028
+
1029
+ # Normalize options
1030
+ #
1031
+ # @param [Array<Symbol>] options
1032
+ # the options to normalize
1033
+ #
1034
+ # @return [undefined]
1035
+ #
1036
+ # @api private
1037
+ def normalize_options(options = OPTIONS)
1038
+ (options & [ :order, :fields, :links, :unique ]).each do |option|
1039
+ send("normalize_#{option}")
1040
+ end
1041
+ end
1042
+
1043
+ # Normalize order elements to Query::Direction instances
872
1044
  #
873
1045
  # @api private
874
1046
  def normalize_order
@@ -876,6 +1048,7 @@ module DataMapper
876
1048
 
877
1049
  # TODO: should Query::Path objects be permitted? If so, then it
878
1050
  # should probably be normalized to a Direction object
1051
+ @order = Array(@order)
879
1052
  @order = @order.map do |order|
880
1053
  case order
881
1054
  when Operator
@@ -898,8 +1071,6 @@ module DataMapper
898
1071
 
899
1072
  # Normalize fields to Property instances
900
1073
  #
901
- # TODO: needs example
902
- #
903
1074
  # @api private
904
1075
  def normalize_fields
905
1076
  @fields = @fields.map do |field|
@@ -921,21 +1092,19 @@ module DataMapper
921
1092
  #
922
1093
  # @api private
923
1094
  def normalize_links
924
- links = @links.dup
1095
+ stack = @links.dup
925
1096
 
926
1097
  @links.clear
927
1098
 
928
- while link = links.pop
1099
+ while link = stack.pop
929
1100
  relationship = case link
930
1101
  when Symbol, String then @relationships[link]
931
1102
  when Associations::Relationship then link
932
1103
  end
933
1104
 
934
- next if @links.include?(relationship)
935
-
936
1105
  if relationship.respond_to?(:links)
937
- links.concat(relationship.links)
938
- else
1106
+ stack.concat(relationship.links)
1107
+ elsif !@links.include?(relationship)
939
1108
  repository_name = relationship.relative_target_repository_name
940
1109
  model = relationship.target_model
941
1110
 
@@ -961,9 +1130,20 @@ module DataMapper
961
1130
  @links << relationship
962
1131
  end
963
1132
  end
1133
+
964
1134
  @links.reverse!
965
1135
  end
966
1136
 
1137
+ # Normalize the unique attribute
1138
+ #
1139
+ # If any links are present, and the unique attribute was not
1140
+ # explicitly specified, then make sure the query is marked as unique
1141
+ #
1142
+ # @api private
1143
+ def normalize_unique
1144
+ @unique = @links.any? unless @options.key?(:unique)
1145
+ end
1146
+
967
1147
  # Append conditions to this Query
968
1148
  #
969
1149
  # TODO: needs example
@@ -991,21 +1171,21 @@ module DataMapper
991
1171
  end
992
1172
  end
993
1173
 
994
- # TODO: document
995
1174
  # @api private
996
- def append_property_condition(property, bind_value, operator)
997
- bind_value = normalize_bind_value(property, bind_value)
998
- negated = operator == :not
1175
+ def append_property_condition(subject, bind_value, operator)
1176
+ negated = operator == :not
999
1177
 
1000
1178
  if operator == :eql || negated
1001
- operator = case bind_value
1002
- when Array, Range, Set, Collection then :in
1003
- when Regexp then :regexp
1004
- else :eql
1179
+ # transform :relationship => nil into :relationship.not => association
1180
+ if subject.respond_to?(:collection_for) && bind_value.nil?
1181
+ negated = !negated
1182
+ bind_value = collection_for_nil(subject)
1005
1183
  end
1184
+
1185
+ operator = equality_operator_for_type(bind_value)
1006
1186
  end
1007
1187
 
1008
- condition = Conditions::Comparison.new(operator, property, bind_value)
1188
+ condition = Conditions::Comparison.new(operator, subject, bind_value)
1009
1189
 
1010
1190
  if negated
1011
1191
  condition = Conditions::Operation.new(:not, condition)
@@ -1014,20 +1194,39 @@ module DataMapper
1014
1194
  add_condition(condition)
1015
1195
  end
1016
1196
 
1017
- # TODO: document
1197
+ if RUBY_VERSION >= '1.9'
1198
+ def equality_operator_for_type(bind_value)
1199
+ case bind_value
1200
+ when Enumerable then :in
1201
+ when Regexp then :regexp
1202
+ else :eql
1203
+ end
1204
+ end
1205
+ else
1206
+ def equality_operator_for_type(bind_value)
1207
+ case bind_value
1208
+ when String then :eql
1209
+ when Enumerable then :in
1210
+ when Regexp then :regexp
1211
+ else :eql
1212
+ end
1213
+ end
1214
+ end
1215
+
1018
1216
  # @api private
1019
1217
  def append_symbol_condition(symbol, bind_value, model, operator)
1020
1218
  append_condition(symbol.to_s, bind_value, model, operator)
1021
1219
  end
1022
1220
 
1023
- # TODO: document
1024
1221
  # @api private
1025
1222
  def append_string_condition(string, bind_value, model, operator)
1026
1223
  if string.include?('.')
1027
1224
  query_path = model
1028
1225
 
1029
1226
  target_components = string.split('.')
1030
- operator = target_components.pop.to_sym if DataMapper::Query::Conditions::Comparison.slugs.map{ |s| s.to_s }.include? target_components.last
1227
+ last_component = target_components.last
1228
+ operator = target_components.pop.to_sym if DataMapper::Query::Conditions::Comparison.slugs.any? { |slug| slug.to_s == last_component }
1229
+
1031
1230
  target_components.each { |method| query_path = query_path.send(method) }
1032
1231
 
1033
1232
  append_condition(query_path, bind_value, model, operator)
@@ -1040,16 +1239,18 @@ module DataMapper
1040
1239
  end
1041
1240
  end
1042
1241
 
1043
- # TODO: document
1044
1242
  # @api private
1045
1243
  def append_operator_conditions(operator, bind_value, model)
1046
1244
  append_condition(operator.target, bind_value, model, operator.operator)
1047
1245
  end
1048
1246
 
1049
- # TODO: document
1050
1247
  # @api private
1051
1248
  def append_path(path, bind_value, model, operator)
1052
- @links.unshift(*path.relationships.reverse.map { |relationship| relationship.inverse })
1249
+ path.relationships.each do |relationship|
1250
+ inverse = relationship.inverse
1251
+ @links.unshift(inverse) unless @links.include?(inverse)
1252
+ end
1253
+
1053
1254
  append_condition(path.property, bind_value, path.model, operator)
1054
1255
  end
1055
1256
 
@@ -1066,65 +1267,59 @@ module DataMapper
1066
1267
  @conditions << condition
1067
1268
  end
1068
1269
 
1069
- # TODO: make this typecast all bind values that do not match the
1070
- # property primitive
1071
-
1072
- # TODO: document
1073
- # @api private
1074
- def normalize_bind_value(property_or_path, bind_value)
1075
- # TODO: defer this inside the comparison
1076
- if bind_value.respond_to?(:call)
1077
- bind_value = bind_value.call
1078
- end
1079
-
1080
- # TODO: bypass this for Collection, once subqueries can be handled by adapters
1081
- if bind_value.respond_to?(:to_ary)
1082
- bind_value = bind_value.to_ary
1083
- bind_value.uniq!
1084
- end
1085
-
1086
- # FIXME: causes m:m specs to fail with in-memory adapter
1087
- # if bind_value.instance_of?(Array) && bind_value.size == 1
1088
- # bind_value = bind_value.first
1089
- # end
1090
-
1091
- bind_value
1092
- end
1093
-
1094
1270
  # Extract arguments for #slice and #slice! then return offset and limit
1095
1271
  #
1096
1272
  # @param [Integer, Array(Integer), Range] *args the offset,
1097
1273
  # offset and limit, or range indicating first and last position
1098
1274
  #
1099
1275
  # @return [Integer] the offset
1100
- # @return [Integer, NilClass] the limit, if any
1276
+ # @return [Integer, nil] the limit, if any
1101
1277
  #
1102
1278
  # @api private
1103
1279
  def extract_slice_arguments(*args)
1104
- first_arg, second_arg = args
1105
-
1106
- if args.size == 2 && first_arg.kind_of?(Integer) && second_arg.kind_of?(Integer)
1107
- return first_arg, second_arg
1108
- elsif args.size == 1
1109
- if first_arg.kind_of?(Integer)
1110
- return first_arg, 1
1111
- elsif first_arg.kind_of?(Range)
1112
- offset = first_arg.first
1113
- limit = first_arg.last - offset
1114
- limit += 1 unless first_arg.exclude_end?
1115
- return offset, limit
1116
- end
1280
+ offset, limit = case args.size
1281
+ when 2 then extract_offset_limit_from_two_arguments(*args)
1282
+ when 1 then extract_offset_limit_from_one_argument(*args)
1117
1283
  end
1118
1284
 
1285
+ return offset, limit if offset && limit
1286
+
1119
1287
  raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}"
1120
1288
  end
1121
1289
 
1122
- # TODO: document
1290
+ # @api private
1291
+ def extract_offset_limit_from_two_arguments(*args)
1292
+ args if args.all? { |arg| arg.kind_of?(Integer) }
1293
+ end
1294
+
1295
+ # @api private
1296
+ def extract_offset_limit_from_one_argument(arg)
1297
+ case arg
1298
+ when Integer then extract_offset_limit_from_integer(arg)
1299
+ when Range then extract_offset_limit_from_range(arg)
1300
+ end
1301
+ end
1302
+
1303
+ # @api private
1304
+ def extract_offset_limit_from_integer(integer)
1305
+ [ integer, 1 ]
1306
+ end
1307
+
1308
+ # @api private
1309
+ def extract_offset_limit_from_range(range)
1310
+ offset = range.first
1311
+ limit = range.last - offset
1312
+ limit = limit.succ unless range.exclude_end?
1313
+ return offset, limit
1314
+ end
1315
+
1123
1316
  # @api private
1124
1317
  def get_relative_position(offset, limit)
1125
- new_offset = self.offset + offset
1318
+ self_offset = self.offset
1319
+ self_limit = self.limit
1320
+ new_offset = self_offset + offset
1126
1321
 
1127
- if limit <= 0 || (self.limit && new_offset + limit > self.offset + self.limit)
1322
+ if limit <= 0 || (self_limit && new_offset + limit > self_offset + self_limit)
1128
1323
  raise RangeError, "offset #{offset} and limit #{limit} are outside allowed range"
1129
1324
  end
1130
1325
 
@@ -1142,18 +1337,122 @@ module DataMapper
1142
1337
  end
1143
1338
  end
1144
1339
 
1145
- # TODO: document
1340
+ # @api private
1341
+ def collection_for_nil(relationship)
1342
+ query = relationship.query.dup
1343
+
1344
+ relationship.target_key.each do |target_key|
1345
+ query[target_key.name.not] = nil if target_key.allow_nil?
1346
+ end
1347
+
1348
+ relationship.target_model.all(query)
1349
+ end
1350
+
1146
1351
  # @api private
1147
1352
  def each_comparison
1148
- operands = conditions.operands.dup
1353
+ operands = conditions.operands.to_a
1149
1354
 
1150
1355
  while operand = operands.shift
1151
1356
  if operand.respond_to?(:operands)
1152
- operands.concat(operand.operands)
1357
+ operands.unshift(*operand.operands)
1153
1358
  else
1154
1359
  yield operand
1155
1360
  end
1156
1361
  end
1157
1362
  end
1363
+
1364
+ # Apply a set operation on self and another query
1365
+ #
1366
+ # @param [Symbol] operation
1367
+ # the set operation to apply
1368
+ # @param [Query] other
1369
+ # the other query to apply the set operation on
1370
+ #
1371
+ # @return [Query]
1372
+ # the query that was created for the set operation
1373
+ #
1374
+ # @api private
1375
+ def set_operation(operation, other)
1376
+ assert_valid_other(other)
1377
+ query = self.class.new(@repository, @model, other.to_relative_hash)
1378
+ query.instance_variable_set(:@conditions, other_conditions(other, operation))
1379
+ query
1380
+ end
1381
+
1382
+ # Return the union with another query's conditions
1383
+ #
1384
+ # @param [Query] other
1385
+ # the query conditions to union with
1386
+ #
1387
+ # @return [OrOperation]
1388
+ # the union of the query conditions and other conditions
1389
+ #
1390
+ # @api private
1391
+ def other_conditions(other, operation)
1392
+ query_conditions(self).send(operation, query_conditions(other))
1393
+ end
1394
+
1395
+ # Extract conditions from a Query
1396
+ #
1397
+ # @param [Query] query
1398
+ # the query with conditions
1399
+ #
1400
+ # @return [AbstractOperation]
1401
+ # the operation
1402
+ #
1403
+ # @api private
1404
+ def query_conditions(query)
1405
+ if query.limit || query.links.any?
1406
+ query.to_subquery
1407
+ else
1408
+ query.conditions
1409
+ end
1410
+ end
1411
+
1412
+ # Return a self referrential relationship
1413
+ #
1414
+ # @return [Associations::OneToMany::Relationship]
1415
+ # the 1:m association to the same model
1416
+ #
1417
+ # @api private
1418
+ def self_relationship
1419
+ @self_relationship ||=
1420
+ begin
1421
+ model = self.model
1422
+ Associations::OneToMany::Relationship.new(
1423
+ :self,
1424
+ model,
1425
+ model,
1426
+ self_relationship_options
1427
+ )
1428
+ end
1429
+ end
1430
+
1431
+ # Return options for the self referrential relationship
1432
+ #
1433
+ # @return [Hash]
1434
+ # the options to use with the self referrential relationship
1435
+ #
1436
+ # @api private
1437
+ def self_relationship_options
1438
+ keys = model_key.map { |property| property.name }
1439
+ repository = self.repository
1440
+ {
1441
+ :child_key => keys,
1442
+ :parent_key => keys,
1443
+ :child_repository_name => repository,
1444
+ :parent_repository_name => repository,
1445
+ }
1446
+ end
1447
+
1448
+ # Return the model key
1449
+ #
1450
+ # @return [PropertySet]
1451
+ # the model key
1452
+ #
1453
+ # @api private
1454
+ def model_key
1455
+ @properties.key
1456
+ end
1158
1457
  end # class Query
1159
1458
  end # module DataMapper