dynamoid 3.9.0 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -6
  3. data/README.md +202 -25
  4. data/dynamoid.gemspec +5 -6
  5. data/lib/dynamoid/adapter.rb +19 -13
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +113 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +21 -2
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +40 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +34 -28
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +95 -66
  14. data/lib/dynamoid/associations/belongs_to.rb +6 -6
  15. data/lib/dynamoid/associations.rb +1 -1
  16. data/lib/dynamoid/components.rb +1 -0
  17. data/lib/dynamoid/config/options.rb +12 -12
  18. data/lib/dynamoid/config.rb +3 -0
  19. data/lib/dynamoid/criteria/chain.rb +149 -142
  20. data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
  21. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
  22. data/lib/dynamoid/criteria/where_conditions.rb +36 -0
  23. data/lib/dynamoid/dirty.rb +87 -12
  24. data/lib/dynamoid/document.rb +1 -1
  25. data/lib/dynamoid/dumping.rb +38 -16
  26. data/lib/dynamoid/errors.rb +14 -2
  27. data/lib/dynamoid/fields/declare.rb +6 -6
  28. data/lib/dynamoid/fields.rb +6 -8
  29. data/lib/dynamoid/finders.rb +23 -32
  30. data/lib/dynamoid/indexes.rb +6 -7
  31. data/lib/dynamoid/loadable.rb +3 -2
  32. data/lib/dynamoid/persistence/inc.rb +6 -7
  33. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  34. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  35. data/lib/dynamoid/persistence/save.rb +17 -18
  36. data/lib/dynamoid/persistence/update_fields.rb +7 -5
  37. data/lib/dynamoid/persistence/update_validations.rb +1 -1
  38. data/lib/dynamoid/persistence/upsert.rb +5 -4
  39. data/lib/dynamoid/persistence.rb +77 -21
  40. data/lib/dynamoid/transaction_write/base.rb +47 -0
  41. data/lib/dynamoid/transaction_write/create.rb +49 -0
  42. data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
  43. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
  44. data/lib/dynamoid/transaction_write/destroy.rb +79 -0
  45. data/lib/dynamoid/transaction_write/save.rb +164 -0
  46. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  47. data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
  48. data/lib/dynamoid/transaction_write/upsert.rb +96 -0
  49. data/lib/dynamoid/transaction_write.rb +464 -0
  50. data/lib/dynamoid/type_casting.rb +18 -15
  51. data/lib/dynamoid/undumping.rb +14 -3
  52. data/lib/dynamoid/validations.rb +1 -1
  53. data/lib/dynamoid/version.rb +1 -1
  54. data/lib/dynamoid.rb +7 -0
  55. metadata +30 -16
  56. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
  57. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +0 -40
@@ -48,7 +48,9 @@ module Dynamoid
48
48
  option :dynamodb_timezone, default: :utc # available values - :utc, :local, time zone name like "Hawaii"
49
49
  option :store_datetime_as_string, default: false # store Time fields in ISO 8601 string format
50
50
  option :store_date_as_string, default: false # store Date fields in ISO 8601 string format
51
+ option :store_empty_string_as_nil, default: true # store attribute's empty String value as null
51
52
  option :store_boolean_as_native, default: true
53
+ option :store_binary_as_native, default: false
52
54
  option :backoff, default: nil # callable object to handle exceeding of table throughput limit
53
55
  option :backoff_strategies, default: {
54
56
  constant: BackoffStrategies::ConstantBackoff,
@@ -59,6 +61,7 @@ module Dynamoid
59
61
  option :http_idle_timeout, default: nil # - default: 5
60
62
  option :http_open_timeout, default: nil # - default: 15
61
63
  option :http_read_timeout, default: nil # - default: 60
64
+ option :create_table_on_save, default: true
62
65
 
63
66
  # The default logger for Dynamoid: either the Rails logger or just stdout.
64
67
  #
@@ -1,29 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'key_fields_detector'
4
- require_relative 'ignored_conditions_detector'
5
- require_relative 'overwritten_conditions_detector'
6
4
  require_relative 'nonexistent_fields_detector'
5
+ require_relative 'where_conditions'
7
6
 
8
7
  module Dynamoid
9
8
  module Criteria
10
9
  # The criteria chain is equivalent to an ActiveRecord relation (and realistically I should change the name from
11
10
  # chain to relation). It is a chainable object that builds up a query and eventually executes it by a Query or Scan.
12
11
  class Chain
13
- attr_reader :query, :source, :consistent_read, :key_fields_detector
12
+ attr_reader :source, :consistent_read, :key_fields_detector
14
13
 
15
14
  include Enumerable
15
+
16
+ ALLOWED_FIELD_OPERATORS = Set.new(
17
+ %w[
18
+ eq ne gt lt gte lte between begins_with in contains not_contains null not_null
19
+ ]
20
+ ).freeze
21
+
16
22
  # Create a new criteria chain.
17
23
  #
18
24
  # @param [Class] source the class upon which the ultimate query will be performed.
19
25
  def initialize(source)
20
- @query = {}
26
+ @where_conditions = WhereConditions.new
21
27
  @source = source
22
28
  @consistent_read = false
23
29
  @scan_index_forward = true
24
30
 
25
- # we should re-initialize keys detector every time we change query
26
- @key_fields_detector = KeyFieldsDetector.new(@query, @source)
31
+ # we should re-initialize keys detector every time we change @where_conditions
32
+ @key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source)
27
33
  end
28
34
 
29
35
  # Returns a chain which is a result of filtering current chain with the specified conditions.
@@ -89,30 +95,27 @@ module Dynamoid
89
95
  #
90
96
  # Internally +where+ performs either +Scan+ or +Query+ operation.
91
97
  #
98
+ # Conditions can be specified as an expression as well:
99
+ #
100
+ # Post.where('links_count = :v', v: 2)
101
+ #
102
+ # This way complex expressions can be constructed (e.g. with AND, OR, and NOT
103
+ # keyword):
104
+ #
105
+ # Address.where('city = :c AND (post_code = :pc1 OR post_code = :pc2)', city: 'A', pc1: '001', pc2: '002')
106
+ #
107
+ # See documentation for condition expression's syntax and examples:
108
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
109
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.FilterExpression.html
110
+ #
92
111
  # @return [Dynamoid::Criteria::Chain]
93
112
  # @since 0.2.0
94
- def where(args)
95
- detector = IgnoredConditionsDetector.new(args)
96
- if detector.found?
97
- Dynamoid.logger.warn(detector.warning_message)
98
- end
99
-
100
- detector = OverwrittenConditionsDetector.new(@query, args)
101
- if detector.found?
102
- Dynamoid.logger.warn(detector.warning_message)
103
- end
104
-
105
- detector = NonexistentFieldsDetector.new(args, @source)
106
- if detector.found?
107
- Dynamoid.logger.warn(detector.warning_message)
113
+ def where(conditions, placeholders = nil)
114
+ if conditions.is_a?(Hash)
115
+ where_with_hash(conditions)
116
+ else
117
+ where_with_string(conditions, placeholders)
108
118
  end
109
-
110
- query.update(args.symbolize_keys)
111
-
112
- # we should re-initialize keys detector every time we change query
113
- @key_fields_detector = KeyFieldsDetector.new(@query, @source, forced_index_name: @forced_index_name)
114
-
115
- self
116
119
  end
117
120
 
118
121
  # Turns on strongly consistent reads.
@@ -187,7 +190,7 @@ module Dynamoid
187
190
  def first(*args)
188
191
  n = args.first || 1
189
192
 
190
- return dup.scan_limit(n).to_a.first(*args) if @query.blank?
193
+ return dup.scan_limit(n).to_a.first(*args) if @where_conditions.empty?
191
194
  return super if @key_fields_detector.non_key_present?
192
195
 
193
196
  dup.record_limit(n).to_a.first(*args)
@@ -230,12 +233,12 @@ module Dynamoid
230
233
  ranges = []
231
234
 
232
235
  if @key_fields_detector.key_present?
233
- Dynamoid.adapter.query(source.table_name, range_query).flat_map { |i| i }.collect do |hash|
236
+ Dynamoid.adapter.query(source.table_name, query_key_conditions, query_non_key_conditions, query_options).flat_map { |i| i }.collect do |hash|
234
237
  ids << hash[source.hash_key.to_sym]
235
238
  ranges << hash[source.range_key.to_sym] if source.range_key
236
239
  end
237
240
  else
238
- Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).flat_map { |i| i }.collect do |hash|
241
+ Dynamoid.adapter.scan(source.table_name, scan_conditions, scan_options).flat_map { |i| i }.collect do |hash|
239
242
  ids << hash[source.hash_key.to_sym]
240
243
  ranges << hash[source.range_key.to_sym] if source.range_key
241
244
  end
@@ -384,7 +387,7 @@ module Dynamoid
384
387
  raise Dynamoid::Errors::InvalidIndex, "Unknown index #{index_name}" unless @source.find_index_by_name(index_name)
385
388
 
386
389
  @forced_index_name = index_name
387
- @key_fields_detector = KeyFieldsDetector.new(@query, @source, forced_index_name: index_name)
390
+ @key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: index_name)
388
391
  self
389
392
  end
390
393
 
@@ -451,9 +454,9 @@ module Dynamoid
451
454
  # It takes one or more field names and returns a collection of models with only
452
455
  # these fields set.
453
456
  #
454
- # Post.where('views_count.gt' => 1000).select(:title)
455
- # Post.where('views_count.gt' => 1000).select(:title, :created_at)
456
- # Post.select(:id)
457
+ # Post.where('views_count.gt' => 1000).project(:title)
458
+ # Post.where('views_count.gt' => 1000).project(:title, :created_at)
459
+ # Post.project(:id)
457
460
  #
458
461
  # It can be used to avoid loading large field values and to decrease a
459
462
  # memory footprint.
@@ -487,6 +490,8 @@ module Dynamoid
487
490
  def pluck(*args)
488
491
  fields = args.map(&:to_sym)
489
492
 
493
+ # `project` has a side effect - it sets `@project` instance variable.
494
+ # So use a duplicate to not pollute original chain.
490
495
  scope = dup
491
496
  scope.project(*fields)
492
497
 
@@ -502,6 +507,29 @@ module Dynamoid
502
507
 
503
508
  private
504
509
 
510
+ def where_with_hash(conditions)
511
+ detector = NonexistentFieldsDetector.new(conditions, @source)
512
+ if detector.found?
513
+ Dynamoid.logger.warn(detector.warning_message)
514
+ end
515
+
516
+ @where_conditions.update_with_hash(conditions.symbolize_keys)
517
+
518
+ # we should re-initialize keys detector every time we change @where_conditions
519
+ @key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: @forced_index_name)
520
+
521
+ self
522
+ end
523
+
524
+ def where_with_string(query, placeholders)
525
+ @where_conditions.update_with_string(query, placeholders)
526
+
527
+ # we should re-initialize keys detector every time we change @where_conditions
528
+ @key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: @forced_index_name)
529
+
530
+ self
531
+ end
532
+
505
533
  # The actual records referenced by the association.
506
534
  #
507
535
  # @return [Enumerator] an iterator of the found records.
@@ -535,7 +563,7 @@ module Dynamoid
535
563
  if @key_fields_detector.key_present?
536
564
  raw_pages_via_query
537
565
  else
538
- issue_scan_warning if Dynamoid::Config.warn_on_scan && query.present?
566
+ issue_scan_warning if Dynamoid::Config.warn_on_scan && !@where_conditions.empty?
539
567
  raw_pages_via_scan
540
568
  end
541
569
  end
@@ -547,7 +575,7 @@ module Dynamoid
547
575
  # @since 3.1.0
548
576
  def raw_pages_via_query
549
577
  Enumerator.new do |y|
550
- Dynamoid.adapter.query(source.table_name, range_query).each do |items, metadata|
578
+ Dynamoid.adapter.query(source.table_name, query_key_conditions, query_non_key_conditions, query_options).each do |items, metadata|
551
579
  options = metadata.slice(:last_evaluated_key)
552
580
 
553
581
  y.yield items, options
@@ -562,7 +590,7 @@ module Dynamoid
562
590
  # @since 3.1.0
563
591
  def raw_pages_via_scan
564
592
  Enumerator.new do |y|
565
- Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).each do |items, metadata|
593
+ Dynamoid.adapter.scan(source.table_name, scan_conditions, scan_options).each do |items, metadata|
566
594
  options = metadata.slice(:last_evaluated_key)
567
595
 
568
596
  y.yield items, options
@@ -575,121 +603,94 @@ module Dynamoid
575
603
  Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.underscore}.rb:"
576
604
  Dynamoid.logger.warn "* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'"
577
605
  Dynamoid.logger.warn "* local_secondary_index range_key: 'some-name'"
578
- Dynamoid.logger.warn "Not indexed attributes: #{query.keys.sort.collect { |name| ":#{name}" }.join(', ')}"
606
+ Dynamoid.logger.warn "Not indexed attributes: #{@where_conditions.keys.sort.collect { |name| ":#{name}" }.join(', ')}"
579
607
  end
580
608
 
581
609
  def count_via_query
582
- Dynamoid.adapter.query_count(source.table_name, range_query)
610
+ Dynamoid.adapter.query_count(source.table_name, query_key_conditions, query_non_key_conditions, query_options)
583
611
  end
584
612
 
585
613
  def count_via_scan
586
- Dynamoid.adapter.scan_count(source.table_name, scan_query, scan_opts)
587
- end
588
-
589
- def range_hash(key)
590
- name, operation = key.to_s.split('.')
591
- val = type_cast_condition_parameter(name, query[key])
592
-
593
- case operation
594
- when 'gt'
595
- { range_greater_than: val }
596
- when 'lt'
597
- { range_less_than: val }
598
- when 'gte'
599
- { range_gte: val }
600
- when 'lte'
601
- { range_lte: val }
602
- when 'between'
603
- { range_between: val }
604
- when 'begins_with'
605
- { range_begins_with: val }
606
- end
614
+ Dynamoid.adapter.scan_count(source.table_name, scan_conditions, scan_options)
607
615
  end
608
616
 
609
- def field_hash(key)
610
- name, operation = key.to_s.split('.')
611
- val = type_cast_condition_parameter(name, query[key])
612
-
613
- hash = case operation
614
- when 'ne'
615
- { ne: val }
616
- when 'gt'
617
- { gt: val }
618
- when 'lt'
619
- { lt: val }
620
- when 'gte'
621
- { gte: val }
622
- when 'lte'
623
- { lte: val }
624
- when 'between'
625
- { between: val }
626
- when 'begins_with'
627
- { begins_with: val }
628
- when 'in'
629
- { in: val }
630
- when 'contains'
631
- { contains: val }
632
- when 'not_contains'
633
- { not_contains: val }
634
- # NULL/NOT_NULL operators don't have parameters
635
- # So { null: true } means NULL check and { null: false } means NOT_NULL one
636
- # The same logic is used for { not_null: BOOL }
637
- when 'null'
638
- val ? { null: nil } : { not_null: nil }
639
- when 'not_null'
640
- val ? { not_null: nil } : { null: nil }
641
- end
642
-
643
- { name.to_sym => hash }
644
- end
645
-
646
- def consistent_opts
647
- { consistent_read: consistent_read }
648
- end
649
-
650
- def range_query
651
- opts = {}
652
- query = self.query
617
+ def field_condition(key, value_before_type_casting)
618
+ name, operator = key.to_s.split('.')
619
+ value = type_cast_condition_parameter(name, value_before_type_casting)
620
+ operator ||= 'eq'
653
621
 
654
- # Honor STI and :type field if it presents
655
- if @source.attributes.key?(@source.inheritance_field) &&
656
- @key_fields_detector.hash_key.to_sym != @source.inheritance_field.to_sym
657
- query.update(sti_condition)
622
+ unless operator.in? ALLOWED_FIELD_OPERATORS
623
+ raise Dynamoid::Errors::Error, "Unsupported operator #{operator} in #{key}"
658
624
  end
659
625
 
626
+ condition =
627
+ case operator
628
+ # NULL/NOT_NULL operators don't have parameters
629
+ # So { null: true } means NULL check and { null: false } means NOT_NULL one
630
+ # The same logic is used for { not_null: BOOL }
631
+ when 'null'
632
+ value ? [:null, nil] : [:not_null, nil]
633
+ when 'not_null'
634
+ value ? [:not_null, nil] : [:null, nil]
635
+ else
636
+ [operator.to_sym, value]
637
+ end
638
+
639
+ [name.to_sym, condition]
640
+ end
641
+
642
+ def query_key_conditions
643
+ opts = {}
644
+
660
645
  # Add hash key
661
- opts[:hash_key] = @key_fields_detector.hash_key
662
- opts[:hash_value] = type_cast_condition_parameter(@key_fields_detector.hash_key, query[@key_fields_detector.hash_key])
646
+ # TODO: always have hash key in @where_conditions?
647
+ _, condition = field_condition(@key_fields_detector.hash_key, @where_conditions[@key_fields_detector.hash_key])
648
+ opts[@key_fields_detector.hash_key] = [condition]
663
649
 
664
650
  # Add range key
665
651
  if @key_fields_detector.range_key
666
- add_range_key_to_range_query(query, opts)
667
- end
652
+ if @where_conditions[@key_fields_detector.range_key].present?
653
+ _, condition = field_condition(@key_fields_detector.range_key, @where_conditions[@key_fields_detector.range_key])
654
+ opts[@key_fields_detector.range_key] = [condition]
655
+ end
668
656
 
669
- (query.keys.map(&:to_sym) - [@key_fields_detector.hash_key.to_sym, @key_fields_detector.range_key.try(:to_sym)])
670
- .reject { |k, _| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }
671
- .each do |key|
672
- if key.to_s.include?('.')
673
- opts.update(field_hash(key))
674
- else
675
- value = type_cast_condition_parameter(key, query[key])
676
- opts[key] = { eq: value }
657
+ @where_conditions.keys.select { |k| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }.each do |key|
658
+ name, condition = field_condition(key, @where_conditions[key])
659
+ opts[name] ||= []
660
+ opts[name] << condition
677
661
  end
678
662
  end
679
663
 
680
- opts.merge(query_opts).merge(consistent_opts)
664
+ opts
681
665
  end
682
666
 
683
- def add_range_key_to_range_query(query, opts)
684
- opts[:range_key] = @key_fields_detector.range_key
685
- if query[@key_fields_detector.range_key].present?
686
- value = type_cast_condition_parameter(@key_fields_detector.range_key, query[@key_fields_detector.range_key])
687
- opts.update(range_eq: value)
667
+ def query_non_key_conditions
668
+ hash_conditions = {}
669
+
670
+ # Honor STI and :type field if it presents
671
+ if @source.attributes.key?(@source.inheritance_field) &&
672
+ @key_fields_detector.hash_key.to_sym != @source.inheritance_field.to_sym
673
+ @where_conditions.update_with_hash(sti_condition)
674
+ end
675
+
676
+ # TODO: Separate key conditions and non-key conditions properly:
677
+ # only =, >, >=, <, <=, between and begins_with
678
+ # could be used for sort key in KeyConditionExpression
679
+ keys = (@where_conditions.keys.map(&:to_sym) - [@key_fields_detector.hash_key.to_sym, @key_fields_detector.range_key.try(:to_sym)])
680
+ .reject { |k, _| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }
681
+ keys.each do |key|
682
+ name, condition = field_condition(key, @where_conditions[key])
683
+ hash_conditions[name] ||= []
684
+ hash_conditions[name] << condition
688
685
  end
689
686
 
690
- query.keys.select { |k| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }.each do |key|
691
- opts.merge!(range_hash(key))
687
+ string_conditions = []
688
+ @where_conditions.string_conditions.each do |query, placeholders|
689
+ placeholders ||= {}
690
+ string_conditions << [query, placeholders]
692
691
  end
692
+
693
+ [hash_conditions] + string_conditions
693
694
  end
694
695
 
695
696
  # TODO: casting should be operator aware
@@ -737,7 +738,7 @@ module Dynamoid
737
738
  key
738
739
  end
739
740
 
740
- def query_opts
741
+ def query_options
741
742
  opts = {}
742
743
  # Don't specify select = ALL_ATTRIBUTES option explicitly because it's
743
744
  # already a default value of Select statement. Explicite Select value
@@ -749,30 +750,35 @@ module Dynamoid
749
750
  opts[:exclusive_start_key] = start_key if @start
750
751
  opts[:scan_index_forward] = @scan_index_forward
751
752
  opts[:project] = @project
753
+ opts[:consistent_read] = true if @consistent_read
752
754
  opts
753
755
  end
754
756
 
755
- def scan_query
756
- query = self.query
757
-
757
+ def scan_conditions
758
758
  # Honor STI and :type field if it presents
759
759
  if sti_condition
760
- query.update(sti_condition)
760
+ @where_conditions.update_with_hash(sti_condition)
761
761
  end
762
762
 
763
- {}.tap do |opts|
764
- query.keys.map(&:to_sym).each do |key|
765
- if key.to_s.include?('.')
766
- opts.update(field_hash(key))
767
- else
768
- value = type_cast_condition_parameter(key, query[key])
769
- opts[key] = { eq: value }
770
- end
763
+ hash_conditions = {}
764
+ hash_conditions.tap do |opts|
765
+ @where_conditions.keys.map(&:to_sym).each do |key|
766
+ name, condition = field_condition(key, @where_conditions[key])
767
+ opts[name] ||= []
768
+ opts[name] << condition
771
769
  end
772
770
  end
771
+
772
+ string_conditions = []
773
+ @where_conditions.string_conditions.each do |query, placeholders|
774
+ placeholders ||= {}
775
+ string_conditions << [query, placeholders]
776
+ end
777
+
778
+ [hash_conditions] + string_conditions
773
779
  end
774
780
 
775
- def scan_opts
781
+ def scan_options
776
782
  opts = {}
777
783
  opts[:index_name] = @key_fields_detector.index_name if @key_fields_detector.index_name
778
784
  opts[:record_limit] = @record_limit if @record_limit
@@ -784,6 +790,7 @@ module Dynamoid
784
790
  opts
785
791
  end
786
792
 
793
+ # TODO: return Array, not String
787
794
  def sti_condition
788
795
  condition = {}
789
796
  type = @source.inheritance_field
@@ -5,10 +5,10 @@ module Dynamoid
5
5
  # @private
6
6
  class KeyFieldsDetector
7
7
  class Query
8
- def initialize(query_hash)
9
- @query_hash = query_hash
10
- @fields_with_operator = query_hash.keys.map(&:to_s)
11
- @fields = query_hash.keys.map(&:to_s).map { |s| s.split('.').first }
8
+ def initialize(where_conditions)
9
+ @where_conditions = where_conditions
10
+ @fields_with_operator = where_conditions.keys.map(&:to_s)
11
+ @fields = where_conditions.keys.map(&:to_s).map { |s| s.split('.').first }
12
12
  end
13
13
 
14
14
  def contain_only?(field_names)
@@ -24,10 +24,9 @@ module Dynamoid
24
24
  end
25
25
  end
26
26
 
27
- def initialize(query, source, forced_index_name: nil)
28
- @query = query
27
+ def initialize(where_conditions, source, forced_index_name: nil)
29
28
  @source = source
30
- @query = Query.new(query)
29
+ @query = Query.new(where_conditions)
31
30
  @forced_index_name = forced_index_name
32
31
  @result = find_keys_in_query
33
32
  end
@@ -20,8 +20,8 @@ module Dynamoid
20
20
  fields_list = @nonexistent_fields.map { |s| "`#{s}`" }.join(', ')
21
21
  count = @nonexistent_fields.size
22
22
 
23
- 'where conditions contain nonexistent' \
24
- " field #{'name'.pluralize(count)} #{fields_list}"
23
+ 'where conditions contain nonexistent ' \
24
+ "field #{'name'.pluralize(count)} #{fields_list}"
25
25
  end
26
26
 
27
27
  private
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Criteria
5
+ # @private
6
+ class WhereConditions
7
+ attr_reader :string_conditions
8
+
9
+ def initialize
10
+ @hash_conditions = []
11
+ @string_conditions = []
12
+ end
13
+
14
+ def update_with_hash(hash)
15
+ @hash_conditions << hash.symbolize_keys
16
+ end
17
+
18
+ def update_with_string(query, placeholders)
19
+ @string_conditions << [query, placeholders]
20
+ end
21
+
22
+ def keys
23
+ @hash_conditions.flat_map(&:keys)
24
+ end
25
+
26
+ def empty?
27
+ @hash_conditions.empty? && @string_conditions.empty?
28
+ end
29
+
30
+ def [](key)
31
+ hash = @hash_conditions.find { |h| h.key?(key) }
32
+ hash[key] if hash
33
+ end
34
+ end
35
+ end
36
+ end