dynomite 1.2.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -2
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile +1 -5
  5. data/LICENSE.txt +22 -0
  6. data/README.md +6 -190
  7. data/Rakefile +13 -1
  8. data/dynomite.gemspec +9 -2
  9. data/exe/dynomite +14 -0
  10. data/lib/dynomite/associations/association.rb +126 -0
  11. data/lib/dynomite/associations/belongs_to.rb +35 -0
  12. data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
  13. data/lib/dynomite/associations/has_many.rb +19 -0
  14. data/lib/dynomite/associations/has_one.rb +19 -0
  15. data/lib/dynomite/associations/many_association.rb +257 -0
  16. data/lib/dynomite/associations/single_association.rb +157 -0
  17. data/lib/dynomite/associations.rb +248 -0
  18. data/lib/dynomite/autoloader.rb +25 -0
  19. data/lib/dynomite/cli.rb +48 -0
  20. data/lib/dynomite/client.rb +118 -0
  21. data/lib/dynomite/command.rb +89 -0
  22. data/lib/dynomite/completer/script.rb +6 -0
  23. data/lib/dynomite/completer/script.sh +10 -0
  24. data/lib/dynomite/completer.rb +159 -0
  25. data/lib/dynomite/config.rb +39 -0
  26. data/lib/dynomite/core.rb +18 -19
  27. data/lib/dynomite/engine.rb +45 -0
  28. data/lib/dynomite/erb.rb +5 -3
  29. data/lib/dynomite/error.rb +12 -0
  30. data/lib/dynomite/help/completion.md +20 -0
  31. data/lib/dynomite/help/completion_script.md +3 -0
  32. data/lib/dynomite/help/migrate.md +3 -0
  33. data/lib/dynomite/help.rb +9 -0
  34. data/lib/dynomite/install.rb +4 -0
  35. data/lib/dynomite/item/abstract.rb +15 -0
  36. data/lib/dynomite/item/components.rb +33 -0
  37. data/lib/dynomite/item/dsl.rb +101 -0
  38. data/lib/dynomite/item/id.rb +41 -0
  39. data/lib/dynomite/item/indexes/finder.rb +58 -0
  40. data/lib/dynomite/item/indexes/index.rb +21 -0
  41. data/lib/dynomite/item/indexes/primary_index.rb +18 -0
  42. data/lib/dynomite/item/indexes.rb +25 -0
  43. data/lib/dynomite/item/locking.rb +53 -0
  44. data/lib/dynomite/item/magic_fields.rb +66 -0
  45. data/lib/dynomite/item/primary_key.rb +85 -0
  46. data/lib/dynomite/item/query/delegates.rb +28 -0
  47. data/lib/dynomite/item/query/params/base.rb +42 -0
  48. data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
  49. data/lib/dynomite/item/query/params/filter.rb +41 -0
  50. data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
  51. data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
  52. data/lib/dynomite/item/query/params/function/base.rb +33 -0
  53. data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
  54. data/lib/dynomite/item/query/params/function/contains.rb +7 -0
  55. data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
  56. data/lib/dynomite/item/query/params/helpers.rb +94 -0
  57. data/lib/dynomite/item/query/params/key_condition.rb +34 -0
  58. data/lib/dynomite/item/query/params.rb +115 -0
  59. data/lib/dynomite/item/query/partiql/executer.rb +72 -0
  60. data/lib/dynomite/item/query/partiql.rb +67 -0
  61. data/lib/dynomite/item/query/relation/chain.rb +125 -0
  62. data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
  63. data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
  64. data/lib/dynomite/item/query/relation/delete.rb +38 -0
  65. data/lib/dynomite/item/query/relation/ids.rb +21 -0
  66. data/lib/dynomite/item/query/relation/math.rb +19 -0
  67. data/lib/dynomite/item/query/relation/where_field.rb +32 -0
  68. data/lib/dynomite/item/query/relation/where_group.rb +78 -0
  69. data/lib/dynomite/item/query/relation.rb +127 -0
  70. data/lib/dynomite/item/query.rb +7 -0
  71. data/lib/dynomite/item/read/find.rb +196 -0
  72. data/lib/dynomite/item/read/find_with_event.rb +42 -0
  73. data/lib/dynomite/item/read.rb +90 -0
  74. data/lib/dynomite/item/sti.rb +43 -0
  75. data/lib/dynomite/item/table_namespace.rb +43 -0
  76. data/lib/dynomite/item/typecaster.rb +106 -0
  77. data/lib/dynomite/item/waiter_methods.rb +18 -0
  78. data/lib/dynomite/item/write/base.rb +15 -0
  79. data/lib/dynomite/item/write/delete_item.rb +14 -0
  80. data/lib/dynomite/item/write/put_item.rb +99 -0
  81. data/lib/dynomite/item/write/update_item.rb +73 -0
  82. data/lib/dynomite/item/write.rb +204 -0
  83. data/lib/dynomite/item.rb +113 -286
  84. data/lib/dynomite/migration/dsl/accessor.rb +19 -0
  85. data/lib/dynomite/migration/dsl/index/base.rb +42 -0
  86. data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
  87. data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
  88. data/lib/dynomite/migration/dsl/index.rb +72 -0
  89. data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
  90. data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
  91. data/lib/dynomite/migration/dsl.rb +89 -142
  92. data/lib/dynomite/migration/file_info.rb +28 -0
  93. data/lib/dynomite/migration/generator.rb +30 -16
  94. data/lib/dynomite/migration/helpers.rb +7 -0
  95. data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
  96. data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
  97. data/lib/dynomite/migration/runner.rb +178 -0
  98. data/lib/dynomite/migration/templates/create_table.rb +7 -23
  99. data/lib/dynomite/migration/templates/delete_table.rb +7 -0
  100. data/lib/dynomite/migration/templates/update_table.rb +3 -18
  101. data/lib/dynomite/migration.rb +53 -10
  102. data/lib/dynomite/reserved_words.rb +13 -3
  103. data/lib/dynomite/seed.rb +12 -0
  104. data/lib/dynomite/types.rb +22 -0
  105. data/lib/dynomite/version.rb +1 -1
  106. data/lib/dynomite/waiter.rb +40 -0
  107. data/lib/dynomite.rb +11 -17
  108. data/lib/generators/application_item/application_item_generator.rb +30 -0
  109. data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
  110. data/lib/jets/commands/dynamodb_command.rb +29 -0
  111. data/lib/jets/commands/help/generate.md +33 -0
  112. data/lib/jets/commands/help/migrate.md +3 -0
  113. metadata +201 -17
  114. data/docs/migrations/long-example.rb +0 -127
  115. data/docs/migrations/short-example.rb +0 -40
  116. data/lib/dynomite/db_config.rb +0 -121
  117. data/lib/dynomite/errors.rb +0 -15
  118. data/lib/dynomite/log.rb +0 -15
  119. data/lib/dynomite/migration/common.rb +0 -86
  120. data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
  121. data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
  122. data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
  123. data/lib/dynomite/migration/executor.rb +0 -38
@@ -0,0 +1,21 @@
1
+ module Dynomite::Item::Query::Params::Function
2
+ class AttributeExists < Base
3
+ def filter_expression
4
+ filter_expression = []
5
+ @query[:attribute_exists].each do |path|
6
+ path = normalize_expression_path(path)
7
+ filter_expression << "attribute_exists(#{path})"
8
+ end
9
+ @query[:attribute_not_exists].each do |path|
10
+ path = normalize_expression_path(path)
11
+ filter_expression << "attribute_not_exists(#{path})"
12
+ end
13
+ filter_expression
14
+ end
15
+
16
+ def attribute_names
17
+ paths = @query[:attribute_exists] + @query[:attribute_not_exists]
18
+ build_attribute_names_with_dot_paths(paths)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ module Dynomite::Item::Query::Params::Function
2
+ class AttributeType < Base
3
+ def filter_expression
4
+ filter_expression = []
5
+ @query[:attribute_type].each do |attribute_type|
6
+ path, type = attribute_type[:path], attribute_type[:type]
7
+ path = normalize_expression_path(path)
8
+ type = type_map(type)
9
+ filter_expression << "attribute_type(#{path}, :#{type})"
10
+ end
11
+ filter_expression
12
+ end
13
+
14
+ def attribute_names
15
+ paths = @query[:attribute_type].map { |attribute_type| attribute_type[:path] }
16
+ build_attribute_names_with_dot_paths(paths)
17
+ end
18
+
19
+ def attribute_values
20
+ values = {}
21
+ @query[:attribute_type].each do |attribute_type|
22
+ type = attribute_type[:type]
23
+ type = type_map(type)
24
+ values[":#{type}"] = type
25
+ end
26
+ values
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,33 @@
1
+ module Dynomite::Item::Query::Params::Function
2
+ class Base
3
+ include Dynomite::Item::Query::Params::Helpers
4
+ include Dynomite::Types
5
+
6
+ def initialize(query)
7
+ @query = query
8
+ end
9
+
10
+ def build_attribute_names_with_dot_paths(paths)
11
+ attribute_names = {}
12
+ paths.each do |path|
13
+ fields = path.split('.')
14
+ fields.each do |field|
15
+ if field.starts_with?('#')
16
+ key = field
17
+ value = field[1..-1]
18
+ else
19
+ key = "##{field}"
20
+ value = field
21
+ end
22
+ attribute_names[key] = value
23
+ end
24
+ end
25
+ attribute_names
26
+ end
27
+
28
+ def attribute_values
29
+ {}
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,32 @@
1
+ module Dynomite::Item::Query::Params::Function
2
+ class BeginsWith < Base
3
+ def filter_expression
4
+ filter_expression = []
5
+ @query[query_key].each do |begins_with|
6
+ path, substr = begins_with[:path], begins_with[:substr]
7
+ path = normalize_expression_path(path)
8
+ filter_expression << "#{query_key}(#{path}, :#{substr})"
9
+ end
10
+ filter_expression
11
+ end
12
+
13
+ def attribute_names
14
+ paths = @query[query_key].map { |begins_with| begins_with[:path] }
15
+ build_attribute_names_with_dot_paths(paths)
16
+ end
17
+
18
+ def attribute_values
19
+ values = {}
20
+ @query[query_key].each do |begins_with|
21
+ path, substr = begins_with[:path], begins_with[:substr]
22
+ values[":#{substr}"] = substr
23
+ end
24
+ values
25
+ end
26
+
27
+ # interface method so Contains < BeginsWith can override
28
+ def query_key
29
+ :begins_with # must be a symbol
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ module Dynomite::Item::Query::Params::Function
2
+ class Contains < BeginsWith
3
+ def query_key
4
+ :contains # must be symbol
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ module Dynomite::Item::Query::Params::Function
2
+ class SizeFn < Base
3
+ include Dynomite::Item::Query::Relation::ComparisionMap
4
+
5
+ # Product.size_fn("category.gt", 100)
6
+ def filter_expression
7
+ filter_expression = []
8
+ @query[:size_fn].each_with_index do |size_fn, index|
9
+ path, size = size_fn[:path], size_fn[:size]
10
+ elements = path.split('.')
11
+ operator = elements.pop # remove last element
12
+ path = elements.join('.') # path no longer has operator
13
+ comparision = comparision_for(operator)
14
+ path = normalize_expression_path(path)
15
+ filter_expression << "size(#{path}) #{comparision} :size_value#{index}"
16
+ end
17
+ filter_expression
18
+ end
19
+
20
+ def attribute_names
21
+ paths = @query[:size_fn].map do |size_fn|
22
+ path = size_fn[:path]
23
+ path.split('.')[0..-2].join('.') # remove last element: comparision operator
24
+ end
25
+ build_attribute_names_with_dot_paths(paths)
26
+ end
27
+
28
+ def attribute_values
29
+ values = {}
30
+ @query[:size_fn].each_with_index do |size_fn, index|
31
+ size = size_fn[:size]
32
+ values[":size_value#{index}"] = size
33
+ end
34
+ values
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,94 @@
1
+ class Dynomite::Item::Query::Params
2
+ module Helpers
3
+ # Important to reset relation index for each relation chain so that
4
+ # attribute name references are correct.
5
+ # Using `with_where_groups` when interating ensures index is reset.
6
+ def with_where_groups
7
+ @relation.index = 0
8
+ @relation.query[:where].each do |where_group|
9
+ yield(where_group)
10
+ end
11
+ end
12
+
13
+ def query
14
+ @relation.query
15
+ end
16
+
17
+ # Certain queries require a scan, so we can't use the key condition
18
+ def scan_required?(index)
19
+ return true if index.nil? # first check
20
+ return true if query[:force_scan]
21
+ return true if disable_index_for_any_or?
22
+ return true if disable_index_for_not?(index)
23
+ return true if disable_index_for_consistent_read?(index)
24
+
25
+ all_where_fields.find do |full_field|
26
+ field, operator = full_field.split('.')
27
+ index.fields.include?(field) && !operator.nil?
28
+ end
29
+ end
30
+
31
+ # Always run scan when any or in chain
32
+ # For dynomite, `or` expressions will always result in a scan operation.
33
+ # This is because `key_condition_expression` does not support OR expressions.
34
+ # Nor does it make sense to use query with an index in the first pass with
35
+ # `key_condition_expression` and use `filter_expression` in the second pass.
36
+ # The `key_condition_expression` and `filter_expression` are AND with each other,
37
+ # so it would not be possible to do an OR without a scan.
38
+ def disable_index_for_any_or?
39
+ disable = query[:where].any? { |where_group| where_group.or? }
40
+ logger.info "Disabling index since an or was used" if disable && ENV['DYNOMITE_DEBUG']
41
+ disable
42
+ end
43
+
44
+ def disable_index_for_not?(index)
45
+ disable = query[:where].any? do |where_group|
46
+ x = where_group.fields & index.fields
47
+ !x.empty? && where_group.not?
48
+ end
49
+ logger.info "Disabling index since a not was used for the index" if disable && ENV['DYNOMITE_DEBUG']
50
+ disable
51
+ end
52
+
53
+ def disable_index_for_consistent_read?(index)
54
+ if query.key?(:consistent_read)
55
+ if index.nil?
56
+ true # must use scan for consistent read
57
+ elsif index.primary?
58
+ false # can use index for consistent ready for the primary key index only
59
+ else
60
+ true # must use scan for GSI indexes
61
+ end
62
+ else
63
+ false
64
+ end
65
+ end
66
+
67
+ # full field names with operator
68
+ def all_where_fields
69
+ query[:where].map(&:keys).flatten.map(&:to_s)
70
+ end
71
+
72
+ def all_where_field_names
73
+ all_where_fields.map { |k| k.split('.').first }
74
+ end
75
+
76
+ def normalize_expression_path(path)
77
+ path.split('.').map do |field|
78
+ field.starts_with?('#') ? field : field.prepend('#')
79
+ end.join('.')
80
+ end
81
+
82
+ def normalize_project_expression(args)
83
+ project_expression = []
84
+ args.map do |element|
85
+ if element.is_a?(String)
86
+ project_expression += element.split(',').map(&:strip)
87
+ else
88
+ project_expression << element.to_s
89
+ end
90
+ end
91
+ project_expression
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,34 @@
1
+ class Dynomite::Item::Query::Params
2
+ class KeyCondition < Base
3
+ def initialize(relation, index, partition_key_field, sort_key_field)
4
+ @relation, @index, @partition_key_field, @sort_key_field = relation, index, partition_key_field, sort_key_field
5
+ @expressions = []
6
+ end
7
+
8
+ def expression
9
+ build
10
+ join_expressions
11
+ end
12
+
13
+ def build
14
+ with_where_groups do |where_group|
15
+ expression = where_group.build_compare_expression_if do |field|
16
+ @index.fields.include?(field)
17
+ end
18
+ next unless expression
19
+ @expressions << expression
20
+ end
21
+ end
22
+ memoize :build
23
+
24
+ def full_primary_key_in_query?
25
+ field_names = all_where_field_names
26
+ if @sort_key_field
27
+ field_names.include?(@sort_key_field) && field_names.include?(@partition_key_field)
28
+ else
29
+ field_names.include?(@partition_key_field)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,115 @@
1
+ module Dynomite::Item::Query
2
+ class Params
3
+ extend Memoist
4
+ include Dynomite::Types
5
+ include Helpers
6
+
7
+ attr_reader :source
8
+ delegate :partition_key_field, :sort_key_field, :table_name, to: :source
9
+
10
+ def initialize(relation, source)
11
+ @relation, @source = relation, source
12
+ @query = relation.query
13
+ end
14
+
15
+ # key condition
16
+ # 1. primary key highest precedence
17
+ # 2. index
18
+ # filter expression
19
+ # 1. if field used in key condition
20
+ # 2. then don’t use in filter expression
21
+ # attributes
22
+ # 1. all values will be mapped over
23
+ # 2. will be in key condition or filter expression
24
+ # index name
25
+ # 1. if key condition set
26
+ # 2. unless primary key
27
+ def to_h
28
+ # set @index first. used throughout class
29
+ @index = index_finder.find(@query[:index_name])
30
+ @index = nil if scan_required?(@index) # IE: NOT operator on where field
31
+
32
+ # must build in this order
33
+ build_key_condition_expression if @index # must build first
34
+ build_filter_expression
35
+ build_attributes # must build last
36
+
37
+ @params = {
38
+ expression_attribute_names: @expression_attribute_names, # both scan and query
39
+ expression_attribute_values: @expression_attribute_values, # both scan and query
40
+ table_name: table_name,
41
+ }
42
+
43
+ @params[:filter_expression] = @filter_expression # both scan and query
44
+ @params[:key_condition_expression] = @key_condition_expression # query only. required
45
+
46
+ # primary index does not have a name but they are added to the @key_condition_expression
47
+ @params[:index_name] = @index.index_name if @index && !@index.primary? # both scan and query can use index
48
+
49
+ @params.reject! { |k,v| v.blank? }
50
+
51
+ # scan_index_forward after reject! so it's not removed
52
+ @params[:scan_index_forward] = !!@query[:scan_index_forward] if @query.key?(:scan_index_forward)
53
+ @params[:limit] = @query[:limit] if @query.key?(:limit)
54
+ @params[:projection_expression] = projection_expression if projection_expression
55
+ @params[:consistent_read] = @query[:consistent_read] if @query.key?(:consistent_read)
56
+ @params[:exclusive_start_key] = @query[:exclusive_start_key] if @query.key?(:exclusive_start_key)
57
+
58
+ log_index_info
59
+ @params
60
+ end
61
+
62
+ def projection_expression
63
+ return if @query[:projection_expression].nil?
64
+ projection_expression = normalize_project_expression(@query[:projection_expression])
65
+ projection_expression.map do |field|
66
+ '#'+field
67
+ end.join(", ")
68
+ end
69
+
70
+ # key_condition_expression is the most restrictive way to query.
71
+ # It requires the primary key and sort key.
72
+ # It also requires either the primary key or an index.
73
+ # Otherwise, we do not set it at all.
74
+ #
75
+ # So we'll build this first and then add the other expressions to it.
76
+ #
77
+ # key condition
78
+ # 1. primary key highest precedence
79
+ # 2. index
80
+ # 3. track fields used
81
+ def build_key_condition_expression
82
+ key_condition = KeyCondition.new(@relation, @index, partition_key_field, sort_key_field)
83
+ @key_condition_expression = key_condition.expression
84
+ end
85
+
86
+ def build_filter_expression
87
+ filter = Filter.new(@relation, @index)
88
+ @filter_expression = filter.expression
89
+ end
90
+
91
+ def build_attributes
92
+ expression_attribute = ExpressionAttribute.new(@relation)
93
+ @expression_attribute_names = expression_attribute.names
94
+ @expression_attribute_values = expression_attribute.values
95
+ end
96
+
97
+ def log_index_info
98
+ return unless ENV['DYNOMITE_DEBUG']
99
+
100
+ if @index
101
+ Dynomite.logger.info "Index used #{@index.index_name}"
102
+ elsif @params[:@expression_attribute_names]
103
+ attributes_list = @params[:@expression_attribute_names].values.join(", ")
104
+ Dynomite.logger.info "Not using index. None found for the attributes: #{attributes_list}"
105
+ else
106
+ Dynomite.logger.info "Not using index. @params #{@params.inspect}"
107
+ end
108
+ end
109
+
110
+ def index_finder
111
+ Dynomite::Item::Indexes::Finder.new(@source, @query)
112
+ end
113
+ memoize :index_finder
114
+ end
115
+ end
@@ -0,0 +1,72 @@
1
+ module Dynomite::Item::Query::Partiql
2
+ class Executer
3
+ include Dynomite::Client
4
+
5
+ def initialize(source)
6
+ @source = source # source is the model class. IE: Post User etc
7
+ end
8
+
9
+ # Execute PartiQL query
10
+ #
11
+ # AWS Docs:
12
+ # - https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#execute_statement-instance_method
13
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.select.html
14
+ #
15
+ # resp = client.execute_statement({
16
+ # statement: "PartiQLStatement", # required
17
+ # parameters: ["value"], # value <Hash,Array,String,Numeric,Boolean,IO,Set,nil>
18
+ # consistent_read: false,
19
+ # next_token: "PartiQLNextToken",
20
+ # return_consumed_capacity: "INDEXES", # accepts INDEXES, TOTAL, NONE
21
+ # limit: 1,
22
+ # return_values_on_condition_check_failure: "ALL_OLD", # accepts ALL_OLD, NONE
23
+ # })
24
+ def call(statement, parameters = {}, options = {})
25
+ total_count = 0
26
+ # total_limit is the total limit across all pages
27
+ # For the AWS API call itself use the default limit and allow AWS to scan 1MB for page
28
+ total_limit = parameters.delete(:limit)
29
+ enumerator = Enumerator.new do |y|
30
+ next_token = :start
31
+ while next_token
32
+ if next_token && next_token != :start
33
+ options[:next_token] = next_token
34
+ end
35
+
36
+ params = { statement: statement }
37
+ params[:parameters] = parameters unless parameters.empty?
38
+ raw = options.delete(:raw)
39
+ params.merge!(options)
40
+ log_debug(params)
41
+ resp = client.execute_statement(params)
42
+ if raw
43
+ y.yield(resp.items)
44
+ else
45
+ page = resp.items.map { |i| build_item(i) }
46
+ y.yield(page)
47
+ end
48
+
49
+ # Track total_count across pages. If limit is set, then stop when we reach it.
50
+ # Remember the limit is per page for each API call, not total.
51
+ total_count += page.size
52
+ break if total_limit && total_count >= total_limit
53
+
54
+ next_token = resp.next_token
55
+ end
56
+ end
57
+ if statement =~ /^SELECT/i
58
+ enumerator.lazy.flat_map { |i| i } # lazy.flat_map flattens the array since yielding pages
59
+ # Returns a lazy enumerator: #<Enumerator::Lazy: ...>
60
+ else
61
+ # For non-SELECT statements: INSERT, UPDATE, DELETE
62
+ enumerator.first # call first to execute the query immediately
63
+ end
64
+ end
65
+
66
+ def build_item(i)
67
+ item = @source.new(i) # IE: Post.new(i)
68
+ item.new_record = false
69
+ item
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,67 @@
1
+ module Dynomite::Item::Query
2
+ module Partiql
3
+ extend ActiveSupport::Concern
4
+ class_methods do
5
+ # Example:
6
+ #
7
+ # Product.execute_pql('SELECT * FROM "demo-dev_products" WHERE name = ?', ['Laptop'])
8
+ #
9
+ # Note WHERE is required
10
+ def execute_pql(statement, parameters = {}, options = {})
11
+ Executer.new(self).call(statement, parameters, options)
12
+ end
13
+
14
+ # Example:
15
+ #
16
+ # Product.execute_pql('SELECT * FROM "demo-dev_products" WHERE name = ?', ['Laptop'])
17
+ # Product.find_by_pql('name = ?', ['Laptop'])
18
+ #
19
+ # Returns [Item, Item, ...] (lazy)
20
+ def find_by_pql(where, parameters = {}, options = {})
21
+ select_all(where, parameters, options.merge(raw: false))
22
+ end
23
+
24
+ # Example:
25
+ #
26
+ # Product.execute_pql('SELECT * FROM "demo-dev_products" WHERE name = ?', ['Laptop'])
27
+ # Product.select_all('name = ?', ['Laptop'])
28
+ #
29
+ # Returns [Hash, Hash, ...] (lazy)
30
+ def select_all(where, parameters = {}, options = {})
31
+ options[:raw] = true unless options.key?(:raw)
32
+ statement = %Q|SELECT * FROM "#{table_name}" WHERE #{where}|
33
+ execute_pql(statement, parameters, options)
34
+ end
35
+
36
+ # Example:
37
+ #
38
+ # Post.execute_pql('UPDATE "demo-dev_posts" SET title = ? WHERE id = ?', ['post 1b', 'post-gRssngpbm5OfDIwr'])
39
+ # Post.update_pql('SET title = ? WHERE id = ?', ['post 1c', 'post-gRssngpbm5OfDIwr'])
40
+ #
41
+ def update_pql(set_where, parameters, options = {})
42
+ statement = %Q|UPDATE "#{table_name}" #{set_where}|
43
+ execute_pql(statement, parameters, options).to_a # to_a to force the lazy Enumerator to execute
44
+ end
45
+
46
+ # Example:
47
+ #
48
+ # Post.execute_pql('DELETE FROM "demo-dev_posts" WHERE id = ?', ['post-2QXlmfHCKcPDsnJC'])
49
+ # Post.delete_pql('id = ?', ['post-2QXlmfHCKcPDsnJC'])
50
+ #
51
+ def delete_pql(where, parameters = {}, options = {})
52
+ statement = %Q|DELETE FROM "#{table_name}" WHERE #{where}|
53
+ execute_pql(statement, parameters, options).to_a # to_a to force the lazy Enumerator to execute
54
+ end
55
+
56
+ # Example:
57
+ #
58
+ # Post.execute_pql(%Q|INSERT INTO "demo-dev_posts" VALUE {'id': ?, 'title': ?}|, ['post-1', 'post 1'])
59
+ # Post.insert_pql("{'id': ?, 'title': ?}", ['post-3', 'post 3'])
60
+ #
61
+ def insert_pql(values, parameters = {}, options = {})
62
+ statement = %Q|INSERT INTO "#{table_name}" VALUE #{values}|
63
+ execute_pql(statement, parameters, options).to_a # to_a to force the lazy Enumerator to execute
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,125 @@
1
+ class Dynomite::Item::Query::Relation
2
+ # Builds up the query with methods like where and eventually executes Query or Scan.
3
+ module Chain
4
+ def where(args={})
5
+ @query[:where] << WhereGroup.new(self, args)
6
+ self
7
+ end
8
+ alias :and :where
9
+
10
+ def or(args={})
11
+ @query[:where] << WhereGroup.new(self, args, or: true)
12
+ self
13
+ end
14
+
15
+ def not(args={})
16
+ @query[:where] << WhereGroup.new(self, args, not: true)
17
+ self
18
+ end
19
+
20
+ def excluding(*args)
21
+ ids = args.map do |object|
22
+ object.is_a?(Dynomite::Item) ? object.id : object
23
+ end
24
+ self.not("id.in": ids)
25
+ end
26
+
27
+ def scan_index_forward(value=true)
28
+ @query[:scan_index_forward] = value
29
+ self
30
+ end
31
+
32
+ def scan_index_backward(value=true)
33
+ scan_index_forward(!value)
34
+ end
35
+
36
+ # The default limit for both Scan and Query in Amazon DynamoDB is 1 MB of data read.
37
+ # It'll stop per api call regardless of the limit you set once it hits 1MB.
38
+ def limit(value)
39
+ @query[:limit] = value
40
+ self
41
+ end
42
+
43
+ # Product.where(category: "Electronics").project("id, category").first
44
+ def project(*fields)
45
+ @query[:projection_expression] = fields
46
+ self
47
+ end
48
+ alias projection_expression project
49
+
50
+ # Disable use of index and query method. Force a scan method
51
+ def force_scan
52
+ @query[:force_scan] = true
53
+ self
54
+ end
55
+
56
+ # Note consistent read it supported with GSI.
57
+ # You may want to use force_scan if really need a consistent read.
58
+ def consistent_read(value=true)
59
+ @query[:consistent_read] = value
60
+ self
61
+ end
62
+ alias consistent consistent_read
63
+
64
+ def exclusive_start_key(hash)
65
+ @query[:exclusive_start_key] = hash
66
+ self
67
+ end
68
+ alias start_from exclusive_start_key
69
+ alias start_at exclusive_start_key
70
+ alias start exclusive_start_key
71
+
72
+ # Could add some magically behavior to strip off the -index if the index name is 3 characters long.
73
+ # but think that's even more obscure.
74
+ #
75
+ # suffix allows for shorter syntax:
76
+ #
77
+ # index_name('created_at') vs index_name('created_at-index')
78
+ #
79
+ # Note: Tried using the shorter index method name but it seems to conflict an index method.
80
+ # Even though Enumerable has an index method, it doesn't seem to be the one thats conflicting.
81
+ # It's somewhere else.
82
+ def index_name(name, suffix: 'index')
83
+ name = [name, suffix].compact.join('-') if !name.ends_with?('index') && suffix
84
+ @query[:index_name] = name.to_s
85
+ self
86
+ end
87
+
88
+ def warn_on_scan(value=true)
89
+ @warn_on_scan = value
90
+ self
91
+ end
92
+
93
+ def attribute_exists(path)
94
+ @query[:attribute_exists] << path
95
+ self
96
+ end
97
+
98
+ def attribute_not_exists(path)
99
+ @query[:attribute_not_exists] << path
100
+ self
101
+ end
102
+
103
+ def attribute_type(path, type)
104
+ @query[:attribute_type] << {path: path, type: type}
105
+ self
106
+ end
107
+
108
+ # This is a function that accepts a path and a value.
109
+ # This is different from the comparision operator. IE: ""
110
+ def begins_with(path, substr)
111
+ @query[:begins_with] << {path: path, substr: substr}
112
+ self
113
+ end
114
+
115
+ def contains(path, substr)
116
+ @query[:contains] << {path: path, substr: substr}
117
+ self
118
+ end
119
+
120
+ def size_fn(path, size)
121
+ @query[:size_fn] << {path: path, size: size}
122
+ self
123
+ end
124
+ end
125
+ end