dynamoid-advanced-where 1.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 +7 -0
- data/.circleci/config.yml +97 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Appraisals +8 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +121 -0
- data/README.md +375 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dynamoid_advanced_where.gemspec +41 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/dynamoid_3.4.gemfile +8 -0
- data/gemfiles/dynamoid_3.4.gemfile.lock +118 -0
- data/gemfiles/dynamoid_latest.gemfile +8 -0
- data/gemfiles/dynamoid_latest.gemfile.lock +118 -0
- data/lib/dynamoid_advanced_where.rb +8 -0
- data/lib/dynamoid_advanced_where/batched_updater.rb +229 -0
- data/lib/dynamoid_advanced_where/filter_builder.rb +136 -0
- data/lib/dynamoid_advanced_where/integrations/model.rb +34 -0
- data/lib/dynamoid_advanced_where/nodes.rb +15 -0
- data/lib/dynamoid_advanced_where/nodes/and_node.rb +43 -0
- data/lib/dynamoid_advanced_where/nodes/base_node.rb +18 -0
- data/lib/dynamoid_advanced_where/nodes/equality_node.rb +37 -0
- data/lib/dynamoid_advanced_where/nodes/exists_node.rb +44 -0
- data/lib/dynamoid_advanced_where/nodes/field_node.rb +186 -0
- data/lib/dynamoid_advanced_where/nodes/greater_than_node.rb +25 -0
- data/lib/dynamoid_advanced_where/nodes/includes.rb +29 -0
- data/lib/dynamoid_advanced_where/nodes/less_than_node.rb +27 -0
- data/lib/dynamoid_advanced_where/nodes/literal_node.rb +28 -0
- data/lib/dynamoid_advanced_where/nodes/not.rb +35 -0
- data/lib/dynamoid_advanced_where/nodes/null_node.rb +25 -0
- data/lib/dynamoid_advanced_where/nodes/operation_node.rb +44 -0
- data/lib/dynamoid_advanced_where/nodes/or_node.rb +41 -0
- data/lib/dynamoid_advanced_where/nodes/root_node.rb +47 -0
- data/lib/dynamoid_advanced_where/nodes/subfield.rb +17 -0
- data/lib/dynamoid_advanced_where/query_builder.rb +47 -0
- data/lib/dynamoid_advanced_where/query_materializer.rb +73 -0
- data/lib/dynamoid_advanced_where/version.rb +3 -0
- metadata +216 -0
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './nodes/null_node'
|
4
|
+
|
5
|
+
module DynamoidAdvancedWhere
|
6
|
+
class FilterBuilder
|
7
|
+
VALID_COMPARETORS_FOR_RANGE_FILTER = [
|
8
|
+
Nodes::GreaterThanNode
|
9
|
+
].freeze
|
10
|
+
|
11
|
+
attr_accessor :expression_node, :klass
|
12
|
+
|
13
|
+
def initialize(root_node:, klass:)
|
14
|
+
self.expression_node = root_node.child_node
|
15
|
+
self.klass = klass
|
16
|
+
end
|
17
|
+
|
18
|
+
def index_nodes
|
19
|
+
[
|
20
|
+
extract_query_filter_node,
|
21
|
+
extract_range_key_node
|
22
|
+
].compact
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_query_filter
|
26
|
+
{
|
27
|
+
key_condition_expression: key_condition_expression
|
28
|
+
}.merge!(expression_filters)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_scan_filter
|
32
|
+
expression_filters
|
33
|
+
end
|
34
|
+
|
35
|
+
def must_scan?
|
36
|
+
!extract_query_filter_node.is_a?(Nodes::BaseNode)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def key_condition_expression
|
42
|
+
@key_condition_expression ||= [
|
43
|
+
extract_query_filter_node,
|
44
|
+
extract_range_key_node
|
45
|
+
].compact.map(&:to_expression).join(' AND ')
|
46
|
+
end
|
47
|
+
|
48
|
+
def expression_attribute_names
|
49
|
+
[
|
50
|
+
expression_node,
|
51
|
+
*index_nodes
|
52
|
+
].map(&:expression_attribute_names).inject({}, &:merge!)
|
53
|
+
end
|
54
|
+
|
55
|
+
def expression_attribute_values
|
56
|
+
[
|
57
|
+
expression_node,
|
58
|
+
*index_nodes
|
59
|
+
].map(&:expression_attribute_values).inject({}, &:merge!)
|
60
|
+
end
|
61
|
+
|
62
|
+
def expression_filters
|
63
|
+
{
|
64
|
+
filter_expression: expression_node.to_expression,
|
65
|
+
expression_attribute_names: expression_attribute_names,
|
66
|
+
expression_attribute_values: expression_attribute_values
|
67
|
+
}.delete_if { |_, v| v.nil? || v.empty? }
|
68
|
+
end
|
69
|
+
|
70
|
+
def extract_query_filter_node
|
71
|
+
@extract_query_filter_node ||=
|
72
|
+
case expression_node
|
73
|
+
when Nodes::EqualityNode
|
74
|
+
node = expression_node
|
75
|
+
if field_node_valid_for_key_filter(expression_node)
|
76
|
+
self.expression_node = Nodes::NullNode.new
|
77
|
+
node
|
78
|
+
end
|
79
|
+
when Nodes::AndNode
|
80
|
+
id_filters = expression_node.child_nodes.select do |i|
|
81
|
+
field_node_valid_for_key_filter(i)
|
82
|
+
end
|
83
|
+
|
84
|
+
if id_filters.length == 1
|
85
|
+
self.expression_node = Nodes::AndNode.new(
|
86
|
+
*(expression_node.child_nodes - id_filters)
|
87
|
+
)
|
88
|
+
|
89
|
+
id_filters.first
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def field_node_valid_for_key_filter(node)
|
95
|
+
node.is_a?(Nodes::EqualityNode) &&
|
96
|
+
node.lh_operation.is_a?(Nodes::FieldNode) &&
|
97
|
+
node.lh_operation.field_path.length == 1 &&
|
98
|
+
node.lh_operation.field_path[0].to_s == hash_key
|
99
|
+
end
|
100
|
+
|
101
|
+
def extract_range_key_node
|
102
|
+
return unless extract_query_filter_node
|
103
|
+
|
104
|
+
@extract_range_key_node ||=
|
105
|
+
case expression_node
|
106
|
+
when Nodes::AndNode
|
107
|
+
id_filters = expression_node.child_nodes.select do |i|
|
108
|
+
field_node_valid_for_range_filter(i)
|
109
|
+
end
|
110
|
+
|
111
|
+
if id_filters.length == 1
|
112
|
+
self.expression_node = Nodes::AndNode.new(
|
113
|
+
*(expression_node.child_nodes - id_filters)
|
114
|
+
)
|
115
|
+
|
116
|
+
id_filters.first
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def field_node_valid_for_range_filter(node)
|
122
|
+
node.lh_operation.is_a?(Nodes::FieldNode) &&
|
123
|
+
node.lh_operation.field_path.length == 1 &&
|
124
|
+
node.lh_operation.field_path[0].to_s == range_key &&
|
125
|
+
VALID_COMPARETORS_FOR_RANGE_FILTER.any? { |type| node.is_a?(type) }
|
126
|
+
end
|
127
|
+
|
128
|
+
def hash_key
|
129
|
+
@hash_key ||= klass.hash_key.to_s
|
130
|
+
end
|
131
|
+
|
132
|
+
def range_key
|
133
|
+
@range_key ||= klass.range_key.to_s
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dynamoid_advanced_where/query_builder'
|
4
|
+
|
5
|
+
module DynamoidAdvancedWhere
|
6
|
+
# Allows classes to be queried by where, all, first, and each and return criteria chains.
|
7
|
+
module Integrations
|
8
|
+
module Model
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
def advanced_where(&blk)
|
13
|
+
DynamoidAdvancedWhere::QueryBuilder.new(klass: self, &blk)
|
14
|
+
end
|
15
|
+
|
16
|
+
def batch_update
|
17
|
+
advanced_where {}.batch_update
|
18
|
+
end
|
19
|
+
|
20
|
+
def where(*args, &blk)
|
21
|
+
if !args.empty?
|
22
|
+
raise ArgumentError, 'You may not specify where arguments and block' if blk
|
23
|
+
|
24
|
+
super(*args)
|
25
|
+
else
|
26
|
+
DynamoidAdvancedWhere::QueryBuilder.new(klass: self, &blk)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Dynamoid::Document.send(:include, DynamoidAdvancedWhere::Integrations::Model)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative './nodes/base_node'
|
2
|
+
require_relative './nodes/field_node'
|
3
|
+
require_relative './nodes/literal_node'
|
4
|
+
require_relative './nodes/operation_node'
|
5
|
+
require_relative './nodes/root_node'
|
6
|
+
require_relative './nodes/and_node'
|
7
|
+
require_relative './nodes/or_node'
|
8
|
+
|
9
|
+
require_relative './nodes/equality_node'
|
10
|
+
require_relative './nodes/exists_node'
|
11
|
+
require_relative './nodes/includes'
|
12
|
+
|
13
|
+
require_relative './nodes/greater_than_node'
|
14
|
+
|
15
|
+
require_relative './nodes/less_than_node'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DynamoidAdvancedWhere
|
4
|
+
module Nodes
|
5
|
+
class AndNode < BaseNode
|
6
|
+
include Concerns::Negatable
|
7
|
+
attr_accessor :child_nodes
|
8
|
+
|
9
|
+
def initialize(*child_nodes)
|
10
|
+
self.child_nodes = child_nodes.freeze
|
11
|
+
freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_expression
|
15
|
+
return if child_nodes.empty?
|
16
|
+
|
17
|
+
"(#{child_nodes.map(&:to_expression).join(') and (')})"
|
18
|
+
end
|
19
|
+
|
20
|
+
def expression_attribute_names
|
21
|
+
child_nodes.map(&:expression_attribute_names).inject({}, &:merge!)
|
22
|
+
end
|
23
|
+
|
24
|
+
def expression_attribute_values
|
25
|
+
child_nodes.map(&:expression_attribute_values).inject({}, &:merge!)
|
26
|
+
end
|
27
|
+
|
28
|
+
def and(other_value)
|
29
|
+
AndNode.new(other_value, *child_nodes)
|
30
|
+
end
|
31
|
+
alias & and
|
32
|
+
end
|
33
|
+
|
34
|
+
module Concerns
|
35
|
+
module SupportsLogicalAnd
|
36
|
+
def and(other_value)
|
37
|
+
AndNode.new(self, other_value)
|
38
|
+
end
|
39
|
+
alias & and
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './operation_node'
|
4
|
+
require_relative './not'
|
5
|
+
|
6
|
+
module DynamoidAdvancedWhere
|
7
|
+
module Nodes
|
8
|
+
class EqualityNode < OperationNode
|
9
|
+
include Concerns::Negatable
|
10
|
+
|
11
|
+
self.operator = '='
|
12
|
+
end
|
13
|
+
|
14
|
+
module Concerns
|
15
|
+
module SupportsEquality
|
16
|
+
def eq(other_value)
|
17
|
+
val = if respond_to?(:parse_right_hand_side)
|
18
|
+
parse_right_hand_side(other_value)
|
19
|
+
else
|
20
|
+
other_value
|
21
|
+
end
|
22
|
+
|
23
|
+
EqualityNode.new(
|
24
|
+
lh_operation: self,
|
25
|
+
rh_operation: LiteralNode.new(val)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
alias == eq
|
29
|
+
|
30
|
+
def not_eq(other_value)
|
31
|
+
eq(other_value).negate
|
32
|
+
end
|
33
|
+
alias != not_eq
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module DynamoidAdvancedWhere
|
6
|
+
module Nodes
|
7
|
+
class ExistsNode < BaseNode
|
8
|
+
include Concerns::Negatable
|
9
|
+
|
10
|
+
attr_accessor :field_node, :prefix
|
11
|
+
def initialize(field_node:)
|
12
|
+
self.field_node = field_node
|
13
|
+
self.prefix = SecureRandom.hex
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_expression
|
18
|
+
"NOT(
|
19
|
+
attribute_not_exists(#{field_node.to_expression})
|
20
|
+
or #{field_node.to_expression} = :#{prefix}
|
21
|
+
)"
|
22
|
+
end
|
23
|
+
|
24
|
+
def expression_attribute_names
|
25
|
+
field_node.expression_attribute_names
|
26
|
+
end
|
27
|
+
|
28
|
+
def expression_attribute_values
|
29
|
+
{
|
30
|
+
":#{prefix}" => nil
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module Concerns
|
36
|
+
module SupportsExistance
|
37
|
+
def exists?
|
38
|
+
ExistsNode.new(field_node: self)
|
39
|
+
end
|
40
|
+
alias present? exists?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './equality_node'
|
4
|
+
require_relative './greater_than_node'
|
5
|
+
require_relative './exists_node'
|
6
|
+
require_relative './includes'
|
7
|
+
require_relative './subfield'
|
8
|
+
|
9
|
+
module DynamoidAdvancedWhere
|
10
|
+
module Nodes
|
11
|
+
class FieldNode < BaseNode
|
12
|
+
include Concerns::SupportsEquality
|
13
|
+
include Concerns::SupportsExistance
|
14
|
+
|
15
|
+
attr_accessor :field_path, :attr_prefix
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def create_node(field_path:, attr_config:)
|
19
|
+
specific_klass = FIELD_MAPPING.detect do |config, _type|
|
20
|
+
config.respond_to?(:call) ? config.call(attr_config) : config <= attr_config
|
21
|
+
end&.last
|
22
|
+
|
23
|
+
unless specific_klass
|
24
|
+
raise ArgumentError, "unable to find field type for `#{attr_config}`"
|
25
|
+
end
|
26
|
+
|
27
|
+
specific_klass.new(field_path: field_path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(field_path:)
|
32
|
+
self.field_path = field_path.is_a?(Array) ? field_path : [field_path]
|
33
|
+
self.attr_prefix = SecureRandom.hex
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_expression
|
38
|
+
String.new.tap do |s|
|
39
|
+
field_path.collect.with_index do |segment, i|
|
40
|
+
if segment.is_a?(Integer)
|
41
|
+
s << "[#{segment}]"
|
42
|
+
else
|
43
|
+
s << '.' unless s.blank?
|
44
|
+
s << "##{attr_prefix}#{i}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def expression_attribute_names
|
51
|
+
field_path.each_with_object({}).with_index do |(segment, hsh), i|
|
52
|
+
next if segment.is_a?(Integer)
|
53
|
+
|
54
|
+
hsh["##{attr_prefix}#{i}"] = segment
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def expression_attribute_values
|
59
|
+
{}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class StringAttributeNode < FieldNode
|
64
|
+
include Concerns::SupportsIncludes
|
65
|
+
end
|
66
|
+
class NativeBooleanAttributeNode < FieldNode; end
|
67
|
+
|
68
|
+
class StringBooleanAttributeNode < FieldNode
|
69
|
+
def parse_right_hand_side(val)
|
70
|
+
val ? 't' : 'f'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class NumberAttributeNode < FieldNode
|
75
|
+
include Concerns::SupportsGreaterThan
|
76
|
+
|
77
|
+
ALLOWED_COMPARISON_TYPES = [
|
78
|
+
Numeric
|
79
|
+
].freeze
|
80
|
+
|
81
|
+
def parse_right_hand_side(val)
|
82
|
+
unless ALLOWED_COMPARISON_TYPES.detect { |k| val.is_a?(k) }
|
83
|
+
raise ArgumentError, "unable to compare number to `#{val.class}`"
|
84
|
+
end
|
85
|
+
|
86
|
+
val
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class NumericDatetimeAttributeNode < FieldNode
|
91
|
+
include Concerns::SupportsGreaterThan
|
92
|
+
|
93
|
+
def parse_right_hand_side(val)
|
94
|
+
if val.is_a?(Date)
|
95
|
+
val.to_time.to_i
|
96
|
+
elsif val.is_a?(Time)
|
97
|
+
val.to_f
|
98
|
+
else
|
99
|
+
raise ArgumentError, "unable to compare datetime to type #{val.class}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class NumericDateAttributeNode < FieldNode
|
105
|
+
include Concerns::SupportsGreaterThan
|
106
|
+
|
107
|
+
def parse_right_hand_side(val)
|
108
|
+
if !val.is_a?(Date) || val.is_a?(DateTime)
|
109
|
+
raise ArgumentError, "unable to compare date to type #{val.class}"
|
110
|
+
end
|
111
|
+
|
112
|
+
(val - Dynamoid::Persistence::UNIX_EPOCH_DATE).to_i
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class StringSetAttributeNode < FieldNode
|
117
|
+
include Concerns::SupportsIncludes
|
118
|
+
|
119
|
+
def parse_right_hand_side(val)
|
120
|
+
unless val.is_a?(String)
|
121
|
+
raise ArgumentError, "unable to compare date to type #{val.class}"
|
122
|
+
end
|
123
|
+
|
124
|
+
val
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class IntegerSetAttributeNode < FieldNode
|
129
|
+
include Concerns::SupportsIncludes
|
130
|
+
|
131
|
+
def parse_right_hand_side(val)
|
132
|
+
unless val.is_a?(Integer)
|
133
|
+
raise ArgumentError, "unable to compare date to type #{val.class}"
|
134
|
+
end
|
135
|
+
|
136
|
+
val
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class MapAttributeNode < FieldNode
|
141
|
+
include Concerns::SupportsSubFields
|
142
|
+
end
|
143
|
+
|
144
|
+
class RawAttributeNode < FieldNode
|
145
|
+
include Concerns::SupportsSubFields
|
146
|
+
end
|
147
|
+
|
148
|
+
class CustomClassAttributeNode < FieldNode
|
149
|
+
include Concerns::SupportsSubFields
|
150
|
+
end
|
151
|
+
|
152
|
+
FIELD_MAPPING = {
|
153
|
+
{ type: :string } => StringAttributeNode,
|
154
|
+
{ type: :number } => NumberAttributeNode,
|
155
|
+
|
156
|
+
# Boolean Fields
|
157
|
+
{ type: :boolean, store_as_native_boolean: true } =>
|
158
|
+
NativeBooleanAttributeNode,
|
159
|
+
{ type: :boolean, store_as_native_boolean: false } =>
|
160
|
+
StringBooleanAttributeNode,
|
161
|
+
|
162
|
+
# Datetime fields
|
163
|
+
{ type: :datetime, store_as_string: true } => nil,
|
164
|
+
{ type: :datetime, store_as_string: false } => NumericDatetimeAttributeNode,
|
165
|
+
{ type: :datetime } => NumericDatetimeAttributeNode,
|
166
|
+
|
167
|
+
# Date fields
|
168
|
+
{ type: :date, store_as_string: true } => nil,
|
169
|
+
{ type: :date, store_as_string: false } => NumericDateAttributeNode,
|
170
|
+
{ type: :date } => NumericDateAttributeNode,
|
171
|
+
|
172
|
+
# Set Types
|
173
|
+
{ type: :set, of: :string } => StringSetAttributeNode,
|
174
|
+
{ type: :set, of: :integer } => IntegerSetAttributeNode,
|
175
|
+
|
176
|
+
# Map Types
|
177
|
+
{ type: :map } => MapAttributeNode,
|
178
|
+
|
179
|
+
# Raw Types
|
180
|
+
{ type: :raw } => RawAttributeNode,
|
181
|
+
|
182
|
+
# Custom Object
|
183
|
+
->(c) { c[:type].is_a?(Class) } => CustomClassAttributeNode
|
184
|
+
}.freeze
|
185
|
+
end
|
186
|
+
end
|