dynomite 1.2.7 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +17 -2
- data/CHANGELOG.md +18 -0
- data/Gemfile +1 -5
- data/LICENSE.txt +22 -0
- data/README.md +6 -190
- data/Rakefile +13 -1
- data/dynomite.gemspec +9 -2
- data/exe/dynomite +14 -0
- data/lib/dynomite/associations/association.rb +126 -0
- data/lib/dynomite/associations/belongs_to.rb +35 -0
- data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
- data/lib/dynomite/associations/has_many.rb +19 -0
- data/lib/dynomite/associations/has_one.rb +19 -0
- data/lib/dynomite/associations/many_association.rb +257 -0
- data/lib/dynomite/associations/single_association.rb +157 -0
- data/lib/dynomite/associations.rb +248 -0
- data/lib/dynomite/autoloader.rb +25 -0
- data/lib/dynomite/cli.rb +48 -0
- data/lib/dynomite/client.rb +118 -0
- data/lib/dynomite/command.rb +89 -0
- data/lib/dynomite/completer/script.rb +6 -0
- data/lib/dynomite/completer/script.sh +10 -0
- data/lib/dynomite/completer.rb +159 -0
- data/lib/dynomite/config.rb +39 -0
- data/lib/dynomite/core.rb +18 -19
- data/lib/dynomite/engine.rb +45 -0
- data/lib/dynomite/erb.rb +5 -3
- data/lib/dynomite/error.rb +12 -0
- data/lib/dynomite/help/completion.md +20 -0
- data/lib/dynomite/help/completion_script.md +3 -0
- data/lib/dynomite/help/migrate.md +3 -0
- data/lib/dynomite/help.rb +9 -0
- data/lib/dynomite/install.rb +4 -0
- data/lib/dynomite/item/abstract.rb +15 -0
- data/lib/dynomite/item/components.rb +33 -0
- data/lib/dynomite/item/dsl.rb +101 -0
- data/lib/dynomite/item/id.rb +41 -0
- data/lib/dynomite/item/indexes/finder.rb +58 -0
- data/lib/dynomite/item/indexes/index.rb +21 -0
- data/lib/dynomite/item/indexes/primary_index.rb +18 -0
- data/lib/dynomite/item/indexes.rb +25 -0
- data/lib/dynomite/item/locking.rb +53 -0
- data/lib/dynomite/item/magic_fields.rb +66 -0
- data/lib/dynomite/item/primary_key.rb +85 -0
- data/lib/dynomite/item/query/delegates.rb +28 -0
- data/lib/dynomite/item/query/params/base.rb +42 -0
- data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
- data/lib/dynomite/item/query/params/filter.rb +41 -0
- data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
- data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
- data/lib/dynomite/item/query/params/function/base.rb +33 -0
- data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
- data/lib/dynomite/item/query/params/function/contains.rb +7 -0
- data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
- data/lib/dynomite/item/query/params/helpers.rb +94 -0
- data/lib/dynomite/item/query/params/key_condition.rb +34 -0
- data/lib/dynomite/item/query/params.rb +115 -0
- data/lib/dynomite/item/query/partiql/executer.rb +72 -0
- data/lib/dynomite/item/query/partiql.rb +67 -0
- data/lib/dynomite/item/query/relation/chain.rb +125 -0
- data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
- data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
- data/lib/dynomite/item/query/relation/delete.rb +38 -0
- data/lib/dynomite/item/query/relation/ids.rb +21 -0
- data/lib/dynomite/item/query/relation/math.rb +19 -0
- data/lib/dynomite/item/query/relation/where_field.rb +32 -0
- data/lib/dynomite/item/query/relation/where_group.rb +78 -0
- data/lib/dynomite/item/query/relation.rb +127 -0
- data/lib/dynomite/item/query.rb +7 -0
- data/lib/dynomite/item/read/find.rb +196 -0
- data/lib/dynomite/item/read/find_with_event.rb +42 -0
- data/lib/dynomite/item/read.rb +90 -0
- data/lib/dynomite/item/sti.rb +43 -0
- data/lib/dynomite/item/table_namespace.rb +43 -0
- data/lib/dynomite/item/typecaster.rb +106 -0
- data/lib/dynomite/item/waiter_methods.rb +18 -0
- data/lib/dynomite/item/write/base.rb +15 -0
- data/lib/dynomite/item/write/delete_item.rb +14 -0
- data/lib/dynomite/item/write/put_item.rb +99 -0
- data/lib/dynomite/item/write/update_item.rb +73 -0
- data/lib/dynomite/item/write.rb +204 -0
- data/lib/dynomite/item.rb +113 -286
- data/lib/dynomite/migration/dsl/accessor.rb +19 -0
- data/lib/dynomite/migration/dsl/index/base.rb +42 -0
- data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
- data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
- data/lib/dynomite/migration/dsl/index.rb +72 -0
- data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
- data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
- data/lib/dynomite/migration/dsl.rb +89 -142
- data/lib/dynomite/migration/file_info.rb +28 -0
- data/lib/dynomite/migration/generator.rb +30 -16
- data/lib/dynomite/migration/helpers.rb +7 -0
- data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
- data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
- data/lib/dynomite/migration/runner.rb +178 -0
- data/lib/dynomite/migration/templates/create_table.rb +7 -23
- data/lib/dynomite/migration/templates/delete_table.rb +7 -0
- data/lib/dynomite/migration/templates/update_table.rb +3 -18
- data/lib/dynomite/migration.rb +53 -10
- data/lib/dynomite/reserved_words.rb +13 -3
- data/lib/dynomite/seed.rb +12 -0
- data/lib/dynomite/types.rb +22 -0
- data/lib/dynomite/version.rb +1 -1
- data/lib/dynomite/waiter.rb +40 -0
- data/lib/dynomite.rb +11 -17
- data/lib/generators/application_item/application_item_generator.rb +30 -0
- data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
- data/lib/jets/commands/dynamodb_command.rb +29 -0
- data/lib/jets/commands/help/generate.md +33 -0
- data/lib/jets/commands/help/migrate.md +3 -0
- metadata +201 -17
- data/docs/migrations/long-example.rb +0 -127
- data/docs/migrations/short-example.rb +0 -40
- data/lib/dynomite/db_config.rb +0 -121
- data/lib/dynomite/errors.rb +0 -15
- data/lib/dynomite/log.rb +0 -15
- data/lib/dynomite/migration/common.rb +0 -86
- data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
- data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
- data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
- 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,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
|