dynamoid_advanced_where 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +100 -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 +119 -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
|