dynomite 1.2.7 → 2.0.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 (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