Skip to content

Commit 76c5522

Browse files
committed
Add NULL and NOT_NULL operators
1 parent 8b176b7 commit 76c5522

File tree

6 files changed

+86
-8
lines changed

6 files changed

+86
-8
lines changed

lib/dynamoid/adapter_plugin/aws_sdk_v3.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ class AwsSdkV3
2222
range_eq: 'EQ'
2323
}.freeze
2424

25-
# Don't implement NULL and NOT_NULL because it doesn't make seanse -
26-
# we declare schema in models
2725
FIELD_MAP = {
2826
eq: 'EQ',
2927
ne: 'NE',
@@ -35,7 +33,9 @@ class AwsSdkV3
3533
between: 'BETWEEN',
3634
in: 'IN',
3735
contains: 'CONTAINS',
38-
not_contains: 'NOT_CONTAINS'
36+
not_contains: 'NOT_CONTAINS',
37+
null: 'NULL',
38+
not_null: 'NOT_NULL',
3939
}.freeze
4040
HASH_KEY = 'HASH'
4141
RANGE_KEY = 'RANGE'
@@ -619,15 +619,15 @@ def result_item_to_hash(item)
619619
# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
620620
# @params [String] operator: value of RANGE_MAP or FIELD_MAP hash, e.g. "EQ", "LT" etc
621621
# @params [Object] value: scalar value or array/set
622-
def attribute_value_list(operator, value)
623-
self.class.attribute_value_list(operator, value)
624-
end
625-
626622
def self.attribute_value_list(operator, value)
627623
# For BETWEEN and IN operators we should keep value as is (it should be already an array)
624+
# NULL and NOT_NULL require absence of attribute list
628625
# For all the other operators we wrap the value with array
626+
# https://docs.aws.amazon.com/en_us/amazondynamodb/latest/developerguide/LegacyConditionalParameters.Conditions.html
629627
if %w[BETWEEN IN].include?(operator)
630628
[value].flatten
629+
elsif %w[NULL NOT_NULL].include?(operator)
630+
nil
631631
else
632632
[value]
633633
end

lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def scan_filter
7575
comparison_operator: AwsSdkV3::FIELD_MAP[cond.keys[0]],
7676
attribute_value_list: AwsSdkV3.attribute_value_list(AwsSdkV3::FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
7777
}
78+
# nil means operator doesn't require attribute value list
79+
conditions.delete(:attribute_value_list) if conditions[:attribute_value_list].nil?
7880
result[attr] = condition
7981
result
8082
end

lib/dynamoid/criteria/chain.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,13 @@ def field_hash(key)
271271
{ contains: val }
272272
when 'not_contains'
273273
{ not_contains: val }
274+
# NULL/NOT_NULL operators don't have parameters
275+
# So { null: true } means NULL check and { null: false } means NOT_NULL one
276+
# The same logic is used for { not_null: BOOL }
277+
when 'null'
278+
val ? { null: nil } : { not_null: nil }
279+
when 'not_null'
280+
val ? { not_null: nil } : { null: nil }
274281
end
275282

276283
{ name.to_sym => hash }
@@ -314,10 +321,15 @@ def range_query
314321
opts.merge(query_opts).merge(consistent_opts)
315322
end
316323

324+
# TODO casting should be operator aware
325+
# e.g. for NULL operator value should be boolean
326+
# and isn't related to an attribute own type
317327
def type_cast_condition_parameter(key, value)
318328
return value if %i[array set].include?(source.attributes[key.to_sym][:type])
319329

320-
if !value.respond_to?(:to_ary)
330+
if [true, false].include?(value) # Support argument for null/not_null operators
331+
value
332+
elsif !value.respond_to?(:to_ary)
321333
options = source.attributes[key.to_sym]
322334
value_casted = TypeCasting.cast_field(value, options)
323335
Dumping.dump_field(value_casted, options)

spec/dynamoid/criteria/chain_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,34 @@ def request_params
341341
expect(model.where(name: 'a', 'job_title.not_contains': 'Consul').all)
342342
.to contain_exactly(customer2)
343343
end
344+
345+
it 'supports null' do
346+
model.create_table
347+
348+
put_attributes(model.table_name, name: 'a', last_name: 'aa', age: 1)
349+
put_attributes(model.table_name, name: 'a', last_name: 'bb', age: 2)
350+
put_attributes(model.table_name, name: 'a', last_name: 'cc', )
351+
352+
documents = model.where(name: 'a', 'age.null': true).to_a
353+
expect(documents.map(&:last_name)).to contain_exactly('cc')
354+
355+
documents = model.where(name: 'a', 'age.null': false).to_a
356+
expect(documents.map(&:last_name)).to contain_exactly('aa', 'bb')
357+
end
358+
359+
it 'supports not_null' do
360+
model.create_table
361+
362+
put_attributes(model.table_name, name: 'a', last_name: 'aa', age: 1)
363+
put_attributes(model.table_name, name: 'a', last_name: 'bb', age: 2)
364+
put_attributes(model.table_name, name: 'a', last_name: 'cc', )
365+
366+
documents = model.where(name: 'a', 'age.not_null': true).to_a
367+
expect(documents.map(&:last_name)).to contain_exactly('aa', 'bb')
368+
369+
documents = model.where('age.not_null': false).to_a
370+
expect(documents.map(&:last_name)).to contain_exactly('cc')
371+
end
344372
end
345373

346374
# http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.ScanFilter.html?shortFooter=true
@@ -490,6 +518,34 @@ def request_params
490518
expect(model.where('job_title.not_contains': 'Consul').all)
491519
.to contain_exactly(customer2)
492520
end
521+
522+
it 'supports null' do
523+
model.create_table
524+
525+
put_attributes(model.table_name, id: '1', age: 1)
526+
put_attributes(model.table_name, id: '2', age: 2)
527+
put_attributes(model.table_name, id: '3')
528+
529+
documents = model.where('age.null': true).to_a
530+
expect(documents.map(&:id)).to contain_exactly('3')
531+
532+
documents = model.where('age.null': false).to_a
533+
expect(documents.map(&:id)).to contain_exactly('1', '2')
534+
end
535+
536+
it 'supports not_null' do
537+
model.create_table
538+
539+
put_attributes(model.table_name, id: '1', age: 1)
540+
put_attributes(model.table_name, id: '2', age: 2)
541+
put_attributes(model.table_name, id: '3')
542+
543+
documents = model.where('age.not_null': true).to_a
544+
expect(documents.map(&:id)).to contain_exactly('1', '2')
545+
546+
documents = model.where('age.not_null': false).to_a
547+
expect(documents.map(&:id)).to contain_exactly('3')
548+
end
493549
end
494550

495551
describe 'Lazy loading' do

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@
4747
config.include NewClassHelper
4848
config.include DumpingHelper
4949
config.include PersistenceHelper
50+
config.include ChainHelper
5051
config.include ActiveSupport::Testing::TimeHelpers
5152
end

spec/support/chain_helper.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module ChainHelper
4+
def put_attributes(table_name, attributes)
5+
Dynamoid.adapter.put_item(table_name, attributes)
6+
end
7+
end

0 commit comments

Comments
 (0)