dynamoid 3.9.0 → 3.11.0

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.
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