duck_record 0.0.20 → 0.0.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ module DuckRecord::Associations
2
+ module ForeignAssociation # :nodoc:
3
+ def foreign_key_present?
4
+ if reflection.klass.primary_key
5
+ owner.attribute_present?(reflection.active_record_primary_key)
6
+ else
7
+ false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module DuckRecord
2
+ # = Active Record Has Many Association
3
+ module Associations
4
+ # This is the proxy that handles a has many association.
5
+ #
6
+ # If the association has a <tt>:through</tt> option further specialization
7
+ # is provided by its child HasManyThroughAssociation.
8
+ class HasManyAssociation < CollectionAssociation #:nodoc:
9
+ include ForeignAssociation
10
+
11
+ def insert_record(record, validate = true, raise = false)
12
+ set_owner_attributes(record)
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ module DuckRecord
2
+ # = Active Record Has One Association
3
+ module Associations
4
+ class HasOneAssociation < SingularAssociation #:nodoc:
5
+ include ForeignAssociation
6
+
7
+ def replace(record)
8
+ if owner.class.readonly_attributes.include?(reflection.foreign_key.to_s)
9
+ return
10
+ end
11
+
12
+ raise_on_type_mismatch!(record) if record
13
+ load_target
14
+
15
+ return target unless target || record
16
+
17
+ self.target = record
18
+ end
19
+
20
+ private
21
+
22
+ def foreign_key_present?
23
+ true
24
+ end
25
+
26
+ # The reason that the save param for replace is false, if for create (not just build),
27
+ # is because the setting of the foreign keys is actually handled by the scoping when
28
+ # the record is instantiated, and so they are set straight away and do not need to be
29
+ # updated within replace.
30
+ def set_new_record(record)
31
+ replace(record)
32
+ end
33
+
34
+ def nullify_owner_attributes(record)
35
+ record[reflection.foreign_key] = nil
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ module DuckRecord
2
+ module Associations
3
+ class SingularAssociation < Association #:nodoc:
4
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
5
+ def reader
6
+ if !loaded? || stale_target?
7
+ reload
8
+ end
9
+
10
+ target
11
+ end
12
+
13
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
14
+ def writer(record)
15
+ replace(record)
16
+ end
17
+
18
+ def build(attributes = {})
19
+ record = build_record(attributes)
20
+ yield(record) if block_given?
21
+ set_new_record(record)
22
+ record
23
+ end
24
+
25
+ # Implements the reload reader method, e.g. foo.reload_bar for
26
+ # Foo.has_one :bar
27
+ def force_reload_reader
28
+ klass.uncached { reload }
29
+ target
30
+ end
31
+
32
+ private
33
+
34
+ def create_scope
35
+ scope.scope_for_create.stringify_keys.except(klass.primary_key)
36
+ end
37
+
38
+ def find_target
39
+ return scope.take if skip_statement_cache?
40
+
41
+ conn = klass.connection
42
+ sc = reflection.association_scope_cache(conn, owner) do
43
+ ActiveRecord::StatementCache.create(conn) { |params|
44
+ as = ActiveRecord::Associations::AssociationScope.create { params.bind }
45
+ target_scope.merge(as.scope(self, conn)).limit(1)
46
+ }
47
+ end
48
+
49
+ binds = ActiveRecord::Associations::AssociationScope.get_bind_values(owner, reflection.chain)
50
+ sc.execute(binds, klass, conn).first
51
+ rescue ::RangeError
52
+ nil
53
+ end
54
+
55
+ def replace(record)
56
+ raise NotImplementedError, "Subclasses must implement a replace(record) method"
57
+ end
58
+
59
+ def set_new_record(record)
60
+ replace(record)
61
+ end
62
+
63
+ def _create_record(attributes, raise_error = false)
64
+ record = build_record(attributes)
65
+ yield(record) if block_given?
66
+ saved = record.save
67
+ set_new_record(record)
68
+ raise ActiveRecord::RecordInvalid.new(record) if !saved && raise_error
69
+ record
70
+ end
71
+ end
72
+ end
73
+ end
@@ -139,7 +139,7 @@ module DuckRecord
139
139
  end
140
140
 
141
141
  included do
142
- Associations::Builder::EmbedsAssociation.extensions << AssociationBuilderExtension
142
+ Associations::Builder::Association.extensions << AssociationBuilderExtension
143
143
  mattr_accessor :index_nested_attribute_errors, instance_writer: false
144
144
  self.index_nested_attribute_errors = false
145
145
  end
@@ -12,18 +12,24 @@ module DuckRecord
12
12
  self._reflections = {}
13
13
  end
14
14
 
15
- def self.create(macro, name, options, ar)
15
+ def self.create(macro, name, scope, options, ar)
16
16
  klass = \
17
17
  case macro
18
18
  when :embeds_many
19
19
  EmbedsManyReflection
20
20
  when :embeds_one
21
21
  EmbedsOneReflection
22
+ when :belongs_to
23
+ BelongsToReflection
24
+ when :has_many
25
+ HasManyReflection
26
+ when :has_one
27
+ HasOneReflection
22
28
  else
23
29
  raise "Unsupported Macro: #{macro}"
24
30
  end
25
31
 
26
- klass.new(name, options, ar)
32
+ klass.new(name, scope, options, ar)
27
33
  end
28
34
 
29
35
  def self.add_reflection(ar, name, reflection)
@@ -110,7 +116,7 @@ module DuckRecord
110
116
  # ThroughReflection
111
117
  # PolymorphicReflection
112
118
  # RuntimeReflection
113
- class AbstractReflection # :nodoc:
119
+ class AbstractReflection
114
120
  # Returns a new, unsaved instance of the associated class. +attributes+ will
115
121
  # be passed to the class's constructor.
116
122
  def build_association(attributes, &block)
@@ -143,6 +149,8 @@ module DuckRecord
143
149
  # <tt>has_many :clients</tt> returns <tt>:clients</tt>
144
150
  attr_reader :name
145
151
 
152
+ attr_reader :scope
153
+
146
154
  # Returns the hash of options used for the macro.
147
155
  #
148
156
  # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
@@ -153,8 +161,9 @@ module DuckRecord
153
161
 
154
162
  attr_reader :plural_name # :nodoc:
155
163
 
156
- def initialize(name, options, duck_record)
164
+ def initialize(name, scope, options, duck_record)
157
165
  @name = name
166
+ @scope = scope
158
167
  @options = options
159
168
  @duck_record = duck_record
160
169
  @klass = options[:anonymous_class]
@@ -173,6 +182,16 @@ module DuckRecord
173
182
  name.constantize
174
183
  end
175
184
 
185
+ # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
186
+ # and +other_aggregation+ has an options hash assigned to it.
187
+ def ==(other_aggregation)
188
+ super ||
189
+ other_aggregation.kind_of?(self.class) &&
190
+ name == other_aggregation.name &&
191
+ !other_aggregation.options.nil? &&
192
+ active_record == other_aggregation.active_record
193
+ end
194
+
176
195
  private
177
196
 
178
197
  def derive_class_name
@@ -182,7 +201,7 @@ module DuckRecord
182
201
 
183
202
  # Holds all the metadata about an association as it was specified in the
184
203
  # Active Record class.
185
- class EmbedsAssociationReflection < MacroReflection #:nodoc:
204
+ class EmbedsAssociationReflection < MacroReflection
186
205
  # Returns the target association's class.
187
206
  #
188
207
  # class Author < ActiveRecord::Base
@@ -205,7 +224,7 @@ module DuckRecord
205
224
 
206
225
  attr_accessor :parent_reflection # Reflection
207
226
 
208
- def initialize(name, options, duck_record)
227
+ def initialize(name, scope, options, duck_record)
209
228
  super
210
229
  @constructable = calculate_constructable(macro, options)
211
230
 
@@ -286,7 +305,7 @@ module DuckRecord
286
305
  end
287
306
  end
288
307
 
289
- class EmbedsManyReflection < EmbedsAssociationReflection # :nodoc:
308
+ class EmbedsManyReflection < EmbedsAssociationReflection
290
309
  def macro; :embeds_many; end
291
310
 
292
311
  def collection?; true; end
@@ -296,7 +315,7 @@ module DuckRecord
296
315
  end
297
316
  end
298
317
 
299
- class EmbedsOneReflection < EmbedsAssociationReflection # :nodoc:
318
+ class EmbedsOneReflection < EmbedsAssociationReflection
300
319
  def macro; :embeds_one; end
301
320
 
302
321
  def has_one?; true; end
@@ -305,5 +324,323 @@ module DuckRecord
305
324
  Associations::EmbedsOneAssociation
306
325
  end
307
326
  end
327
+
328
+ # Holds all the metadata about an association as it was specified in the
329
+ # Active Record class.
330
+ class AssociationReflection < MacroReflection
331
+ alias active_record duck_record
332
+
333
+ def quoted_table_name
334
+ klass.quoted_table_name
335
+ end
336
+
337
+ def primary_key_type
338
+ klass.type_for_attribute(klass.primary_key)
339
+ end
340
+
341
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
342
+
343
+ def join_keys
344
+ get_join_keys klass
345
+ end
346
+
347
+ # Returns a list of scopes that should be applied for this Reflection
348
+ # object when querying the database.
349
+ def scopes
350
+ scope ? [scope] : []
351
+ end
352
+
353
+ def scope_chain
354
+ chain.map(&:scopes)
355
+ end
356
+ deprecate :scope_chain
357
+
358
+ def join_scopes(table, predicate_builder) # :nodoc:
359
+ if scope
360
+ [ActiveRecord::Relation.create(klass, table, predicate_builder)
361
+ .instance_exec(&scope)]
362
+ else
363
+ []
364
+ end
365
+ end
366
+
367
+ def klass_join_scope(table, predicate_builder) # :nodoc:
368
+ relation = ActiveRecord::Relation.create(klass, table, predicate_builder)
369
+ klass.scope_for_association(relation)
370
+ end
371
+
372
+ def constraints
373
+ chain.map(&:scopes).flatten
374
+ end
375
+
376
+ def alias_candidate(name)
377
+ "#{plural_name}_#{name}"
378
+ end
379
+
380
+ def chain
381
+ collect_join_chain
382
+ end
383
+
384
+ def get_join_keys(association_klass)
385
+ JoinKeys.new(join_pk(association_klass), join_fk)
386
+ end
387
+
388
+ # Returns the target association's class.
389
+ #
390
+ # class Author < ActiveRecord::Base
391
+ # has_many :books
392
+ # end
393
+ #
394
+ # Author.reflect_on_association(:books).klass
395
+ # # => Book
396
+ #
397
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
398
+ # a new association object. Use +build_association+ or +create_association+
399
+ # instead. This allows plugins to hook into association object creation.
400
+ def klass
401
+ @klass ||= compute_class(class_name)
402
+ end
403
+
404
+ def compute_class(name)
405
+ active_record.send(:compute_type, name)
406
+ end
407
+
408
+ def table_name
409
+ klass.table_name
410
+ end
411
+
412
+ attr_reader :type, :foreign_type
413
+ attr_accessor :parent_reflection # Reflection
414
+
415
+ def initialize(name, scope, options, active_record)
416
+ super
417
+ @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
418
+ @foreign_type = options[:foreign_type] || "#{name}_type"
419
+ @constructable = calculate_constructable(macro, options)
420
+ @association_scope_cache = {}
421
+ @scope_lock = Mutex.new
422
+
423
+ if options[:class_name] && options[:class_name].class == Class
424
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
425
+ Passing a class to the `class_name` is deprecated and will raise
426
+ an ArgumentError in Rails 5.2. It eagerloads more classes than
427
+ necessary and potentially creates circular dependencies.
428
+
429
+ Please pass the class name as a string:
430
+ `#{macro} :#{name}, class_name: '#{options[:class_name]}'`
431
+ MSG
432
+ end
433
+ end
434
+
435
+ def association_scope_cache(conn, owner)
436
+ key = conn.prepared_statements
437
+ @association_scope_cache[key] ||= @scope_lock.synchronize {
438
+ @association_scope_cache[key] ||= yield
439
+ }
440
+ end
441
+
442
+ def constructable? # :nodoc:
443
+ @constructable
444
+ end
445
+
446
+ def join_table
447
+ @join_table ||= options[:join_table] || derive_join_table
448
+ end
449
+
450
+ def foreign_key
451
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze
452
+ end
453
+
454
+ def association_foreign_key
455
+ @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
456
+ end
457
+
458
+ # klass option is necessary to support loading polymorphic associations
459
+ def association_primary_key(klass = nil)
460
+ options[:primary_key] || primary_key(klass || self.klass)
461
+ end
462
+
463
+ def association_primary_key_type
464
+ klass.type_for_attribute(association_primary_key.to_s)
465
+ end
466
+
467
+ def active_record_primary_key
468
+ @active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
469
+ end
470
+
471
+ def check_validity!
472
+ unless klass < ActiveRecord::Base
473
+ raise ArgumentError, "#{klass} must be inherited from ActiveRecord::Base."
474
+ end
475
+ end
476
+
477
+ def check_preloadable!
478
+ return unless scope
479
+
480
+ if scope.arity > 0
481
+ raise ArgumentError, <<-MSG.squish
482
+ The association scope '#{name}' is instance dependent (the scope
483
+ block takes an argument). Preloading instance dependent scopes is
484
+ not supported.
485
+ MSG
486
+ end
487
+ end
488
+ alias :check_eager_loadable! :check_preloadable!
489
+
490
+ def join_id_for(owner) # :nodoc:
491
+ owner[active_record_primary_key]
492
+ end
493
+
494
+ def source_reflection
495
+ self
496
+ end
497
+
498
+ # A chain of reflections from this one back to the owner. For more see the explanation in
499
+ # ThroughReflection.
500
+ def collect_join_chain
501
+ [self]
502
+ end
503
+
504
+ # This is for clearing cache on the reflection. Useful for tests that need to compare
505
+ # SQL queries on associations.
506
+ def clear_association_scope_cache # :nodoc:
507
+ @association_scope_cache.clear
508
+ end
509
+
510
+ def nested?
511
+ false
512
+ end
513
+
514
+ def has_scope?
515
+ scope
516
+ end
517
+
518
+ # Returns the macro type.
519
+ #
520
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
521
+ def macro; raise NotImplementedError; end
522
+
523
+ # Returns whether or not this association reflection is for a collection
524
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
525
+ # +has_and_belongs_to_many+, +false+ otherwise.
526
+ def collection?
527
+ false
528
+ end
529
+
530
+ # Returns whether or not the association should be validated as part of
531
+ # the parent's validation.
532
+ #
533
+ # Unless you explicitly disable validation with
534
+ # <tt>validate: false</tt>, validation will take place when:
535
+ #
536
+ # * you explicitly enable validation; <tt>validate: true</tt>
537
+ # * you use autosave; <tt>autosave: true</tt>
538
+ # * the association is a +has_many+ association
539
+ def validate?
540
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
541
+ end
542
+
543
+ # Returns +true+ if +self+ is a +belongs_to+ reflection.
544
+ def belongs_to?; false; end
545
+
546
+ # Returns +true+ if +self+ is a +has_one+ reflection.
547
+ def has_one?; false; end
548
+
549
+ def association_class; raise NotImplementedError; end
550
+
551
+ def add_as_source(seed)
552
+ seed
553
+ end
554
+
555
+ def extensions
556
+ Array(options[:extend])
557
+ end
558
+
559
+ protected
560
+
561
+ def actual_source_reflection # FIXME: this is a horrible name
562
+ self
563
+ end
564
+
565
+ private
566
+
567
+ def join_pk(_)
568
+ foreign_key
569
+ end
570
+
571
+ def join_fk
572
+ active_record_primary_key
573
+ end
574
+
575
+ def calculate_constructable(_macro, _options)
576
+ false
577
+ end
578
+
579
+ def derive_class_name
580
+ class_name = name.to_s
581
+ class_name = class_name.singularize if collection?
582
+ class_name.camelize
583
+ end
584
+
585
+ def derive_foreign_key
586
+ if options[:as]
587
+ "#{options[:as]}_id"
588
+ else
589
+ "#{name}_id"
590
+ end
591
+ end
592
+
593
+ def primary_key(klass)
594
+ klass.primary_key || raise(ActiveRecord::UnknownPrimaryKey.new(klass))
595
+ end
596
+ end
597
+
598
+ class BelongsToReflection < AssociationReflection # :nodoc:
599
+ def macro; :belongs_to; end
600
+
601
+ def belongs_to?; true; end
602
+
603
+ def association_class
604
+ Associations::BelongsToAssociation
605
+ end
606
+
607
+ def join_id_for(owner) # :nodoc:
608
+ owner[foreign_key]
609
+ end
610
+
611
+ private
612
+
613
+ def join_fk
614
+ foreign_key
615
+ end
616
+
617
+ def join_pk(_klass)
618
+ association_primary_key
619
+ end
620
+ end
621
+
622
+ class HasManyReflection < AssociationReflection # :nodoc:
623
+ def macro; :has_many; end
624
+
625
+ def collection?; true; end
626
+
627
+ def association_class
628
+ Associations::HasManyAssociation
629
+ end
630
+
631
+ def association_primary_key(klass = nil)
632
+ primary_key(klass || self.klass)
633
+ end
634
+ end
635
+
636
+ class HasOneReflection < AssociationReflection # :nodoc:
637
+ def macro; :has_one; end
638
+
639
+ def has_one?; true; end
640
+
641
+ def association_class
642
+ Associations::HasOneAssociation
643
+ end
644
+ end
308
645
  end
309
646
  end