Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix field names with special characters in where conditions #812

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,46 +22,50 @@ def initialize(conditions, name_placeholders, value_placeholders, name_placehold
def build
clauses = @conditions.map do |name, attribute_conditions|
attribute_conditions.map do |operator, value|
name_or_placeholder = name_or_placeholder_for(name)
# replace attribute names with placeholders unconditionally to support
# - special characters (e.g. '.', ':', and '#') and
# - leading '_'
# See
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#Expressions.ExpressionAttributeNames.AttributeNamesContainingSpecialCharacters
name_placeholder = name_placeholder_for(name)

case operator
when :eq
"#{name_or_placeholder} = #{value_placeholder_for(value)}"
"#{name_placeholder} = #{value_placeholder_for(value)}"
when :ne
"#{name_or_placeholder} <> #{value_placeholder_for(value)}"
"#{name_placeholder} <> #{value_placeholder_for(value)}"
when :gt
"#{name_or_placeholder} > #{value_placeholder_for(value)}"
"#{name_placeholder} > #{value_placeholder_for(value)}"
when :lt
"#{name_or_placeholder} < #{value_placeholder_for(value)}"
"#{name_placeholder} < #{value_placeholder_for(value)}"
when :gte
"#{name_or_placeholder} >= #{value_placeholder_for(value)}"
"#{name_placeholder} >= #{value_placeholder_for(value)}"
when :lte
"#{name_or_placeholder} <= #{value_placeholder_for(value)}"
"#{name_placeholder} <= #{value_placeholder_for(value)}"
when :between
"#{name_or_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
"#{name_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
when :begins_with
"begins_with (#{name_or_placeholder}, #{value_placeholder_for(value)})"
"begins_with (#{name_placeholder}, #{value_placeholder_for(value)})"
when :in
list = value.map(&method(:value_placeholder_for)).join(' , ')
"#{name_or_placeholder} IN (#{list})"
"#{name_placeholder} IN (#{list})"
when :contains
"contains (#{name_or_placeholder}, #{value_placeholder_for(value)})"
"contains (#{name_placeholder}, #{value_placeholder_for(value)})"
when :not_contains
"NOT contains (#{name_or_placeholder}, #{value_placeholder_for(value)})"
"NOT contains (#{name_placeholder}, #{value_placeholder_for(value)})"
when :null
"attribute_not_exists (#{name_or_placeholder})"
"attribute_not_exists (#{name_placeholder})"
when :not_null
"attribute_exists (#{name_or_placeholder})"
"attribute_exists (#{name_placeholder})"
end
end
end.flatten

@expression = clauses.join(' AND ')
end

def name_or_placeholder_for(name)
return name unless name.upcase.in?(Dynamoid::AdapterPlugin::AwsSdkV3::RESERVED_WORDS)

def name_placeholder_for(name)
placeholder = @name_placeholder_sequence.call
@name_placeholders[placeholder] = name
placeholder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ def build
return if @names.nil? || @names.empty?

clauses = @names.map do |name|
if name.upcase.in?(Dynamoid::AdapterPlugin::AwsSdkV3::RESERVED_WORDS)
placeholder = @name_placeholder_sequence.call
@name_placeholders[placeholder] = name
placeholder
else
name.to_s
end
# replace attribute names with placeholders unconditionally to support
# - special characters (e.g. '.', ':', and '#') and
# - leading '_'
# See
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#Expressions.ExpressionAttributeNames.AttributeNamesContainingSpecialCharacters
placeholder = @name_placeholder_sequence.call
@name_placeholders[placeholder] = name
placeholder
end

@expression = clauses.join(' , ')
Expand Down
170 changes: 170 additions & 0 deletions spec/dynamoid/criteria/chain_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,30 @@ def request_params
expect(documents.map(&:id)).to eql ['1']
end

it 'allows conditions with attribute names containing special characters' do
model = new_class do
range :'sort:key'
end

model.create_table
put_attributes(model.table_name, id: '1', 'sort:key': 'c')

documents = model.where(id: '1', 'sort:key': 'c').to_a
expect(documents.map(&:id)).to eql ['1']
end

it 'allows conditions with attribute names starting with _' do
model = new_class do
range :_sortKey
end

model.create_table
put_attributes(model.table_name, id: '1', _sortKey: 'c')

documents = model.where(id: '1', _sortKey: 'c').to_a
expect(documents.map(&:id)).to eql ['1']
end

it 'raises error when operator is not supported' do
expect do
model.where(name: 'Bob', 'age.foo': 10).to_a
Expand Down Expand Up @@ -443,6 +467,30 @@ def request_params
expect(documents.map(&:id)).to eql ['1']
end

it 'allows conditions with attribute names containing special characters' do
model = new_class do
field :'last:name'
end

model.create_table
put_attributes(model.table_name, id: '1', 'last:name': 'c')

documents = model.where(id: '1', 'last:name': 'c').to_a
expect(documents.map(&:id)).to eql ['1']
end

it 'allows conditions with attribute names starting with _' do
model = new_class do
field :_lastName
end

model.create_table
put_attributes(model.table_name, id: '1', _lastName: 'c')

documents = model.where(id: '1', _lastName: 'c').to_a
expect(documents.map(&:id)).to eql ['1']
end

it 'raises error when operator is not supported' do
expect do
model.where(name: 'a', 'age.foo': 9).to_a
Expand Down Expand Up @@ -659,6 +707,30 @@ def request_params
expect(documents.map(&:id)).to eql ['1']
end

it 'allows conditions with attribute names containing special characters' do
model = new_class do
field :'last:name'
end

model.create_table
put_attributes(model.table_name, id: '1', 'last:name': 'c')

documents = model.where('last:name': 'c').to_a
expect(documents.map(&:id)).to eql ['1']
end

it 'allows conditions with attribute names starting with _' do
model = new_class do
field :_lastName
end

model.create_table
put_attributes(model.table_name, id: '1', _lastName: 'c')

documents = model.where(_lastName: 'c').to_a
expect(documents.map(&:id)).to eql ['1']
end

it 'raises error when operator is not supported' do
expect do
model.where('age.foo': 9).to_a
Expand Down Expand Up @@ -1903,6 +1975,62 @@ def request_params
expect(obj.attributes).to eq(bucket: 2)
end
end

context 'when attribute name contains special characters' do
let(:model) do
new_class do
field :'first:name'
end
end

it 'works with Scan' do
model.create('first:name': 'Alex')

chain = described_class.new(model)
expect(chain).to receive(:raw_pages_via_scan).and_call_original

obj, = chain.project(:'first:name').to_a
expect(obj.attributes).to eq('first:name': 'Alex')
end

it 'works with Query' do
object = model.create('first:name': 'Alex')

chain = described_class.new(model)
expect(chain).to receive(:raw_pages_via_query).and_call_original

obj, = chain.where(id: object.id).project(:'first:name').to_a
expect(obj.attributes).to eq('first:name': 'Alex')
end
end

context 'when attribute name starts with _' do
let(:model) do
new_class do
field :_name
end
end

it 'works with Scan' do
model.create(_name: 'Alex')

chain = described_class.new(model)
expect(chain).to receive(:raw_pages_via_scan).and_call_original

obj, = chain.project(:_name).to_a
expect(obj.attributes).to eq(_name: 'Alex')
end

it 'works with Query' do
object = model.create(_name: 'Alex')

chain = described_class.new(model)
expect(chain).to receive(:raw_pages_via_query).and_call_original

obj, = chain.where(id: object.id).project(:_name).to_a
expect(obj.attributes).to eq(_name: 'Alex')
end
end
end

describe '#pluck' do
Expand Down Expand Up @@ -2007,6 +2135,48 @@ def request_params
expect(model.where(id: object.id).pluck(:bucket)).to eq([1001])
end
end

context 'when attribute name contains special characters' do
let(:model) do
new_class do
field :'first:name'
end
end

it 'works with Scan' do
model.create('first:name': 'Alice')
model.create('first:name': 'Bob')

expect(model.pluck(:'first:name')).to contain_exactly('Alice', 'Bob')
end

it 'works with Query' do
object = model.create('first:name': 'Alice')

expect(model.where(id: object.id).pluck(:'first:name')).to eq(['Alice'])
end
end

context 'when attribute name starts with _' do
let(:model) do
new_class do
field :_name
end
end

it 'works with Scan' do
model.create(_name: 'Alice')
model.create(_name: 'Bob')

expect(model.pluck(:_name)).to contain_exactly('Alice', 'Bob')
end

it 'works with Query' do
object = model.create(_name: 'Alice')

expect(model.where(id: object.id).pluck(:_name)).to eq(['Alice'])
end
end
end

describe 'User' do
Expand Down
Loading