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