From 51f9438e9840ff5071f0a9c2a6ada964341548e5 Mon Sep 17 00:00:00 2001 From: Sean McGrail Date: Wed, 16 Dec 2020 18:22:28 -0800 Subject: [PATCH] DynamoDB Expression Library (#981) --- feature/dynamodb/expression/condition.go | 1577 +++++++++++++++++ feature/dynamodb/expression/condition_test.go | 1510 ++++++++++++++++ feature/dynamodb/expression/doc.go | 47 + feature/dynamodb/expression/error.go | 59 + feature/dynamodb/expression/error_test.go | 51 + feature/dynamodb/expression/examples_test.go | 309 ++++ feature/dynamodb/expression/expression.go | 633 +++++++ .../dynamodb/expression/expression_test.go | 1101 ++++++++++++ feature/dynamodb/expression/go.mod | 16 + feature/dynamodb/expression/go.sum | 55 + feature/dynamodb/expression/key_condition.go | 561 ++++++ .../dynamodb/expression/key_condition_test.go | 419 +++++ feature/dynamodb/expression/operand.go | 656 +++++++ feature/dynamodb/expression/operand_test.go | 257 +++ feature/dynamodb/expression/projection.go | 148 ++ .../dynamodb/expression/projection_test.go | 213 +++ feature/dynamodb/expression/update.go | 391 ++++ feature/dynamodb/expression/update_test.go | 730 ++++++++ 18 files changed, 8733 insertions(+) create mode 100644 feature/dynamodb/expression/condition.go create mode 100644 feature/dynamodb/expression/condition_test.go create mode 100644 feature/dynamodb/expression/doc.go create mode 100644 feature/dynamodb/expression/error.go create mode 100644 feature/dynamodb/expression/error_test.go create mode 100644 feature/dynamodb/expression/examples_test.go create mode 100644 feature/dynamodb/expression/expression.go create mode 100644 feature/dynamodb/expression/expression_test.go create mode 100644 feature/dynamodb/expression/go.mod create mode 100644 feature/dynamodb/expression/go.sum create mode 100644 feature/dynamodb/expression/key_condition.go create mode 100644 feature/dynamodb/expression/key_condition_test.go create mode 100644 feature/dynamodb/expression/operand.go create mode 100644 feature/dynamodb/expression/operand_test.go create mode 100644 feature/dynamodb/expression/projection.go create mode 100644 feature/dynamodb/expression/projection_test.go create mode 100644 feature/dynamodb/expression/update.go create mode 100644 feature/dynamodb/expression/update_test.go diff --git a/feature/dynamodb/expression/condition.go b/feature/dynamodb/expression/condition.go new file mode 100644 index 00000000000..39e1103135e --- /dev/null +++ b/feature/dynamodb/expression/condition.go @@ -0,0 +1,1577 @@ +package expression + +import ( + "fmt" + "strings" +) + +// conditionMode specifies the types of the struct conditionBuilder, +// representing the different types of Conditions (i.e. And, Or, Between, ...) +type conditionMode int + +const ( + // unsetCond catches errors for unset ConditionBuilder structs + unsetCond conditionMode = iota + // equalCond represents the Equals Condition + equalCond + // notEqualCond represents the Not Equals Condition + notEqualCond + // lessThanCond represents the LessThan Condition + lessThanCond + // lessThanEqualCond represents the LessThanOrEqual Condition + lessThanEqualCond + // greaterThanCond represents the GreaterThan Condition + greaterThanCond + // greaterThanEqualCond represents the GreaterThanEqual Condition + greaterThanEqualCond + // andCond represents the Logical And Condition + andCond + // orCond represents the Logical Or Condition + orCond + // notCond represents the Logical Not Condition + notCond + // betweenCond represents the Between Condition + betweenCond + // inCond represents the In Condition + inCond + // attrExistsCond represents the Attribute Exists Condition + attrExistsCond + // attrNotExistsCond represents the Attribute Not Exists Condition + attrNotExistsCond + // attrTypeCond represents the Attribute Type Condition + attrTypeCond + // beginsWithCond represents the Begins With Condition + beginsWithCond + // containsCond represents the Contains Condition + containsCond +) + +// DynamoDBAttributeType specifies the type of an DynamoDB item attribute. This +// enum is used in the AttributeType() function in order to be explicit about +// the DynamoDB type that is being checked and ensure compile time checks. +// More Informatin at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions +type DynamoDBAttributeType string + +const ( + // String represents the DynamoDB String type + String DynamoDBAttributeType = "S" + // StringSet represents the DynamoDB String Set type + StringSet = "SS" + // Number represents the DynamoDB Number type + Number = "N" + // NumberSet represents the DynamoDB Number Set type + NumberSet = "NS" + // Binary represents the DynamoDB Binary type + Binary = "B" + // BinarySet represents the DynamoDB Binary Set type + BinarySet = "BS" + // Boolean represents the DynamoDB Boolean type + Boolean = "BOOL" + // Null represents the DynamoDB Null type + Null = "NULL" + // List represents the DynamoDB List type + List = "L" + // Map represents the DynamoDB Map type + Map = "M" +) + +// ConditionBuilder represents Condition Expressions and Filter Expressions +// in DynamoDB. ConditionBuilders are one of the building blocks of the Builder +// struct. Since Filter Expressions support all the same functions and formats +// as Condition Expressions, ConditionBuilders represents both types of +// Expressions. +// More Information at: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html +// More Information on Filter Expressions: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.FilterExpression +type ConditionBuilder struct { + operandList []OperandBuilder + conditionList []ConditionBuilder + mode conditionMode +} + +// Equal returns a ConditionBuilder representing the equality clause of the two +// argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the equal clause of the item attribute "foo" and +// // the value 5 +// condition := expression.Equal(expression.Name("foo"), expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Equal(expression.Name("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo = :five" +func Equal(left, right OperandBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{left, right}, + mode: equalCond, + } +} + +// Equal returns a ConditionBuilder representing the equality clause of the two +// argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the equal clause of the item attribute "foo" and +// // the value 5 +// condition := expression.Name("foo").Equal(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("foo").Equal(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo = :five" +func (nb NameBuilder) Equal(right OperandBuilder) ConditionBuilder { + return Equal(nb, right) +} + +// Equal returns a ConditionBuilder representing the equality clause of the two +// argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the equal clause of the item attribute "foo" and +// // the value 5 +// condition := expression.Value(5).Equal(expression.Name("foo")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Value(5).Equal(expression.Name("foo")) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// ":five = foo" +func (vb ValueBuilder) Equal(right OperandBuilder) ConditionBuilder { + return Equal(vb, right) +} + +// Equal returns a ConditionBuilder representing the equality clause of the two +// argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the equal clause of the size of the item +// // attribute "foo" and the value 5 +// condition := expression.Size(expression.Name("foo")).Equal(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("foo")).Equal(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "size (foo) = :five" +func (sb SizeBuilder) Equal(right OperandBuilder) ConditionBuilder { + return Equal(sb, right) +} + +// NotEqual returns a ConditionBuilder representing the not equal clause of the +// two argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the not equal clause of the item attribute "foo" +// // and the value 5 +// condition := expression.NotEqual(expression.Name("foo"), expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.NotEqual(expression.Name("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo <> :five" +func NotEqual(left, right OperandBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{left, right}, + mode: notEqualCond, + } +} + +// NotEqual returns a ConditionBuilder representing the not equal clause of the +// two argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the not equal clause of the item attribute "foo" +// // and the value 5 +// condition := expression.Name("foo").NotEqual(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("foo").NotEqual(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo <> :five" +func (nb NameBuilder) NotEqual(right OperandBuilder) ConditionBuilder { + return NotEqual(nb, right) +} + +// NotEqual returns a ConditionBuilder representing the not equal clause of the +// two argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the not equal clause of the item attribute "foo" +// // and the value 5 +// condition := expression.Value(5).NotEqual(expression.Name("foo")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Value(5).NotEqual(expression.Name("foo")) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// ":five <> foo" +func (vb ValueBuilder) NotEqual(right OperandBuilder) ConditionBuilder { + return NotEqual(vb, right) +} + +// NotEqual returns a ConditionBuilder representing the not equal clause of the +// two argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the not equal clause of the size of the item +// // attribute "foo" and the value 5 +// condition := expression.Size(expression.Name("foo")).NotEqual(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("foo")).NotEqual(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "size (foo) <> :five" +func (sb SizeBuilder) NotEqual(right OperandBuilder) ConditionBuilder { + return NotEqual(sb, right) +} + +// LessThan returns a ConditionBuilder representing the less than clause of the +// two argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the less than clause of the item attribute "foo" +// // and the value 5 +// condition := expression.LessThan(expression.Name("foo"), expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.LessThan(expression.Name("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo < :five" +func LessThan(left, right OperandBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{left, right}, + mode: lessThanCond, + } +} + +// LessThan returns a ConditionBuilder representing the less than clause of the +// two argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the less than clause of the item attribute "foo" +// // and the value 5 +// condition := expression.Name("foo").LessThan(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("foo").LessThan(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo < :five" +func (nb NameBuilder) LessThan(right OperandBuilder) ConditionBuilder { + return LessThan(nb, right) +} + +// LessThan returns a ConditionBuilder representing the less than clause of the +// two argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the less than clause of the item attribute "foo" +// // and the value 5 +// condition := expression.Value(5).LessThan(expression.Name("foo")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Value(5).LessThan(expression.Name("foo")) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// ":five < foo" +func (vb ValueBuilder) LessThan(right OperandBuilder) ConditionBuilder { + return LessThan(vb, right) +} + +// LessThan returns a ConditionBuilder representing the less than clause of the +// two argument OperandBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the less than clause of the size of the item +// // attribute "foo" and the value 5 +// condition := expression.Size(expression.Name("foo")).LessThan(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("foo")).LessThan(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "size (foo) < :five" +func (sb SizeBuilder) LessThan(right OperandBuilder) ConditionBuilder { + return LessThan(sb, right) +} + +// LessThanEqual returns a ConditionBuilder representing the less than equal to +// clause of the two argument OperandBuilders. The resulting ConditionBuilder +// can be used as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the less than equal to clause of the item +// // attribute "foo" and the value 5 +// condition := expression.LessThanEqual(expression.Name("foo"), expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.LessThanEqual(expression.Name("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo <= :five" +func LessThanEqual(left, right OperandBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{left, right}, + mode: lessThanEqualCond, + } +} + +// LessThanEqual returns a ConditionBuilder representing the less than equal to +// clause of the two argument OperandBuilders. The resulting ConditionBuilder +// can be used as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the less than equal to clause of the item +// // attribute "foo" and the value 5 +// condition := expression.Name("foo").LessThanEqual(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("foo").LessThanEqual(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo <= :five" +func (nb NameBuilder) LessThanEqual(right OperandBuilder) ConditionBuilder { + return LessThanEqual(nb, right) +} + +// LessThanEqual returns a ConditionBuilder representing the less than equal to +// clause of the two argument OperandBuilders. The resulting ConditionBuilder +// can be used as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the less than equal to clause of the item +// // attribute "foo" and the value 5 +// condition := expression.Value(5).LessThanEqual(expression.Name("foo")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Value(5).LessThanEqual(expression.Name("foo")) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// ":five <= foo" +func (vb ValueBuilder) LessThanEqual(right OperandBuilder) ConditionBuilder { + return LessThanEqual(vb, right) +} + +// LessThanEqual returns a ConditionBuilder representing the less than equal to +// clause of the two argument OperandBuilders. The resulting ConditionBuilder +// can be used as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the less than equal to clause of the size of the +// // item attribute "foo" and the value 5 +// condition := expression.Size(expression.Name("foo")).LessThanEqual(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("foo")).LessThanEqual(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "size (foo) <= :five" +func (sb SizeBuilder) LessThanEqual(right OperandBuilder) ConditionBuilder { + return LessThanEqual(sb, right) +} + +// GreaterThan returns a ConditionBuilder representing the greater than clause +// of the two argument OperandBuilders. The resulting ConditionBuilder can be +// used as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the greater than clause of the item attribute +// // "foo" and the value 5 +// condition := expression.GreaterThan(expression.Name("foo"), expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.GreaterThan(expression.Name("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo > :five" +func GreaterThan(left, right OperandBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{left, right}, + mode: greaterThanCond, + } +} + +// GreaterThan returns a ConditionBuilder representing the greater than clause +// of the two argument OperandBuilders. The resulting ConditionBuilder can be +// used as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the greater than clause of the item attribute +// // "foo" and the value 5 +// condition := expression.Name("foo").GreaterThan(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("foo").GreaterThan(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo > :five" +func (nb NameBuilder) GreaterThan(right OperandBuilder) ConditionBuilder { + return GreaterThan(nb, right) +} + +// GreaterThan returns a ConditionBuilder representing the greater than clause +// of the two argument OperandBuilders. The resulting ConditionBuilder can be +// used as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the greater than clause of the item attribute +// // "foo" and the value 5 +// condition := expression.Value(5).GreaterThan(expression.Name("foo")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Value(5).GreaterThan(expression.Name("foo")) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// ":five > foo" +func (vb ValueBuilder) GreaterThan(right OperandBuilder) ConditionBuilder { + return GreaterThan(vb, right) +} + +// GreaterThan returns a ConditionBuilder representing the greater than +// clause of the two argument OperandBuilders. The resulting ConditionBuilder +// can be used as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the greater than clause of the size of the item +// // attribute "foo" and the value 5 +// condition := expression.Size(expression.Name("foo")).GreaterThan(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("foo")).GreaterThan(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "size (foo) > :five" +func (sb SizeBuilder) GreaterThan(right OperandBuilder) ConditionBuilder { + return GreaterThan(sb, right) +} + +// GreaterThanEqual returns a ConditionBuilder representing the greater than +// equal to clause of the two argument OperandBuilders. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the greater than equal to clause of the item +// // attribute "foo" and the value 5 +// condition := expression.GreaterThanEqual(expression.Name("foo"), expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.GreaterThanEqual(expression.Name("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo >= :five" +func GreaterThanEqual(left, right OperandBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{left, right}, + mode: greaterThanEqualCond, + } +} + +// GreaterThanEqual returns a ConditionBuilder representing the greater than +// equal to clause of the two argument OperandBuilders. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the greater than equal to clause of the item +// // attribute "foo" and the value 5 +// condition := expression.Name("foo").GreaterThanEqual(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("foo").GreaterThanEqual(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo >= :five" +func (nb NameBuilder) GreaterThanEqual(right OperandBuilder) ConditionBuilder { + return GreaterThanEqual(nb, right) +} + +// GreaterThanEqual returns a ConditionBuilder representing the greater than +// equal to clause of the two argument OperandBuilders. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the greater than equal to clause of the item +// // attribute "foo" and the value 5 +// condition := expression.Value(5).GreaterThanEqual(expression.Name("foo")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Value(5).GreaterThanEqual(expression.Name("foo")) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// ":five >= foo" +func (vb ValueBuilder) GreaterThanEqual(right OperandBuilder) ConditionBuilder { + return GreaterThanEqual(vb, right) +} + +// GreaterThanEqual returns a ConditionBuilder representing the greater than +// equal to clause of the two argument OperandBuilders. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the greater than equal to clause of the size of +// // the item attribute "foo" and the value 5 +// condition := expression.Size(expression.Name("foo")).GreaterThanEqual(expression.Value(5)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("foo")).GreaterThanEqual(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "size (foo) >= :five" +func (sb SizeBuilder) GreaterThanEqual(right OperandBuilder) ConditionBuilder { + return GreaterThanEqual(sb, right) +} + +// And returns a ConditionBuilder representing the logical AND clause of the +// argument ConditionBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. Note that And() can take a variadic number of +// ConditionBuilders as arguments. +// +// Example: +// +// // condition represents the condition where the item attribute "Name" is +// // equal to value "Generic Name" AND the item attribute "Age" is less +// // than value 40 +// condition := expression.And(expression.Name("Name").Equal(expression.Value("Generic Name")), expression.Name("Age").LessThan(expression.Value(40))) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.And(expression.Name("Name").Equal(expression.Value("Generic Name")), expression.Name("Age").LessThan(expression.Value(40))) +// // Let #NAME, :name, and :forty be ExpressionAttributeName and +// // ExpressionAttributeValues representing the item attribute "Name", the +// // value "Generic Name", and the value 40 +// "(#NAME = :name) AND (Age < :forty)" +func And(left, right ConditionBuilder, other ...ConditionBuilder) ConditionBuilder { + other = append([]ConditionBuilder{left, right}, other...) + return ConditionBuilder{ + conditionList: other, + mode: andCond, + } +} + +// And returns a ConditionBuilder representing the logical AND clause of the +// argument ConditionBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. Note that And() can take a variadic number of +// ConditionBuilders as arguments. +// +// Example: +// +// // condition represents the condition where the item attribute "Name" is +// // equal to value "Generic Name" AND the item attribute "Age" is less +// // than value 40 +// condition := expression.Name("Name").Equal(expression.Value("Generic Name")).And(expression.Name("Age").LessThan(expression.Value(40))) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("Name").Equal(expression.Value("Generic Name")).And(expression.Name("Age").LessThan(expression.Value(40))) +// // Let #NAME, :name, and :forty be ExpressionAttributeName and +// // ExpressionAttributeValues representing the item attribute "Name", the +// // value "Generic Name", and the value 40 +// "(#NAME = :name) AND (Age < :forty)" +func (cb ConditionBuilder) And(right ConditionBuilder, other ...ConditionBuilder) ConditionBuilder { + return And(cb, right, other...) +} + +// Or returns a ConditionBuilder representing the logical OR clause of the +// argument ConditionBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. Note that Or() can take a variadic number of +// ConditionBuilders as arguments. +// +// Example: +// +// // condition represents the condition where the item attribute "Price" is +// // less than the value 100 OR the item attribute "Rating" is greater than +// // the value 8 +// condition := expression.Or(expression.Name("Price").Equal(expression.Value(100)), expression.Name("Rating").LessThan(expression.Value(8))) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Or(expression.Name("Price").Equal(expression.Value(100)), expression.Name("Rating").LessThan(expression.Value(8))) +// // Let :price and :rating be ExpressionAttributeValues representing the +// // the value 100 and value 8 respectively +// "(Price < :price) OR (Rating > :rating)" +func Or(left, right ConditionBuilder, other ...ConditionBuilder) ConditionBuilder { + other = append([]ConditionBuilder{left, right}, other...) + return ConditionBuilder{ + conditionList: other, + mode: orCond, + } +} + +// Or returns a ConditionBuilder representing the logical OR clause of the +// argument ConditionBuilders. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. Note that Or() can take a variadic number of +// ConditionBuilders as arguments. +// +// Example: +// +// // condition represents the condition where the item attribute "Price" is +// // less than the value 100 OR the item attribute "Rating" is greater than +// // the value 8 +// condition := expression.Name("Price").Equal(expression.Value(100)).Or(expression.Name("Rating").LessThan(expression.Value(8))) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("Price").Equal(expression.Value(100)).Or(expression.Name("Rating").LessThan(expression.Value(8))) +// // Let :price and :rating be ExpressionAttributeValues representing the +// // the value 100 and value 8 respectively +// "(Price < :price) OR (Rating > :rating)" +func (cb ConditionBuilder) Or(right ConditionBuilder, other ...ConditionBuilder) ConditionBuilder { + return Or(cb, right, other...) +} + +// Not returns a ConditionBuilder representing the logical NOT clause of the +// argument ConditionBuilder. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the item attribute "Name" +// // does not begin with "test" +// condition := expression.Not(expression.Name("Name").BeginsWith("test")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Not(expression.Name("Name").BeginsWith("test")) +// // Let :prefix be an ExpressionAttributeValue representing the value +// // "test" +// "NOT (begins_with (:prefix))" +func Not(conditionBuilder ConditionBuilder) ConditionBuilder { + return ConditionBuilder{ + conditionList: []ConditionBuilder{conditionBuilder}, + mode: notCond, + } +} + +// Not returns a ConditionBuilder representing the logical NOT clause of the +// argument ConditionBuilder. The resulting ConditionBuilder can be used as a +// part of other Condition Expressions or as an argument to the WithCondition() +// method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the item attribute "Name" +// // does not begin with "test" +// condition := expression.Name("Name").BeginsWith("test").Not() +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("Name").BeginsWith("test").Not() +// // Let :prefix be an ExpressionAttributeValue representing the value +// // "test" +// "NOT (begins_with (:prefix))" +func (cb ConditionBuilder) Not() ConditionBuilder { + return Not(cb) +} + +// Between returns a ConditionBuilder representing the result of the +// BETWEEN function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the value of the item +// // attribute "Rating" is between values 5 and 10 +// condition := expression.Between(expression.Name("Rating"), expression.Value(5), expression.Value(10)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Between(expression.Name("Rating"), expression.Value(5), expression.Value(10)) +// // Let :five and :ten be ExpressionAttributeValues representing the value +// // 5 and the value 10 +// "Rating BETWEEN :five AND :ten" +func Between(op, lower, upper OperandBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{op, lower, upper}, + mode: betweenCond, + } +} + +// Between returns a ConditionBuilder representing the result of the +// BETWEEN function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the value of the item +// // attribute "Rating" is between values 5 and 10 +// condition := expression.Name("Rating").Between(expression.Value(5), expression.Value(10)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("Rating").Between(expression.Value(5), expression.Value(10)) +// // Let :five and :ten be ExpressionAttributeValues representing the value +// // 5 and the value 10 +// "Rating BETWEEN :five AND :ten" +func (nb NameBuilder) Between(lower, upper OperandBuilder) ConditionBuilder { + return Between(nb, lower, upper) +} + +// Between returns a ConditionBuilder representing the result of the +// BETWEEN function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the value 6 is between values +// // 5 and 10 +// condition := expression.Value(6).Between(expression.Value(5), expression.Value(10)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Value(6).Between(expression.Value(5), expression.Value(10)) +// // Let :six, :five and :ten be ExpressionAttributeValues representing the +// // values 6, 5, and 10 respectively +// ":six BETWEEN :five AND :ten" +func (vb ValueBuilder) Between(lower, upper OperandBuilder) ConditionBuilder { + return Between(vb, lower, upper) +} + +// Between returns a ConditionBuilder representing the result of the +// BETWEEN function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the size of the item +// // attribute "InviteList" is between values 5 and 10 +// condition := expression.Size(expression.Name("InviteList")).Between(expression.Value(5), expression.Value(10)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("InviteList")).Between(expression.Value(5), expression.Value(10)) +// // Let :five and :ten be ExpressionAttributeValues representing the value +// // 5 and the value 10 +// "size (InviteList) BETWEEN :five AND :ten" +func (sb SizeBuilder) Between(lower, upper OperandBuilder) ConditionBuilder { + return Between(sb, lower, upper) +} + +// In returns a ConditionBuilder representing the result of the IN function +// in DynamoDB Condition Expressions. The resulting ConditionBuilder can be used +// as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the value of the item +// // attribute "Color" is checked against the list of colors "red", +// // "green", and "blue". +// condition := expression.In(expression.Name("Color"), expression.Value("red"), expression.Value("green"), expression.Value("blue")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.In(expression.Name("Color"), expression.Value("red"), expression.Value("green"), expression.Value("blue")) +// // Let :red, :green, :blue be ExpressionAttributeValues representing the +// // values "red", "green", and "blue" respectively +// "Color IN (:red, :green, :blue)" +func In(left, right OperandBuilder, other ...OperandBuilder) ConditionBuilder { + other = append([]OperandBuilder{left, right}, other...) + return ConditionBuilder{ + operandList: other, + mode: inCond, + } +} + +// In returns a ConditionBuilder representing the result of the IN function +// in DynamoDB Condition Expressions. The resulting ConditionBuilder can be used +// as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the value of the item +// // attribute "Color" is checked against the list of colors "red", +// // "green", and "blue". +// condition := expression.Name("Color").In(expression.Value("red"), expression.Value("green"), expression.Value("blue")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("Color").In(expression.Value("red"), expression.Value("green"), expression.Value("blue")) +// // Let :red, :green, :blue be ExpressionAttributeValues representing the +// // values "red", "green", and "blue" respectively +// "Color IN (:red, :green, :blue)" +func (nb NameBuilder) In(right OperandBuilder, other ...OperandBuilder) ConditionBuilder { + return In(nb, right, other...) +} + +// In returns a ConditionBuilder representing the result of the IN function +// TODO change this one +// in DynamoDB Condition Expressions. The resulting ConditionBuilder can be used +// as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the value "yellow" is checked +// // against the list of colors "red", "green", and "blue". +// condition := expression.Value("yellow").In(expression.Value("red"), expression.Value("green"), expression.Value("blue")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Value("yellow").In(expression.Value("red"), expression.Value("green"), expression.Value("blue")) +// // Let :yellow, :red, :green, :blue be ExpressionAttributeValues +// // representing the values "yellow", "red", "green", and "blue" +// // respectively +// ":yellow IN (:red, :green, :blue)" +func (vb ValueBuilder) In(right OperandBuilder, other ...OperandBuilder) ConditionBuilder { + return In(vb, right, other...) +} + +// In returns a ConditionBuilder representing the result of the IN function +// in DynamoDB Condition Expressions. The resulting ConditionBuilder can be used +// as a part of other Condition Expressions or as an argument to the +// WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the condition where the size of the item +// // attribute "Donuts" is checked against the list of numbers 12, 24, and +// // 36. +// condition := expression.Size(expression.Name("Donuts")).In(expression.Value(12), expression.Value(24), expression.Value(36)) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("Donuts")).In(expression.Value(12), expression.Value(24), expression.Value(36)) +// // Let :dozen, :twoDozen, :threeDozen be ExpressionAttributeValues +// // representing the values 12, 24, and 36 respectively +// "size (Donuts) IN (12, 24, 36)" +func (sb SizeBuilder) In(right OperandBuilder, other ...OperandBuilder) ConditionBuilder { + return In(sb, right, other...) +} + +// AttributeExists returns a ConditionBuilder representing the result of the +// attribute_exists function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "Age" exists or not +// condition := expression.AttributeExists(expression.Name("Age")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.AttributeExists(expression.Name("Age")) +// "attribute_exists (Age))" +func AttributeExists(nameBuilder NameBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{nameBuilder}, + mode: attrExistsCond, + } +} + +// AttributeExists returns a ConditionBuilder representing the result of the +// attribute_exists function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "Age" exists or not +// condition := expression.Name("Age").AttributeExists() +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("Age").AttributeExists() +// "attribute_exists (Age))" +func (nb NameBuilder) AttributeExists() ConditionBuilder { + return AttributeExists(nb) +} + +// AttributeNotExists returns a ConditionBuilder representing the result of +// the attribute_not_exists function in DynamoDB Condition Expressions. The +// resulting ConditionBuilder can be used as a part of other Condition +// Expressions or as an argument to the WithCondition() method for the Builder +// struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "Age" exists or not +// condition := expression.AttributeNotExists(expression.Name("Age")) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.AttributeNotExists(expression.Name("Age")) +// "attribute_not_exists (Age))" +func AttributeNotExists(nameBuilder NameBuilder) ConditionBuilder { + return ConditionBuilder{ + operandList: []OperandBuilder{nameBuilder}, + mode: attrNotExistsCond, + } +} + +// AttributeNotExists returns a ConditionBuilder representing the result of +// the attribute_not_exists function in DynamoDB Condition Expressions. The +// resulting ConditionBuilder can be used as a part of other Condition +// Expressions or as an argument to the WithCondition() method for the Builder +// struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "Age" exists or not +// condition := expression.Name("Age").AttributeNotExists() +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("Age").AttributeNotExists() +// "attribute_not_exists (Age))" +func (nb NameBuilder) AttributeNotExists() ConditionBuilder { + return AttributeNotExists(nb) +} + +// AttributeType returns a ConditionBuilder representing the result of the +// attribute_type function in DynamoDB Condition Expressions. The DynamoDB types +// are represented by the type DynamoDBAttributeType. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "Age" has the DynamoDB type Number or not +// condition := expression.AttributeType(expression.Name("Age"), expression.Number) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.AttributeType(expression.Name("Age"), expression.Number) +// // Let :type be an ExpressionAttributeValue representing the value "N" +// "attribute_type (Age, :type)" +func AttributeType(nameBuilder NameBuilder, attributeType DynamoDBAttributeType) ConditionBuilder { + v := ValueBuilder{ + value: string(attributeType), + } + return ConditionBuilder{ + operandList: []OperandBuilder{nameBuilder, v}, + mode: attrTypeCond, + } +} + +// AttributeType returns a ConditionBuilder representing the result of the +// attribute_type function in DynamoDB Condition Expressions. The DynamoDB types +// are represented by the type DynamoDBAttributeType. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "Age" has the DynamoDB type Number or not +// condition := expression.Name("Age").AttributeType(expression.Number) +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("Age").AttributeType(expression.Number) +// // Let :type be an ExpressionAttributeValue representing the value "N" +// "attribute_type (Age, :type)" +func (nb NameBuilder) AttributeType(attributeType DynamoDBAttributeType) ConditionBuilder { + return AttributeType(nb, attributeType) +} + +// BeginsWith returns a ConditionBuilder representing the result of the +// begins_with function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "CodeName" starts with the substring "Ben" +// condition := expression.BeginsWith(expression.Name("CodeName"), "Ben") +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.BeginsWith(expression.Name("CodeName"), "Ben") +// // Let :ben be an ExpressionAttributeValue representing the value "Ben" +// "begins_with (CodeName, :ben)" +func BeginsWith(nameBuilder NameBuilder, prefix string) ConditionBuilder { + v := ValueBuilder{ + value: prefix, + } + return ConditionBuilder{ + operandList: []OperandBuilder{nameBuilder, v}, + mode: beginsWithCond, + } +} + +// BeginsWith returns a ConditionBuilder representing the result of the +// begins_with function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "CodeName" starts with the substring "Ben" +// condition := expression.Name("CodeName").BeginsWith("Ben") +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("CodeName").BeginsWith("Ben") +// // Let :ben be an ExpressionAttributeValue representing the value "Ben" +// "begins_with (CodeName, :ben)" +func (nb NameBuilder) BeginsWith(prefix string) ConditionBuilder { + return BeginsWith(nb, prefix) +} + +// Contains returns a ConditionBuilder representing the result of the +// contains function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "InviteList" has the value "Ben" +// condition := expression.Contains(expression.Name("InviteList"), "Ben") +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Contains(expression.Name("InviteList"), "Ben") +// // Let :ben be an ExpressionAttributeValue representing the value "Ben" +// "contains (InviteList, :ben)" +func Contains(nameBuilder NameBuilder, substr string) ConditionBuilder { + v := ValueBuilder{ + value: substr, + } + return ConditionBuilder{ + operandList: []OperandBuilder{nameBuilder, v}, + mode: containsCond, + } +} + +// Contains returns a ConditionBuilder representing the result of the +// contains function in DynamoDB Condition Expressions. The resulting +// ConditionBuilder can be used as a part of other Condition Expressions or as +// an argument to the WithCondition() method for the Builder struct. +// +// Example: +// +// // condition represents the boolean condition of whether the item +// // attribute "InviteList" has the value "Ben" +// condition := expression.Name("InviteList").Contains("Ben") +// +// // Used in another Condition Expression +// anotherCondition := expression.Not(condition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithCondition(condition) +// +// Expression Equivalent: +// +// expression.Name("InviteList").Contains("Ben") +// // Let :ben be an ExpressionAttributeValue representing the value "Ben" +// "contains (InviteList, :ben)" +func (nb NameBuilder) Contains(substr string) ConditionBuilder { + return Contains(nb, substr) +} + +// buildTree builds a tree structure of exprNodes based on the tree +// structure of the input ConditionBuilder's child ConditionBuilders and +// OperandBuilders. buildTree() satisfies the treeBuilder interface so +// ConditionBuilder can be a part of Builder and Expression struct. +func (cb ConditionBuilder) buildTree() (exprNode, error) { + childNodes, err := cb.buildChildNodes() + if err != nil { + return exprNode{}, err + } + ret := exprNode{ + children: childNodes, + } + + switch cb.mode { + case equalCond, notEqualCond, lessThanCond, lessThanEqualCond, greaterThanCond, greaterThanEqualCond: + return compareBuildCondition(cb.mode, ret) + case andCond, orCond: + return compoundBuildCondition(cb, ret) + case notCond: + return notBuildCondition(ret) + case betweenCond: + return betweenBuildCondition(ret) + case inCond: + return inBuildCondition(cb, ret) + case attrExistsCond: + return attrExistsBuildCondition(ret) + case attrNotExistsCond: + return attrNotExistsBuildCondition(ret) + case attrTypeCond: + return attrTypeBuildCondition(ret) + case beginsWithCond: + return beginsWithBuildCondition(ret) + case containsCond: + return containsBuildCondition(ret) + case unsetCond: + return exprNode{}, newUnsetParameterError("buildTree", "ConditionBuilder") + default: + return exprNode{}, fmt.Errorf("build condition error: unsupported mode: %v", cb.mode) + } +} + +// compareBuildCondition is the function to make exprNodes from Compare +// ConditionBuilders. compareBuildCondition is only called by the +// buildTree method. This function assumes that the argument ConditionBuilder +// has the right format. +func compareBuildCondition(conditionMode conditionMode, node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + switch conditionMode { + case equalCond: + node.fmtExpr = "$c = $c" + case notEqualCond: + node.fmtExpr = "$c <> $c" + case lessThanCond: + node.fmtExpr = "$c < $c" + case lessThanEqualCond: + node.fmtExpr = "$c <= $c" + case greaterThanCond: + node.fmtExpr = "$c > $c" + case greaterThanEqualCond: + node.fmtExpr = "$c >= $c" + default: + return exprNode{}, fmt.Errorf("build compare condition error: unsupported mode: %v", conditionMode) + } + + return node, nil +} + +// compoundBuildCondition is the function to make exprNodes from And/Or +// ConditionBuilders. compoundBuildCondition is only called by the +// buildTree method. This function assumes that the argument ConditionBuilder +// has the right format. +func compoundBuildCondition(conditionBuilder ConditionBuilder, node exprNode) (exprNode, error) { + // create a string with escaped characters to substitute them with proper + // aliases during runtime + var mode string + switch conditionBuilder.mode { + case andCond: + mode = " AND " + case orCond: + mode = " OR " + default: + return exprNode{}, fmt.Errorf("build compound condition error: unsupported mode: %v", conditionBuilder.mode) + } + node.fmtExpr = "($c)" + strings.Repeat(mode+"($c)", len(conditionBuilder.conditionList)-1) + + return node, nil +} + +// notBuildCondition is the function to make exprNodes from Not +// ConditionBuilders. notBuildCondition is only called by the +// buildTree method. This function assumes that the argument ConditionBuilder +// has the right format. +func notBuildCondition(node exprNode) (exprNode, error) { + // create a string with escaped characters to substitute them with proper + // aliases during runtime + node.fmtExpr = "NOT ($c)" + + return node, nil +} + +// betweenBuildCondition is the function to make exprNodes from Between +// ConditionBuilders. BuildCondition is only called by the +// buildTree method. This function assumes that the argument ConditionBuilder +// has the right format. +func betweenBuildCondition(node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "$c BETWEEN $c AND $c" + + return node, nil +} + +// inBuildCondition is the function to make exprNodes from In +// ConditionBuilders. inBuildCondition is only called by the +// buildTree method. This function assumes that the argument ConditionBuilder +// has the right format. +func inBuildCondition(conditionBuilder ConditionBuilder, node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "$c IN ($c" + strings.Repeat(", $c", len(conditionBuilder.operandList)-2) + ")" + + return node, nil +} + +// attrExistsBuildCondition is the function to make exprNodes from +// AttrExistsCond ConditionBuilders. attrExistsBuildCondition is only +// called by the buildTree method. This function assumes that the argument +// ConditionBuilder has the right format. +func attrExistsBuildCondition(node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "attribute_exists ($c)" + + return node, nil +} + +// attrNotExistsBuildCondition is the function to make exprNodes from +// AttrNotExistsCond ConditionBuilders. attrNotExistsBuildCondition is only +// called by the buildTree method. This function assumes that the argument +// ConditionBuilder has the right format. +func attrNotExistsBuildCondition(node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "attribute_not_exists ($c)" + + return node, nil +} + +// attrTypeBuildCondition is the function to make exprNodes from AttrTypeCond +// ConditionBuilders. attrTypeBuildCondition is only called by the +// buildTree method. This function assumes that the argument +// ConditionBuilder has the right format. +func attrTypeBuildCondition(node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "attribute_type ($c, $c)" + + return node, nil +} + +// beginsWithBuildCondition is the function to make exprNodes from +// BeginsWithCond ConditionBuilders. beginsWithBuildCondition is only +// called by the buildTree method. This function assumes that the argument +// ConditionBuilder has the right format. +func beginsWithBuildCondition(node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "begins_with ($c, $c)" + + return node, nil +} + +// containsBuildCondition is the function to make exprNodes from +// ContainsCond ConditionBuilders. containsBuildCondition is only +// called by the buildTree method. This function assumes that the argument +// ConditionBuilder has the right format. +func containsBuildCondition(node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "contains ($c, $c)" + + return node, nil +} + +// buildChildNodes creates the list of the child exprNodes. This avoids +// duplication of code amongst the various buildTree functions. +func (cb ConditionBuilder) buildChildNodes() ([]exprNode, error) { + childNodes := make([]exprNode, 0, len(cb.conditionList)+len(cb.operandList)) + for _, condition := range cb.conditionList { + node, err := condition.buildTree() + if err != nil { + return []exprNode{}, err + } + childNodes = append(childNodes, node) + } + for _, ope := range cb.operandList { + operand, err := ope.BuildOperand() + if err != nil { + return []exprNode{}, err + } + childNodes = append(childNodes, operand.exprNode) + } + + return childNodes, nil +} diff --git a/feature/dynamodb/expression/condition_test.go b/feature/dynamodb/expression/condition_test.go new file mode 100644 index 00000000000..4257cf95217 --- /dev/null +++ b/feature/dynamodb/expression/condition_test.go @@ -0,0 +1,1510 @@ +package expression + +import ( + "reflect" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +// condErrorMode will help with error cases and checking error types +type condErrorMode string + +const ( + noConditionError condErrorMode = "" + // unsetCondition error will occur when BuildExpression is called on an empty + // ConditionBuilder + unsetCondition = "unset parameter: ConditionBuilder" + // invalidOperand error will occur when an invalid OperandBuilder is used as + // an argument + invalidConditionOperand = "BuildOperand error" +) + +//Compare +func TestCompare(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "name equal name", + input: Name("foo").Equal(Name("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c = $c", + }, + }, + { + name: "value equal value", + input: Value(5).Equal(Value("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + { + name: "name size equal name size", + input: Name("foo[1]").Size().Equal(Name("bar").Size()), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "size ($n[1])", + }, + { + names: []string{"bar"}, + fmtExpr: "size ($n)", + }, + }, + fmtExpr: "$c = $c", + }, + }, + { + name: "name not equal name", + input: Name("foo").NotEqual(Name("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c <> $c", + }, + }, + { + name: "value not equal value", + input: Value(5).NotEqual(Value("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c <> $c", + }, + }, + { + name: "name size not equal name size", + input: Name("foo[1]").Size().NotEqual(Name("bar").Size()), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "size ($n[1])", + }, + { + names: []string{"bar"}, + fmtExpr: "size ($n)", + }, + }, + fmtExpr: "$c <> $c", + }, + }, + { + name: "name less than name", + input: Name("foo").LessThan(Name("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c < $c", + }, + }, + { + name: "value less than value", + input: Value(5).LessThan(Value("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c < $c", + }, + }, + { + name: "name size less than name size", + input: Name("foo[1]").Size().LessThan(Name("bar").Size()), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "size ($n[1])", + }, + { + names: []string{"bar"}, + fmtExpr: "size ($n)", + }, + }, + fmtExpr: "$c < $c", + }, + }, + { + name: "name less than equal name", + input: Name("foo").LessThanEqual(Name("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c <= $c", + }, + }, + { + name: "value less than equal value", + input: Value(5).LessThanEqual(Value("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c <= $c", + }, + }, + { + name: "name size less than equal name size", + input: Name("foo[1]").Size().LessThanEqual(Name("bar").Size()), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "size ($n[1])", + }, + { + names: []string{"bar"}, + fmtExpr: "size ($n)", + }, + }, + fmtExpr: "$c <= $c", + }, + }, + { + name: "name greater than name", + input: Name("foo").GreaterThan(Name("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c > $c", + }, + }, + { + name: "value greater than value", + input: Value(5).GreaterThan(Value("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c > $c", + }, + }, + { + name: "name size greater than name size", + input: Name("foo[1]").Size().GreaterThan(Name("bar").Size()), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "size ($n[1])", + }, + { + names: []string{"bar"}, + fmtExpr: "size ($n)", + }, + }, + fmtExpr: "$c > $c", + }, + }, + { + name: "name greater than equal name", + input: Name("foo").GreaterThanEqual(Name("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c >= $c", + }, + }, + { + name: "value greater than equal value", + input: Value(5).GreaterThanEqual(Value("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c >= $c", + }, + }, + { + name: "name size greater than equal name size", + input: Name("foo[1]").Size().GreaterThanEqual(Name("bar").Size()), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "size ($n[1])", + }, + { + names: []string{"bar"}, + fmtExpr: "size ($n)", + }, + }, + fmtExpr: "$c >= $c", + }, + }, + { + name: "invalid operand error Equal", + input: Name("").Size().Equal(Value(5)), + err: invalidConditionOperand, + }, + { + name: "invalid operand error NotEqual", + input: Name("").Size().NotEqual(Value(5)), + err: invalidConditionOperand, + }, + { + name: "invalid operand error LessThan", + input: Name("").Size().LessThan(Value(5)), + err: invalidConditionOperand, + }, + { + name: "invalid operand error LessThanEqual", + input: Name("").Size().LessThanEqual(Value(5)), + err: invalidConditionOperand, + }, + { + name: "invalid operand error GreaterThan", + input: Name("").Size().GreaterThan(Value(5)), + err: invalidConditionOperand, + }, + { + name: "invalid operand error GreaterThanEqual", + input: Name("").Size().GreaterThanEqual(Value(5)), + err: invalidConditionOperand, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestBuildCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expected exprNode + err condErrorMode + }{ + { + name: "no match error", + input: ConditionBuilder{}, + err: unsetCondition, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestBoolCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "basic method and", + input: Name("foo").Equal(Value(5)).And(Name("bar").Equal(Value("baz"))), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + { + children: []exprNode{ + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "baz"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + fmtExpr: "($c) AND ($c)", + }, + }, + { + name: "basic method or", + input: Name("foo").Equal(Value(5)).Or(Name("bar").Equal(Value("baz"))), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + { + children: []exprNode{ + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "baz"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + fmtExpr: "($c) OR ($c)", + }, + }, + { + name: "variadic function and", + input: And(Name("foo").Equal(Value(5)), Name("bar").Equal(Value("baz")), Name("qux").Equal(Value(true))), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + { + children: []exprNode{ + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "baz"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + { + children: []exprNode{ + { + names: []string{"qux"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberBOOL{Value: true}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + fmtExpr: "($c) AND ($c) AND ($c)", + }, + }, + { + name: "variadic function or", + input: Or(Name("foo").Equal(Value(5)), Name("bar").Equal(Value("baz")), Name("qux").Equal(Value(true))), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + { + children: []exprNode{ + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "baz"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + { + children: []exprNode{ + { + names: []string{"qux"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberBOOL{Value: true}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + fmtExpr: "($c) OR ($c) OR ($c)", + }, + }, + { + name: "invalid operand error And", + input: Name("").Size().GreaterThanEqual(Value(5)).And(Name("[5]").Between(Value(3), Value(9))), + err: invalidConditionOperand, + }, + { + name: "invalid operand error Or", + input: Name("").Size().GreaterThanEqual(Value(5)).Or(Name("[5]").Between(Value(3), Value(9))), + err: invalidConditionOperand, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestNotCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "basic method not", + input: Name("foo").Equal(Value(5)).Not(), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + fmtExpr: "NOT ($c)", + }, + }, + { + name: "basic function not", + input: Not(Name("foo").Equal(Value(5))), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + fmtExpr: "NOT ($c)", + }, + }, + { + name: "invalid operand error not", + input: Name("").Size().GreaterThanEqual(Value(5)).Or(Name("[5]").Between(Value(3), Value(9))).Not(), + err: invalidConditionOperand, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestBetweenCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "basic method between for name", + input: Name("foo").Between(Value(5), Value(7)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "7"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c BETWEEN $c AND $c", + }, + }, + { + name: "basic method between for value", + input: Value(6).Between(Value(5), Value(7)), + expectedNode: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "6"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "7"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c BETWEEN $c AND $c", + }, + }, + { + name: "basic method between for size", + input: Name("foo").Size().Between(Value(5), Value(7)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "size ($n)", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "7"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c BETWEEN $c AND $c", + }, + }, + { + name: "invalid operand error between", + input: Name("[5]").Between(Value(3), Name("foo..bar")), + err: invalidConditionOperand, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestInCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "basic method in for name", + input: Name("foo").In(Value(5), Value(7)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "7"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c IN ($c, $c)", + }, + }, + { + name: "basic method in for value", + input: Value(6).In(Value(5), Value(7)), + expectedNode: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "6"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "7"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c IN ($c, $c)", + }, + }, + { + name: "basic method in for size", + input: Name("foo").Size().In(Value(5), Value(7)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "size ($n)", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "7"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c IN ($c, $c)", + }, + }, + { + name: "invalid operand error in", + input: Name("[5]").In(Value(3), Name("foo..bar")), + err: invalidConditionOperand, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestAttrExistsCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "basic attr exists", + input: Name("foo").AttributeExists(), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "attribute_exists ($c)", + }, + }, + { + name: "basic attr not exists", + input: Name("foo").AttributeNotExists(), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "attribute_not_exists ($c)", + }, + }, + { + name: "invalid operand error attr exists", + input: AttributeExists(Name("")), + err: invalidConditionOperand, + }, + { + name: "invalid operand error attr not exists", + input: AttributeNotExists(Name("foo..bar")), + err: invalidConditionOperand, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestAttrTypeCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "attr type String", + input: Name("foo").AttributeType(String), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "S"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type String", + input: Name("foo").AttributeType(String), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "S"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type StringSet", + input: Name("foo").AttributeType(StringSet), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "SS"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type Number", + input: Name("foo").AttributeType(Number), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "N"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type NumberSet", + input: Name("foo").AttributeType(NumberSet), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "NS"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type Binary", + input: Name("foo").AttributeType(Binary), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "B"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type BinarySet", + input: Name("foo").AttributeType(BinarySet), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "BS"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type Boolean", + input: Name("foo").AttributeType(Boolean), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "BOOL"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type Null", + input: Name("foo").AttributeType(Null), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "NULL"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type List", + input: Name("foo").AttributeType(List), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "L"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type Map", + input: Name("foo").AttributeType(Map), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "M"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "attribute_type ($c, $c)", + }, + }, + { + name: "attr type invalid operand", + input: Name("").AttributeType(Map), + err: invalidConditionOperand, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestBeginsWithCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "basic begins with", + input: Name("foo").BeginsWith("bar"), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "begins_with ($c, $c)", + }, + }, + { + name: "begins with invalid operand", + input: Name("").BeginsWith("bar"), + err: invalidConditionOperand, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestContainsCondition(t *testing.T) { + cases := []struct { + name string + input ConditionBuilder + expectedNode exprNode + err condErrorMode + }{ + { + name: "basic contains", + input: Name("foo").Contains("bar"), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "contains ($c, $c)", + }, + }, + { + name: "contains invalid operand", + input: Name("").Contains("bar"), + err: invalidConditionOperand, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestCompoundBuildCondition(t *testing.T) { + cases := []struct { + name string + inputCond ConditionBuilder + expected string + }{ + { + name: "and", + inputCond: ConditionBuilder{ + conditionList: []ConditionBuilder{ + {}, + {}, + {}, + {}, + }, + mode: andCond, + }, + expected: "($c) AND ($c) AND ($c) AND ($c)", + }, + { + name: "or", + inputCond: ConditionBuilder{ + conditionList: []ConditionBuilder{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + }, + mode: orCond, + }, + expected: "($c) OR ($c) OR ($c) OR ($c) OR ($c) OR ($c) OR ($c)", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + en, err := compoundBuildCondition(c.inputCond, exprNode{}) + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, en.fmtExpr; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestInBuildCondition(t *testing.T) { + cases := []struct { + name string + inputCond ConditionBuilder + expected string + }{ + { + name: "in", + inputCond: ConditionBuilder{ + operandList: []OperandBuilder{ + NameBuilder{}, + NameBuilder{}, + NameBuilder{}, + NameBuilder{}, + NameBuilder{}, + NameBuilder{}, + NameBuilder{}, + }, + mode: andCond, + }, + expected: "$c IN ($c, $c, $c, $c, $c, $c)", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + en, err := inBuildCondition(c.inputCond, exprNode{}) + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, en.fmtExpr; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +// If there is time implement mapEquals diff --git a/feature/dynamodb/expression/doc.go b/feature/dynamodb/expression/doc.go new file mode 100644 index 00000000000..7c1dc413d4c --- /dev/null +++ b/feature/dynamodb/expression/doc.go @@ -0,0 +1,47 @@ +/* +Package expression provides types and functions to create Amazon DynamoDB +Expression strings, ExpressionAttributeNames maps, and ExpressionAttributeValues +maps. + +Using the Package + +The package represents the various DynamoDB Expressions as structs named +accordingly. For example, ConditionBuilder represents a DynamoDB Condition +Expression, an UpdateBuilder represents a DynamoDB Update Expression, and so on. +The following example shows a sample ConditionExpression and how to build an +equivalent ConditionBuilder + + // Let :a be an ExpressionAttributeValue representing the string "No One You Know" + condExpr := "Artist = :a" + condBuilder := expression.Name("Artist").Equal(expression.Value("No One You Know")) + +In order to retrieve the formatted DynamoDB Expression strings, call the getter +methods on the Expression struct. To create the Expression struct, call the +Build() method on the Builder struct. Because some input structs, such as +QueryInput, can have multiple DynamoDB Expressions, multiple structs +representing various DynamoDB Expressions can be added to the Builder struct. +The following example shows a generic usage of the whole package. + + filt := expression.Name("Artist").Equal(expression.Value("No One You Know")) + proj := expression.NamesList(expression.Name("SongTitle"), expression.Name("AlbumTitle")) + expr, err := expression.NewBuilder().WithFilter(filt).WithProjection(proj).Build() + if err != nil { + fmt.Println(err) + } + + input := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String("Music"), + } + +The ExpressionAttributeNames and ExpressionAttributeValues member of the input +struct must always be assigned when using the Expression struct because all item +attribute names and values are aliased. That means that if the +ExpressionAttributeNames and ExpressionAttributeValues member is not assigned +with the corresponding Names() and Values() methods, the DynamoDB operation will +run into a logic error. +*/ +package expression diff --git a/feature/dynamodb/expression/error.go b/feature/dynamodb/expression/error.go new file mode 100644 index 00000000000..7378d7e219a --- /dev/null +++ b/feature/dynamodb/expression/error.go @@ -0,0 +1,59 @@ +package expression + +import ( + "fmt" +) + +// InvalidParameterError is returned if invalid parameters are encountered. This +// error specifically refers to situations where parameters are non-empty but +// have an invalid syntax/format. The error message includes the function +// that returned the error originally and the parameter type that was deemed +// invalid. +// +// Example: +// +// // err is of type InvalidParameterError +// _, err := expression.Name("foo..bar").BuildOperand() +type InvalidParameterError struct { + parameterType string + functionName string +} + +func (ipe InvalidParameterError) Error() string { + return fmt.Sprintf("%s error: invalid parameter: %s", ipe.functionName, ipe.parameterType) +} + +func newInvalidParameterError(funcName, paramType string) InvalidParameterError { + return InvalidParameterError{ + parameterType: paramType, + functionName: funcName, + } +} + +// UnsetParameterError is returned if parameters are empty and uninitialized. +// This error is returned if opaque structs (ConditionBuilder, NameBuilder, +// Builder, etc) are initialized outside of functions in the package, since all +// structs in the package are designed to be initialized with functions. +// +// Example: +// +// // err is of type UnsetParameterError +// _, err := expression.Builder{}.Build() +// _, err := expression.NewBuilder(). +// WithCondition(expression.ConditionBuilder{}). +// Build() +type UnsetParameterError struct { + parameterType string + functionName string +} + +func (upe UnsetParameterError) Error() string { + return fmt.Sprintf("%s error: unset parameter: %s", upe.functionName, upe.parameterType) +} + +func newUnsetParameterError(funcName, paramType string) UnsetParameterError { + return UnsetParameterError{ + parameterType: paramType, + functionName: funcName, + } +} diff --git a/feature/dynamodb/expression/error_test.go b/feature/dynamodb/expression/error_test.go new file mode 100644 index 00000000000..7b4b2217700 --- /dev/null +++ b/feature/dynamodb/expression/error_test.go @@ -0,0 +1,51 @@ +// +build go1.7 + +package expression + +import ( + "testing" +) + +func TestInvalidParameterError(t *testing.T) { + cases := []struct { + name string + input InvalidParameterError + expected string + }{ + { + name: "invalid error", + input: newInvalidParameterError("func", "param"), + expected: "func error: invalid parameter: param", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := c.input.Error() + if e, a := c.expected, actual; e != a { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestUnsetParameterError(t *testing.T) { + cases := []struct { + name string + input UnsetParameterError + expected string + }{ + { + name: "unset error", + input: newUnsetParameterError("func", "param"), + expected: "func error: unset parameter: param", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := c.input.Error() + if e, a := c.expected, actual; e != a { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} diff --git a/feature/dynamodb/expression/examples_test.go b/feature/dynamodb/expression/examples_test.go new file mode 100644 index 00000000000..230cb9a3d0b --- /dev/null +++ b/feature/dynamodb/expression/examples_test.go @@ -0,0 +1,309 @@ +package expression_test + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +var config = struct { + LoadDefaultConfig func(ctx context.Context) (aws.Config, error) +}{ + LoadDefaultConfig: func(ctx context.Context) (aws.Config, error) { + return aws.Config{}, nil + }, +} + +// Using Projection Expression +// +// This example queries items in the Music table. The table has a partition key and +// sort key (Artist and SongTitle), but this query only specifies the partition key +// value. It returns song titles by the artist named "No One You Know". +func ExampleBuilder_WithProjection() { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + fmt.Println(err.Error()) + return + } + + client := dynamodb.NewFromConfig(cfg) + + // Construct the Key condition builder + keyCond := expression.Key("Artist").Equal(expression.Value("No One You Know")) + + // Create the project expression builder with a names list. + proj := expression.NamesList(expression.Name("SongTitle")) + + // Combine the key condition, and projection together as a DynamoDB expression + // builder. + expr, err := expression.NewBuilder(). + WithKeyCondition(keyCond). + WithProjection(proj). + Build() + if err != nil { + fmt.Println(err) + return + } + + // Use the built expression to populate the DynamoDB Query's API input + // parameters. + input := &dynamodb.QueryInput{ + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String("Music"), + } + + result, err := client.Query(context.TODO(), input) + if err != nil { + if apiErr := new(types.ProvisionedThroughputExceededException); errors.As(err, &apiErr) { + fmt.Println("throughput exceeded") + } else if apiErr := new(types.ResourceNotFoundException); errors.As(err, &apiErr) { + fmt.Println("resource not found") + } else if apiErr := new(types.InternalServerError); errors.As(err, &apiErr) { + fmt.Println("internal server error") + } else { + fmt.Println(err) + } + return + } + + fmt.Println(result) +} + +// Using Key Condition Expression +// +// This example queries items in the Music table. The table has a partition key and +// sort key (Artist and SongTitle), but this query only specifies the partition key +// value. It returns song titles by the artist named "No One You Know". +func ExampleBuilder_WithKeyCondition() { + config, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + fmt.Println(err.Error()) + return + } + client := dynamodb.NewFromConfig(config) + + // Construct the Key condition builder + keyCond := expression.Key("Artist").Equal(expression.Value("No One You Know")) + + // Create the project expression builder with a names list. + proj := expression.NamesList(expression.Name("SongTitle")) + + // Combine the key condition, and projection together as a DynamoDB expression + // builder. + expr, err := expression.NewBuilder(). + WithKeyCondition(keyCond). + WithProjection(proj). + Build() + if err != nil { + fmt.Println(err) + return + } + + // Use the built expression to populate the DynamoDB Query's API input + // parameters. + input := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String("Music"), + } + + result, err := client.Query(context.TODO(), input) + if err != nil { + if apiErr := new(types.ProvisionedThroughputExceededException); errors.As(err, &apiErr) { + fmt.Println("throughput exceeded") + } else if apiErr := new(types.ResourceNotFoundException); errors.As(err, &apiErr) { + fmt.Println("resource not found") + } else if apiErr := new(types.InternalServerError); errors.As(err, &apiErr) { + fmt.Println("internal server error") + } else { + fmt.Println(err) + } + return + } + + fmt.Println(result) +} + +// Using Filter Expression +// +// This example scans the entire Music table, and then narrows the results to songs +// by the artist "No One You Know". For each item, only the album title and song title +// are returned. +func ExampleBuilder_WithFilter() { + config, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + fmt.Println(err.Error()) + return + } + client := dynamodb.NewFromConfig(config) + + // Construct the filter builder with a name and value. + filt := expression.Name("Artist").Equal(expression.Value("No One You Know")) + + // Create the names list projection of names to project. + proj := expression.NamesList( + expression.Name("AlbumTitle"), + expression.Name("SongTitle"), + ) + + // Using the filter and projections create a DynamoDB expression from the two. + expr, err := expression.NewBuilder(). + WithFilter(filt). + WithProjection(proj). + Build() + if err != nil { + fmt.Println(err) + return + } + + // Use the built expression to populate the DynamoDB Scan API input parameters. + input := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String("Music"), + } + + result, err := client.Scan(context.TODO(), input) + if err != nil { + if apiErr := new(types.ProvisionedThroughputExceededException); errors.As(err, &apiErr) { + fmt.Println("throughput exceeded") + } else if apiErr := new(types.ResourceNotFoundException); errors.As(err, &apiErr) { + fmt.Println("resource not found") + } else if apiErr := new(types.InternalServerError); errors.As(err, &apiErr) { + fmt.Println("internal server error") + } else { + fmt.Println(err) + } + return + } + + fmt.Println(result) +} + +// Using Update Expression +// +// This example updates an item in the Music table. It adds a new attribute (Year) and +// modifies the AlbumTitle attribute. All of the attributes in the item, as they appear +// after the update, are returned in the response. +func ExampleBuilder_WithUpdate() { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + fmt.Println(err.Error()) + return + } + + client := dynamodb.NewFromConfig(cfg) + + // Create an update to set two fields in the table. + update := expression.Set( + expression.Name("Year"), + expression.Value(2015), + ).Set( + expression.Name("AlbumTitle"), + expression.Value("Louder Than Ever"), + ) + + // Create the DynamoDB expression from the Update. + expr, err := expression.NewBuilder(). + WithUpdate(update). + Build() + if err != nil { + fmt.Println(err) + return + } + + // Use the built expression to populate the DynamoDB UpdateItem API + // input parameters. + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + Key: map[string]types.AttributeValue{ + "Artist": &types.AttributeValueMemberS{Value: "Acme Band"}, + "SongTitle": &types.AttributeValueMemberS{Value: "Happy Day"}, + }, + ReturnValues: types.ReturnValueAllNew, + TableName: aws.String("Music"), + UpdateExpression: expr.Update(), + } + + result, err := client.UpdateItem(context.TODO(), input) + if err != nil { + if apiErr := new(types.ProvisionedThroughputExceededException); errors.As(err, &apiErr) { + fmt.Println("throughput exceeded") + } else if apiErr := new(types.ResourceNotFoundException); errors.As(err, &apiErr) { + fmt.Println("resource not found") + } else if apiErr := new(types.InternalServerError); errors.As(err, &apiErr) { + fmt.Println("internal server error") + } else { + fmt.Println(err) + } + return + } + + fmt.Println(result) +} + +// Using Condition Expression +// +// This example deletes an item from the Music table if the rating is lower than +// 7. +func ExampleBuilder_WithCondition() { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + fmt.Println(err) + return + } + svc := dynamodb.NewFromConfig(cfg) + + // Create a condition where the Rating field must be less than 7. + cond := expression.Name("Rating").LessThan(expression.Value(7)) + + // Create a DynamoDB expression from the condition. + expr, err := expression.NewBuilder(). + WithCondition(cond). + Build() + if err != nil { + fmt.Println(err) + return + } + + // Use the built expression to populate the DeleteItem API operation with the + // condition expression. + input := &dynamodb.DeleteItemInput{ + Key: map[string]types.AttributeValue{ + "Artist": &types.AttributeValueMemberS{Value: "No One You Know"}, + "SongTitle": &types.AttributeValueMemberS{Value: "Scared of My Shadow"}, + }, + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + ConditionExpression: expr.Condition(), + TableName: aws.String("Music"), + } + + result, err := svc.DeleteItem(context.TODO(), input) + if err != nil { + if apiErr := new(types.ProvisionedThroughputExceededException); errors.As(err, &apiErr) { + fmt.Println("throughput exceeded") + } else if apiErr := new(types.ResourceNotFoundException); errors.As(err, &apiErr) { + fmt.Println("resource not found") + } else if apiErr := new(types.InternalServerError); errors.As(err, &apiErr) { + fmt.Println("internal server error") + } else { + fmt.Println(err) + } + return + } + + fmt.Println(result) +} diff --git a/feature/dynamodb/expression/expression.go b/feature/dynamodb/expression/expression.go new file mode 100644 index 00000000000..f6fd3201f77 --- /dev/null +++ b/feature/dynamodb/expression/expression.go @@ -0,0 +1,633 @@ +package expression + +import ( + "fmt" + "sort" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +// expressionType specifies the type of Expression. Declaring this type is used +// to eliminate magic strings +type expressionType string + +const ( + projection expressionType = "projection" + keyCondition = "keyCondition" + condition = "condition" + filter = "filter" + update = "update" +) + +// Implement the Sort interface +type typeList []expressionType + +func (l typeList) Len() int { + return len(l) +} + +func (l typeList) Less(i, j int) bool { + return string(l[i]) < string(l[j]) +} + +func (l typeList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +// Builder represents the struct that builds the Expression struct. Methods such +// as WithProjection() and WithCondition() can add different kinds of DynamoDB +// Expressions to the Builder. The method Build() creates an Expression struct +// with the specified types of DynamoDB Expressions. +// +// Example: +// +// keyCond := expression.Key("someKey").Equal(expression.Value("someValue")) +// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName")) +// +// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj) +// expr := builder.Build() +// +// queryInput := dynamodb.QueryInput{ +// KeyConditionExpression: expr.KeyCondition(), +// ProjectionExpression: expr.Projection(), +// ExpressionAttributeNames: expr.Names(), +// ExpressionAttributeValues: expr.Values(), +// TableName: aws.String("SomeTable"), +// } +type Builder struct { + expressionMap map[expressionType]treeBuilder +} + +// NewBuilder returns an empty Builder struct. Methods such as WithProjection() +// and WithCondition() can add different kinds of DynamoDB Expressions to the +// Builder. The method Build() creates an Expression struct with the specified +// types of DynamoDB Expressions. +// +// Example: +// +// keyCond := expression.Key("someKey").Equal(expression.Value("someValue")) +// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName")) +// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj) +func NewBuilder() Builder { + return Builder{} +} + +// Build builds an Expression struct representing multiple types of DynamoDB +// Expressions. Getter methods on the resulting Expression struct returns the +// DynamoDB Expression strings as well as the maps that correspond to +// ExpressionAttributeNames and ExpressionAttributeValues. Calling Build() on an +// empty Builder returns the typed error EmptyParameterError. +// +// Example: +// +// // keyCond represents the Key Condition Expression +// keyCond := expression.Key("someKey").Equal(expression.Value("someValue")) +// // proj represents the Projection Expression +// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName")) +// +// // Add keyCond and proj to builder as a Key Condition and Projection +// // respectively +// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj) +// expr := builder.Build() +// +// queryInput := dynamodb.QueryInput{ +// KeyConditionExpression: expr.KeyCondition(), +// ProjectionExpression: expr.Projection(), +// ExpressionAttributeNames: expr.Names(), +// ExpressionAttributeValues: expr.Values(), +// TableName: aws.String("SomeTable"), +// } +func (b Builder) Build() (Expression, error) { + if b.expressionMap == nil { + return Expression{}, newUnsetParameterError("Build", "Builder") + } + + aliasList, expressionMap, err := b.buildChildTrees() + if err != nil { + return Expression{}, err + } + + expression := Expression{ + expressionMap: expressionMap, + } + + if len(aliasList.namesList) != 0 { + namesMap := map[string]string{} + for ind, val := range aliasList.namesList { + namesMap[fmt.Sprintf("#%v", ind)] = val + } + expression.namesMap = namesMap + } + + if len(aliasList.valuesList) != 0 { + valuesMap := map[string]types.AttributeValue{} + for i := 0; i < len(aliasList.valuesList); i++ { + valuesMap[fmt.Sprintf(":%v", i)] = aliasList.valuesList[i] + } + expression.valuesMap = valuesMap + } + + return expression, nil +} + +// buildChildTrees compiles the list of treeBuilders that are the children of +// the argument Builder. The returned aliasList represents all the alias tokens +// used in the expression strings. The returned map[string]string maps the type +// of expression (i.e. "condition", "update") to the appropriate expression +// string. +func (b Builder) buildChildTrees() (aliasList, map[expressionType]string, error) { + aList := aliasList{} + formattedExpressions := map[expressionType]string{} + keys := typeList{} + + for expressionType := range b.expressionMap { + keys = append(keys, expressionType) + } + + sort.Sort(keys) + + for _, key := range keys { + node, err := b.expressionMap[key].buildTree() + if err != nil { + return aliasList{}, nil, err + } + formattedExpression, err := node.buildExpressionString(&aList) + if err != nil { + return aliasList{}, nil, err + } + formattedExpressions[key] = formattedExpression + } + + return aList, formattedExpressions, nil +} + +// WithCondition method adds the argument ConditionBuilder as a Condition +// Expression to the argument Builder. If the argument Builder already has a +// ConditionBuilder representing a Condition Expression, WithCondition() +// overwrites the existing ConditionBuilder. +// +// Example: +// +// // let builder be an existing Builder{} and cond be an existing +// // ConditionBuilder{} +// builder = builder.WithCondition(cond) +// +// // add other DynamoDB Expressions to the builder. let proj be an already +// // existing ProjectionBuilder +// builder = builder.WithProjection(proj) +// // create an Expression struct +// expr := builder.Build() +func (b Builder) WithCondition(conditionBuilder ConditionBuilder) Builder { + if b.expressionMap == nil { + b.expressionMap = map[expressionType]treeBuilder{} + } + b.expressionMap[condition] = conditionBuilder + return b +} + +// WithProjection method adds the argument ProjectionBuilder as a Projection +// Expression to the argument Builder. If the argument Builder already has a +// ProjectionBuilder representing a Projection Expression, WithProjection() +// overwrites the existing ProjectionBuilder. +// +// Example: +// +// // let builder be an existing Builder{} and proj be an existing +// // ProjectionBuilder{} +// builder = builder.WithProjection(proj) +// +// // add other DynamoDB Expressions to the builder. let cond be an already +// // existing ConditionBuilder +// builder = builder.WithCondition(cond) +// // create an Expression struct +// expr := builder.Build() +func (b Builder) WithProjection(projectionBuilder ProjectionBuilder) Builder { + if b.expressionMap == nil { + b.expressionMap = map[expressionType]treeBuilder{} + } + b.expressionMap[projection] = projectionBuilder + return b +} + +// WithKeyCondition method adds the argument KeyConditionBuilder as a Key +// Condition Expression to the argument Builder. If the argument Builder already +// has a KeyConditionBuilder representing a Key Condition Expression, +// WithKeyCondition() overwrites the existing KeyConditionBuilder. +// +// Example: +// +// // let builder be an existing Builder{} and keyCond be an existing +// // KeyConditionBuilder{} +// builder = builder.WithKeyCondition(keyCond) +// +// // add other DynamoDB Expressions to the builder. let cond be an already +// // existing ConditionBuilder +// builder = builder.WithCondition(cond) +// // create an Expression struct +// expr := builder.Build() +func (b Builder) WithKeyCondition(keyConditionBuilder KeyConditionBuilder) Builder { + if b.expressionMap == nil { + b.expressionMap = map[expressionType]treeBuilder{} + } + b.expressionMap[keyCondition] = keyConditionBuilder + return b +} + +// WithFilter method adds the argument ConditionBuilder as a Filter Expression +// to the argument Builder. If the argument Builder already has a +// ConditionBuilder representing a Filter Expression, WithFilter() +// overwrites the existing ConditionBuilder. +// +// Example: +// +// // let builder be an existing Builder{} and filt be an existing +// // ConditionBuilder{} +// builder = builder.WithFilter(filt) +// +// // add other DynamoDB Expressions to the builder. let cond be an already +// // existing ConditionBuilder +// builder = builder.WithCondition(cond) +// // create an Expression struct +// expr := builder.Build() +func (b Builder) WithFilter(filterBuilder ConditionBuilder) Builder { + if b.expressionMap == nil { + b.expressionMap = map[expressionType]treeBuilder{} + } + b.expressionMap[filter] = filterBuilder + return b +} + +// WithUpdate method adds the argument UpdateBuilder as an Update Expression +// to the argument Builder. If the argument Builder already has a UpdateBuilder +// representing a Update Expression, WithUpdate() overwrites the existing +// UpdateBuilder. +// +// Example: +// +// // let builder be an existing Builder{} and update be an existing +// // UpdateBuilder{} +// builder = builder.WithUpdate(update) +// +// // add other DynamoDB Expressions to the builder. let cond be an already +// // existing ConditionBuilder +// builder = builder.WithCondition(cond) +// // create an Expression struct +// expr := builder.Build() +func (b Builder) WithUpdate(updateBuilder UpdateBuilder) Builder { + if b.expressionMap == nil { + b.expressionMap = map[expressionType]treeBuilder{} + } + b.expressionMap[update] = updateBuilder + return b +} + +// Expression represents a collection of DynamoDB Expressions. The getter +// methods of the Expression struct retrieves the formatted DynamoDB +// Expressions, ExpressionAttributeNames, and ExpressionAttributeValues. +// +// Example: +// +// // keyCond represents the Key Condition Expression +// keyCond := expression.Key("someKey").Equal(expression.Value("someValue")) +// // proj represents the Projection Expression +// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName")) +// +// // Add keyCond and proj to builder as a Key Condition and Projection +// // respectively +// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj) +// expr := builder.Build() +// +// queryInput := dynamodb.QueryInput{ +// KeyConditionExpression: expr.KeyCondition(), +// ProjectionExpression: expr.Projection(), +// ExpressionAttributeNames: expr.Names(), +// ExpressionAttributeValues: expr.Values(), +// TableName: aws.String("SomeTable"), +// } +type Expression struct { + expressionMap map[expressionType]string + namesMap map[string]string + valuesMap map[string]types.AttributeValue +} + +// treeBuilder interface is fulfilled by builder structs that represent +// different types of Expressions. +type treeBuilder interface { + // buildTree creates the tree structure of exprNodes. The tree structure + // of exprNodes are traversed in order to build the string representing + // different types of Expressions as well as the maps that represent + // ExpressionAttributeNames and ExpressionAttributeValues. + buildTree() (exprNode, error) +} + +// Condition returns the *string corresponding to the Condition Expression +// of the argument Expression. This method is used to satisfy the members of +// DynamoDB input structs. If the Expression does not have a condition +// expression this method returns nil. +// +// Example: +// +// // let expression be an instance of Expression{} +// +// deleteInput := dynamodb.DeleteItemInput{ +// ConditionExpression: expression.Condition(), +// ExpressionAttributeNames: expression.Names(), +// ExpressionAttributeValues: expression.Values(), +// Key: map[string]types.AttributeValue{ +// "PartitionKey": &types.AttributeValueMemberS{Value: "SomeKey"}, +// }, +// TableName: aws.String("SomeTable"), +// } +func (e Expression) Condition() *string { + return e.returnExpression(condition) +} + +// Filter returns the *string corresponding to the Filter Expression of the +// argument Expression. This method is used to satisfy the members of DynamoDB +// input structs. If the Expression does not have a filter expression this +// method returns nil. +// +// Example: +// +// // let expression be an instance of Expression{} +// +// queryInput := dynamodb.QueryInput{ +// KeyConditionExpression: expression.KeyCondition(), +// FilterExpression: expression.Filter(), +// ExpressionAttributeNames: expression.Names(), +// ExpressionAttributeValues: expression.Values(), +// TableName: aws.String("SomeTable"), +// } +func (e Expression) Filter() *string { + return e.returnExpression(filter) +} + +// Projection returns the *string corresponding to the Projection Expression +// of the argument Expression. This method is used to satisfy the members of +// DynamoDB input structs. If the Expression does not have a projection +// expression this method returns nil. +// +// Example: +// +// // let expression be an instance of Expression{} +// +// queryInput := dynamodb.QueryInput{ +// KeyConditionExpression: expression.KeyCondition(), +// ProjectionExpression: expression.Projection(), +// ExpressionAttributeNames: expression.Names(), +// ExpressionAttributeValues: expression.Values(), +// TableName: aws.String("SomeTable"), +// } +func (e Expression) Projection() *string { + return e.returnExpression(projection) +} + +// KeyCondition returns the *string corresponding to the Key Condition +// Expression of the argument Expression. This method is used to satisfy the +// members of DynamoDB input structs. If the argument Expression does not have a +// KeyConditionExpression, KeyCondition() returns nil. +// +// Example: +// +// // let expression be an instance of Expression{} +// +// queryInput := dynamodb.QueryInput{ +// KeyConditionExpression: expression.KeyCondition(), +// ProjectionExpression: expression.Projection(), +// ExpressionAttributeNames: expression.Names(), +// ExpressionAttributeValues: expression.Values(), +// TableName: aws.String("SomeTable"), +// } +func (e Expression) KeyCondition() *string { + return e.returnExpression(keyCondition) +} + +// Update returns the *string corresponding to the Update Expression of the +// argument Expression. This method is used to satisfy the members of DynamoDB +// input structs. If the argument Expression does not have a UpdateExpression, +// Update() returns nil. +// +// Example: +// +// // let expression be an instance of Expression{} +// +// updateInput := dynamodb.UpdateInput{ +// Key: map[string]types.AttributeValue{ +// "PartitionKey": &types.AttributeValueMemberS{Value: "someKey"}, +// }, +// UpdateExpression: expression.Update(), +// ExpressionAttributeNames: expression.Names(), +// ExpressionAttributeValues: expression.Values(), +// TableName: aws.String("SomeTable"), +// } +func (e Expression) Update() *string { + return e.returnExpression(update) +} + +// Names returns the map[string]*string corresponding to the +// ExpressionAttributeNames of the argument Expression. This method is used to +// satisfy the members of DynamoDB input structs. If Expression does not use +// ExpressionAttributeNames, this method returns nil. The +// ExpressionAttributeNames and ExpressionAttributeValues member of the input +// struct must always be assigned when using the Expression struct since all +// item attribute names and values are aliased. That means that if the +// ExpressionAttributeNames and ExpressionAttributeValues member is not assigned +// with the corresponding Names() and Values() methods, the DynamoDB operation +// will run into a logic error. +// +// Example: +// +// // let expression be an instance of Expression{} +// +// queryInput := dynamodb.QueryInput{ +// KeyConditionExpression: expression.KeyCondition(), +// ProjectionExpression: expression.Projection(), +// ExpressionAttributeNames: expression.Names(), +// ExpressionAttributeValues: expression.Values(), +// TableName: aws.String("SomeTable"), +// } +func (e Expression) Names() map[string]string { + return e.namesMap +} + +// Values returns the map[string]*dynamodb.AttributeValue corresponding to +// the ExpressionAttributeValues of the argument Expression. This method is used +// to satisfy the members of DynamoDB input structs. If Expression does not use +// ExpressionAttributeValues, this method returns nil. The +// ExpressionAttributeNames and ExpressionAttributeValues member of the input +// struct must always be assigned when using the Expression struct since all +// item attribute names and values are aliased. That means that if the +// ExpressionAttributeNames and ExpressionAttributeValues member is not assigned +// with the corresponding Names() and Values() methods, the DynamoDB operation +// will run into a logic error. +// +// Example: +// +// // let expression be an instance of Expression{} +// +// queryInput := dynamodb.QueryInput{ +// KeyConditionExpression: expression.KeyCondition(), +// ProjectionExpression: expression.Projection(), +// ExpressionAttributeNames: expression.Names(), +// ExpressionAttributeValues: expression.Values(), +// TableName: aws.String("SomeTable"), +// } +func (e Expression) Values() map[string]types.AttributeValue { + return e.valuesMap +} + +// returnExpression returns *string corresponding to the type of Expression +// string specified by the expressionType. If there is no corresponding +// expression available in Expression, the method returns nil +func (e Expression) returnExpression(expressionType expressionType) *string { + if e.expressionMap == nil { + return nil + } + if s, exists := e.expressionMap[expressionType]; exists { + return &s + } + return nil +} + +// exprNode are the generic nodes that represents both Operands and +// Conditions. The purpose of exprNode is to be able to call an generic +// recursive function on the top level exprNode to be able to determine a root +// node in order to deduplicate name aliases. +// fmtExpr is a string that has escaped characters to refer to +// names/values/children which needs to be aliased at runtime in order to avoid +// duplicate values. The rules are as follows: +// $n: Indicates that an alias of a name needs to be inserted. The +// corresponding name to be alias is in the []names slice. +// $v: Indicates that an alias of a value needs to be inserted. The +// corresponding value to be alias is in the []values slice. +// $c: Indicates that the fmtExpr of a child exprNode needs to be inserted. +// The corresponding child node is in the []children slice. +type exprNode struct { + names []string + values []types.AttributeValue + children []exprNode + fmtExpr string +} + +// aliasList keeps track of all the names we need to alias in the nested +// struct of conditions and operands. This allows each alias to be unique. +// aliasList is passed in as a pointer when buildChildTrees is called in +// order to deduplicate all names within the tree strcuture of the exprNodes. +type aliasList struct { + namesList []string + valuesList []types.AttributeValue +} + +// buildExpressionString returns a string with aliasing for names/values +// specified by aliasList. The string corresponds to the expression that the +// exprNode tree represents. +func (en exprNode) buildExpressionString(aliasList *aliasList) (string, error) { + // Since each exprNode contains a slice of names, values, and children that + // correspond to the escaped characters, we an index to traverse the slices + index := struct { + name, value, children int + }{} + + formattedExpression := en.fmtExpr + + for i := 0; i < len(formattedExpression); { + if formattedExpression[i] != '$' { + i++ + continue + } + + if i == len(formattedExpression)-1 { + return "", fmt.Errorf("buildexprNode error: invalid escape character") + } + + var alias string + var err error + // if an escaped character is found, substitute it with the proper alias + // TODO consider AST instead of string in the future + switch formattedExpression[i+1] { + case 'n': + alias, err = substitutePath(index.name, en, aliasList) + if err != nil { + return "", err + } + index.name++ + + case 'v': + alias, err = substituteValue(index.value, en, aliasList) + if err != nil { + return "", err + } + index.value++ + + case 'c': + alias, err = substituteChild(index.children, en, aliasList) + if err != nil { + return "", err + } + index.children++ + + default: + return "", fmt.Errorf("buildexprNode error: invalid escape rune %#v", formattedExpression[i+1]) + } + formattedExpression = formattedExpression[:i] + alias + formattedExpression[i+2:] + i += len(alias) + } + + return formattedExpression, nil +} + +// substitutePath substitutes the escaped character $n with the appropriate +// alias. +func substitutePath(index int, node exprNode, aliasList *aliasList) (string, error) { + if index >= len(node.names) { + return "", fmt.Errorf("substitutePath error: exprNode []names out of range") + } + str, err := aliasList.aliasPath(node.names[index]) + if err != nil { + return "", err + } + return str, nil +} + +// substituteValue substitutes the escaped character $v with the appropriate +// alias. +func substituteValue(index int, node exprNode, aliasList *aliasList) (string, error) { + if index >= len(node.values) { + return "", fmt.Errorf("substituteValue error: exprNode []values out of range") + } + str, err := aliasList.aliasValue(node.values[index]) + if err != nil { + return "", err + } + return str, nil +} + +// substituteChild substitutes the escaped character $c with the appropriate +// alias. +func substituteChild(index int, node exprNode, aliasList *aliasList) (string, error) { + if index >= len(node.children) { + return "", fmt.Errorf("substituteChild error: exprNode []children out of range") + } + return node.children[index].buildExpressionString(aliasList) +} + +// aliasValue returns the corresponding alias to the dav value argument. Since +// values are not deduplicated as of now, all values are just appended to the +// aliasList and given the index as the alias. +func (al *aliasList) aliasValue(dav types.AttributeValue) (string, error) { + al.valuesList = append(al.valuesList, dav) + return fmt.Sprintf(":%d", len(al.valuesList)-1), nil +} + +// aliasPath returns the corresponding alias to the argument string. The +// argument is checked against all existing aliasList names in order to avoid +// duplicate strings getting two different aliases. +func (al *aliasList) aliasPath(nm string) (string, error) { + for ind, name := range al.namesList { + if nm == name { + return fmt.Sprintf("#%d", ind), nil + } + } + al.namesList = append(al.namesList, nm) + return fmt.Sprintf("#%d", len(al.namesList)-1), nil +} diff --git a/feature/dynamodb/expression/expression_test.go b/feature/dynamodb/expression/expression_test.go new file mode 100644 index 00000000000..e748277d281 --- /dev/null +++ b/feature/dynamodb/expression/expression_test.go @@ -0,0 +1,1101 @@ +package expression + +import ( + "reflect" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +type exprErrorMode string + +const ( + noExpressionError exprErrorMode = "" + // invalidEscChar error will occer if the escape char '$' is either followed + // by an unsupported character or if the escape char is the last character + invalidEscChar = "invalid escape" + // outOfRange error will occur if there are more escaped chars than there are + // actual values to be aliased. + outOfRange = "out of range" + // invalidBuilderOperand error will occur if an invalid operand is used + // as input for Build() + invalidExpressionBuildOperand = "BuildOperand error" + // unsetBuilder error will occur if Build() is called on an unset Builder + unsetBuilder = "unset parameter: Builder" + // unsetConditionBuilder error will occur if an unset ConditionBuilder is + // used in WithCondition() + unsetConditionBuilder = "unset parameter: ConditionBuilder" +) + +func TestBuild(t *testing.T) { + cases := []struct { + name string + input Builder + expected Expression + err exprErrorMode + }{ + { + name: "condition", + input: NewBuilder().WithCondition(Name("foo").Equal(Value(5))), + expected: Expression{ + expressionMap: map[expressionType]string{ + condition: "#0 = :0", + }, + namesMap: map[string]string{ + "#0": "foo", + }, + valuesMap: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + }, + }, + }, + { + name: "projection", + input: NewBuilder().WithProjection(NamesList(Name("foo"), Name("bar"), Name("baz"))), + expected: Expression{ + expressionMap: map[expressionType]string{ + projection: "#0, #1, #2", + }, + namesMap: map[string]string{ + "#0": "foo", + "#1": "bar", + "#2": "baz", + }, + }, + }, + { + name: "keyCondition", + input: NewBuilder().WithKeyCondition(Key("foo").Equal(Value(5))), + expected: Expression{ + expressionMap: map[expressionType]string{ + keyCondition: "#0 = :0", + }, + namesMap: map[string]string{ + "#0": "foo", + }, + valuesMap: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + }, + }, + }, + { + name: "filter", + input: NewBuilder().WithFilter(Name("foo").Equal(Value(5))), + expected: Expression{ + expressionMap: map[expressionType]string{ + filter: "#0 = :0", + }, + namesMap: map[string]string{ + "#0": "foo", + }, + valuesMap: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + }, + }, + }, + { + name: "update", + input: NewBuilder().WithUpdate(Set(Name("foo"), Value(5))), + expected: Expression{ + expressionMap: map[expressionType]string{ + update: "SET #0 = :0\n", + }, + namesMap: map[string]string{ + "#0": "foo", + }, + valuesMap: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + }, + }, + }, + { + name: "compound", + input: NewBuilder(). + WithCondition(Name("foo").Equal(Value(5))). + WithFilter(Name("bar").LessThan(Value(6))). + WithProjection(NamesList(Name("foo"), Name("bar"), Name("baz"))). + WithKeyCondition(Key("foo").Equal(Value(5))). + WithUpdate(Set(Name("foo"), Value(5))), + expected: Expression{ + expressionMap: map[expressionType]string{ + condition: "#0 = :0", + filter: "#1 < :1", + projection: "#0, #1, #2", + keyCondition: "#0 = :2", + update: "SET #0 = :3\n", + }, + namesMap: map[string]string{ + "#0": "foo", + "#1": "bar", + "#2": "baz", + }, + valuesMap: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + ":1": &types.AttributeValueMemberN{Value: "6"}, + ":2": &types.AttributeValueMemberN{Value: "5"}, + ":3": &types.AttributeValueMemberN{Value: "5"}, + }, + }, + }, + { + name: "invalid Builder", + input: NewBuilder().WithCondition(Name("").Equal(Value(5))), + err: invalidExpressionBuildOperand, + }, + { + name: "unset Builder", + input: Builder{}, + err: unsetBuilder, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.Build() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestCondition(t *testing.T) { + cases := []struct { + name string + input Builder + expected *string + err exprErrorMode + }{ + { + name: "condition", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + condition: Name("foo").Equal(Value(5)), + }, + }, + expected: aws.String("#0 = :0"), + }, + { + name: "unset builder", + input: Builder{}, + err: unsetBuilder, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := c.input.Build() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + } + actual := expr.Condition() + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestFilter(t *testing.T) { + cases := []struct { + name string + input Builder + expected *string + err exprErrorMode + }{ + { + name: "filter", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + filter: Name("foo").Equal(Value(5)), + }, + }, + expected: aws.String("#0 = :0"), + }, + { + name: "unset builder", + input: Builder{}, + err: unsetBuilder, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := c.input.Build() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + } + actual := expr.Filter() + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestProjection(t *testing.T) { + cases := []struct { + name string + input Builder + expected *string + err exprErrorMode + }{ + { + name: "projection", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + projection: NamesList(Name("foo"), Name("bar"), Name("baz")), + }, + }, + expected: aws.String("#0, #1, #2"), + }, + { + name: "unset builder", + input: Builder{}, + err: unsetBuilder, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := c.input.Build() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + } + actual := expr.Projection() + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestKeyCondition(t *testing.T) { + cases := []struct { + name string + input Builder + expected *string + err exprErrorMode + }{ + { + name: "keyCondition", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + keyCondition: KeyConditionBuilder{ + operandList: []OperandBuilder{ + KeyBuilder{ + key: "foo", + }, + ValueBuilder{ + value: 5, + }, + }, + mode: equalKeyCond, + }, + }, + }, + expected: aws.String("#0 = :0"), + }, + { + name: "empty builder", + input: Builder{}, + err: unsetBuilder, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := c.input.Build() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + } + actual := expr.KeyCondition() + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestUpdate(t *testing.T) { + cases := []struct { + name string + input Builder + expected *string + err exprErrorMode + }{ + { + name: "update", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + update: UpdateBuilder{ + operationList: map[operationMode][]operationBuilder{ + setOperation: { + { + name: NameBuilder{ + name: "foo", + }, + value: ValueBuilder{ + value: 5, + }, + mode: setOperation, + }, + }, + }, + }, + }, + }, + expected: aws.String("SET #0 = :0\n"), + }, + { + name: "multiple sets", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + update: UpdateBuilder{ + operationList: map[operationMode][]operationBuilder{ + setOperation: { + { + name: NameBuilder{ + name: "foo", + }, + value: ValueBuilder{ + value: 5, + }, + mode: setOperation, + }, + { + name: NameBuilder{ + name: "bar", + }, + value: ValueBuilder{ + value: 6, + }, + mode: setOperation, + }, + { + name: NameBuilder{ + name: "baz", + }, + value: ValueBuilder{ + value: 7, + }, + mode: setOperation, + }, + }, + }, + }, + }, + }, + expected: aws.String("SET #0 = :0, #1 = :1, #2 = :2\n"), + }, + { + name: "unset builder", + input: Builder{}, + err: unsetBuilder, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := c.input.Build() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + } + actual := expr.Update() + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestNames(t *testing.T) { + cases := []struct { + name string + input Builder + expected map[string]string + err exprErrorMode + }{ + { + name: "projection", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + projection: NamesList(Name("foo"), Name("bar"), Name("baz")), + }, + }, + expected: map[string]string{ + "#0": "foo", + "#1": "bar", + "#2": "baz", + }, + }, + { + name: "aggregate", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + condition: ConditionBuilder{ + operandList: []OperandBuilder{ + NameBuilder{ + name: "foo", + }, + ValueBuilder{ + value: 5, + }, + }, + mode: equalCond, + }, + filter: ConditionBuilder{ + operandList: []OperandBuilder{ + NameBuilder{ + name: "bar", + }, + ValueBuilder{ + value: 6, + }, + }, + mode: lessThanCond, + }, + projection: ProjectionBuilder{ + names: []NameBuilder{ + { + name: "foo", + }, + { + name: "bar", + }, + { + name: "baz", + }, + }, + }, + }, + }, + expected: map[string]string{ + "#0": "foo", + "#1": "bar", + "#2": "baz", + }, + }, + { + name: "unset", + input: Builder{}, + err: unsetBuilder, + }, + { + name: "unset ConditionBuilder", + input: NewBuilder().WithCondition(ConditionBuilder{}), + err: unsetConditionBuilder, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := c.input.Build() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + } + actual := expr.Names() + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestValues(t *testing.T) { + cases := []struct { + name string + input Builder + expected map[string]types.AttributeValue + err exprErrorMode + }{ + { + name: "empty list", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + update: Name("groups").Equal(Value([]string{})), + }, + }, + expected: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberL{Value: []types.AttributeValue{}}, + }, + }, + { + name: "dynamodb.AttributeValue values are used directly", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + update: Name("key").Equal(Value(&types.AttributeValueMemberS{Value: "value"})), + }, + }, + expected: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberS{Value: "value"}, + }, + }, + { + name: "condition", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + condition: Name("foo").Equal(Value(5)), + }, + }, + expected: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + }, + }, + { + name: "aggregate", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + condition: ConditionBuilder{ + operandList: []OperandBuilder{ + NameBuilder{ + name: "foo", + }, + ValueBuilder{ + value: 5, + }, + }, + mode: equalCond, + }, + filter: ConditionBuilder{ + operandList: []OperandBuilder{ + NameBuilder{ + name: "bar", + }, + ValueBuilder{ + value: 6, + }, + }, + mode: lessThanCond, + }, + projection: ProjectionBuilder{ + names: []NameBuilder{ + { + name: "foo", + }, + { + name: "bar", + }, + { + name: "baz", + }, + }, + }, + }, + }, + expected: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + ":1": &types.AttributeValueMemberN{Value: "6"}, + }, + }, + { + name: "unset", + input: Builder{}, + err: unsetBuilder, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := c.input.Build() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + } + actual := expr.Values() + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestBuildChildTrees(t *testing.T) { + cases := []struct { + name string + input Builder + expectedaliasList aliasList + expectedStringMap map[expressionType]string + err exprErrorMode + }{ + { + name: "aggregate", + input: Builder{ + expressionMap: map[expressionType]treeBuilder{ + condition: ConditionBuilder{ + operandList: []OperandBuilder{ + NameBuilder{ + name: "foo", + }, + ValueBuilder{ + value: 5, + }, + }, + mode: equalCond, + }, + filter: ConditionBuilder{ + operandList: []OperandBuilder{ + NameBuilder{ + name: "bar", + }, + ValueBuilder{ + value: 6, + }, + }, + mode: lessThanCond, + }, + projection: ProjectionBuilder{ + names: []NameBuilder{ + { + name: "foo", + }, + { + name: "bar", + }, + { + name: "baz", + }, + }, + }, + }, + }, + expectedaliasList: aliasList{ + namesList: []string{"foo", "bar", "baz"}, + valuesList: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + &types.AttributeValueMemberN{Value: "6"}, + }, + }, + expectedStringMap: map[expressionType]string{ + condition: "#0 = :0", + filter: "#1 < :1", + projection: "#0, #1, #2", + }, + }, + { + name: "unset", + input: Builder{}, + expectedaliasList: aliasList{}, + expectedStringMap: map[expressionType]string{}, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actualAL, actualSM, err := c.input.buildChildTrees() + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + } + if e, a := c.expectedaliasList, actualAL; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := c.expectedStringMap, actualSM; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestBuildExpressionString(t *testing.T) { + cases := []struct { + name string + input exprNode + expectedNames map[string]*string + expectedValues map[string]types.AttributeValue + expectedExpression string + err exprErrorMode + }{ + { + name: "basic name", + input: exprNode{ + names: []string{"foo"}, + fmtExpr: "$n", + }, + + expectedValues: map[string]types.AttributeValue{}, + expectedNames: map[string]*string{ + "#0": aws.String("foo"), + }, + expectedExpression: "#0", + }, + { + name: "basic value", + input: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + expectedNames: map[string]*string{}, + expectedValues: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + }, + expectedExpression: ":0", + }, + { + name: "nested path", + input: exprNode{ + names: []string{"foo", "bar"}, + fmtExpr: "$n.$n", + }, + + expectedValues: map[string]types.AttributeValue{}, + expectedNames: map[string]*string{ + "#0": aws.String("foo"), + "#1": aws.String("bar"), + }, + expectedExpression: "#0.#1", + }, + { + name: "nested path with index", + input: exprNode{ + names: []string{"foo", "bar", "baz"}, + fmtExpr: "$n.$n[0].$n", + }, + expectedValues: map[string]types.AttributeValue{}, + expectedNames: map[string]*string{ + "#0": aws.String("foo"), + "#1": aws.String("bar"), + "#2": aws.String("baz"), + }, + expectedExpression: "#0.#1[0].#2", + }, + { + name: "basic size", + input: exprNode{ + names: []string{"foo"}, + fmtExpr: "size ($n)", + }, + expectedValues: map[string]types.AttributeValue{}, + expectedNames: map[string]*string{ + "#0": aws.String("foo"), + }, + expectedExpression: "size (#0)", + }, + { + name: "duplicate path name", + input: exprNode{ + names: []string{"foo", "foo"}, + fmtExpr: "$n.$n", + }, + expectedValues: map[string]types.AttributeValue{}, + expectedNames: map[string]*string{ + "#0": aws.String("foo"), + }, + expectedExpression: "#0.#0", + }, + { + name: "equal expression", + input: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + + expectedNames: map[string]*string{ + "#0": aws.String("foo"), + }, + expectedValues: map[string]types.AttributeValue{ + ":0": &types.AttributeValueMemberN{Value: "5"}, + }, + expectedExpression: "#0 = :0", + }, + { + name: "missing char after $", + input: exprNode{ + names: []string{"foo", "foo"}, + fmtExpr: "$n.$", + }, + err: invalidEscChar, + }, + { + name: "names out of range", + input: exprNode{ + names: []string{"foo"}, + fmtExpr: "$n.$n", + }, + err: outOfRange, + }, + { + name: "values out of range", + input: exprNode{ + fmtExpr: "$v", + }, + err: outOfRange, + }, + { + name: "children out of range", + input: exprNode{ + fmtExpr: "$c", + }, + err: outOfRange, + }, + { + name: "invalid escape char", + input: exprNode{ + fmtExpr: "$!", + }, + err: invalidEscChar, + }, + { + name: "unset exprNode", + input: exprNode{}, + expectedExpression: "", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := c.input.buildExpressionString(&aliasList{}) + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expectedExpression, expr; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestReturnExpression(t *testing.T) { + cases := []struct { + name string + input Expression + expected *string + }{ + { + name: "projection exists", + input: Expression{ + expressionMap: map[expressionType]string{ + projection: "#0, #1, #2", + }, + }, + expected: aws.String("#0, #1, #2"), + }, + { + name: "projection not exists", + input: Expression{ + expressionMap: map[expressionType]string{}, + }, + expected: nil, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := c.input.returnExpression(projection) + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + }) + } +} + +func TestAliasValue(t *testing.T) { + cases := []struct { + name string + input *aliasList + expected string + err exprErrorMode + }{ + { + name: "first item", + input: &aliasList{}, + expected: ":0", + }, + { + name: "fifth item", + input: &aliasList{ + valuesList: []types.AttributeValue{ + types.AttributeValue(nil), + types.AttributeValue(nil), + types.AttributeValue(nil), + types.AttributeValue(nil), + }, + }, + expected: ":4", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + str, err := c.input.aliasValue(types.AttributeValue(nil)) + + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, str; e != a { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestAliasPath(t *testing.T) { + cases := []struct { + name string + inputList *aliasList + inputName string + expected string + err exprErrorMode + }{ + { + name: "new unique item", + inputList: &aliasList{}, + inputName: "foo", + expected: "#0", + }, + { + name: "duplicate item", + inputList: &aliasList{ + namesList: []string{ + "foo", + "bar", + }, + }, + inputName: "foo", + expected: "#0", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + str, err := c.inputList.aliasPath(c.inputName) + + if c.err != noExpressionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, str; e != a { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} diff --git a/feature/dynamodb/expression/go.mod b/feature/dynamodb/expression/go.mod new file mode 100644 index 00000000000..26f11aaa215 --- /dev/null +++ b/feature/dynamodb/expression/go.mod @@ -0,0 +1,16 @@ +module github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression + +go 1.15 + +require ( + github.com/aws/aws-sdk-go-v2 v0.30.1-0.20201216221327-f18ebfdeb472 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v0.0.0-20201217001131-4ae90bb70aa7 + github.com/aws/aws-sdk-go-v2/service/dynamodb v0.30.0 +) + +replace ( + github.com/aws/aws-sdk-go-v2 => ../../../ + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue => ../../../feature/dynamodb/attributevalue + github.com/aws/aws-sdk-go-v2/service/dynamodb => ../../../service/dynamodb/ + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams => ../../../service/dynamodbstreams +) diff --git a/feature/dynamodb/expression/go.sum b/feature/dynamodb/expression/go.sum new file mode 100644 index 00000000000..174df60224b --- /dev/null +++ b/feature/dynamodb/expression/go.sum @@ -0,0 +1,55 @@ +github.com/aws/aws-sdk-go v1.35.28/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= +github.com/aws/aws-sdk-go v1.35.37/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.36.8 h1:3nvY3Ax2RC6PN1i0OKppxjq3doHWqiYtvenLQ/oZ5jI= +github.com/aws/aws-sdk-go v1.36.8/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v0.30.0 h1:/CjXUnWXnvdgOqHa65UIs2TODa5D5lm3ty7O0wWuYHY= +github.com/aws/aws-sdk-go-v2 v0.30.0/go.mod h1:vEDjzdktTH+FoEOV6BWgYLEzvPti13Onp5/qQrSiLPg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.1-0.20201113222241-726e4a15683d h1:cnCSbwLs1cqUcDaK6uJ03wFeKnfzB9URzvP7gkBq4to= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.1-0.20201113222241-726e4a15683d/go.mod h1:8ssQ+eALAh5+Z5uix7Ku/rzM1uDVNQrAQx7cNiq1Rwo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.1 h1:97Qz2pFoNTlum+0RNwmUaI9IK9SkT+7CikD8zTnlGTs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.1/go.mod h1:NPTV0PtdITqVGH/e7Xo1Z0TnnKCn2pIH+ZT23dl5Ut0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.2-0.20201216221327-f18ebfdeb472 h1:aidk8O5PMWlZTOXsXzJ1aN5HOBHLMP68VDah+LHOpUY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.2-0.20201216221327-f18ebfdeb472/go.mod h1:NPTV0PtdITqVGH/e7Xo1Z0TnnKCn2pIH+ZT23dl5Ut0= +github.com/awslabs/smithy-go v0.3.0/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.3.1-0.20201104233911-38864709e183/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4 h1:Aj5dOF+lDoEhU92no7YZF0IokuWGjiNrcm/DGIG3iII= +github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.4.0 h1:El0KyKn4zdM3pLuWJlgoeitQuu/mjwUPssr7L3xu3vs= +github.com/awslabs/smithy-go v0.4.0/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.4.1-0.20201208232924-b8cdbaa577ff h1:mtSekcc5R2mJG5+cdIlL15WD//Lobtzil5hkcr8WhiA= +github.com/awslabs/smithy-go v0.4.1-0.20201208232924-b8cdbaa577ff/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.4.1-0.20201216214517-20e212c92831 h1:1yUZARt7tftsJrn/7eEIs6qix36mIm+/5Ui72B7ClCA= +github.com/awslabs/smithy-go v0.4.1-0.20201216214517-20e212c92831/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/feature/dynamodb/expression/key_condition.go b/feature/dynamodb/expression/key_condition.go new file mode 100644 index 00000000000..e8917b67746 --- /dev/null +++ b/feature/dynamodb/expression/key_condition.go @@ -0,0 +1,561 @@ +package expression + +import ( + "fmt" +) + +// keyConditionMode specifies the types of the struct KeyConditionBuilder, +// representing the different types of KeyConditions (i.e. And, Or, Between, ...) +type keyConditionMode int + +const ( + // unsetKeyCond catches errors for unset KeyConditionBuilder structs + unsetKeyCond keyConditionMode = iota + // invalidKeyCond catches errors in the construction of KeyConditionBuilder structs + invalidKeyCond + // equalKeyCond represents the Equals KeyCondition + equalKeyCond + // lessThanKeyCond represents the Less Than KeyCondition + lessThanKeyCond + // lessThanEqualKeyCond represents the Less Than Or Equal To KeyCondition + lessThanEqualKeyCond + // greaterThanKeyCond represents the Greater Than KeyCondition + greaterThanKeyCond + // greaterThanEqualKeyCond represents the Greater Than Or Equal To KeyCondition + greaterThanEqualKeyCond + // andKeyCond represents the Logical And KeyCondition + andKeyCond + // betweenKeyCond represents the Between KeyCondition + betweenKeyCond + // beginsWithKeyCond represents the Begins With KeyCondition + beginsWithKeyCond +) + +// KeyConditionBuilder represents Key Condition Expressions in DynamoDB. +// KeyConditionBuilders are the building blocks of Expressions. +// More Information at: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions +type KeyConditionBuilder struct { + operandList []OperandBuilder + keyConditionList []KeyConditionBuilder + mode keyConditionMode +} + +// KeyEqual returns a KeyConditionBuilder representing the equality clause +// of the two argument OperandBuilders. The resulting KeyConditionBuilder can be +// used as a part of other Key Condition Expressions or as an argument to the +// WithKeyCondition() method for the Builder struct. +// +// Example: +// +// // keyCondition represents the equal clause of the key "foo" and the +// // value 5 +// keyCondition := expression.KeyEqual(expression.Key("foo"), expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithKeyCondition(keyCondition) +// +// Expression Equivalent: +// +// expression.KeyEqual(expression.Key("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo = :five" +func KeyEqual(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyConditionBuilder{ + operandList: []OperandBuilder{keyBuilder, valueBuilder}, + mode: equalKeyCond, + } +} + +// Equal returns a KeyConditionBuilder representing the equality clause of +// the two argument OperandBuilders. The resulting KeyConditionBuilder can be +// used as a part of other Key Condition Expressions or as an argument to the +// WithKeyCondition() method for the Builder struct. +// +// Example: +// +// // keyCondition represents the equal clause of the key "foo" and the +// // value 5 +// keyCondition := expression.Key("foo").Equal(expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// // Used to make an Builder +// builder := expression.NewBuilder().WithKeyCondition(keyCondition) +// +// Expression Equivalent: +// +// expression.Key("foo").Equal(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo = :five" +func (kb KeyBuilder) Equal(valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyEqual(kb, valueBuilder) +} + +// KeyLessThan returns a KeyConditionBuilder representing the less than +// clause of the two argument OperandBuilders. The resulting KeyConditionBuilder +// can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the less than clause of the key "foo" and the +// // value 5 +// keyCondition := expression.KeyLessThan(expression.Key("foo"), expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.KeyLessThan(expression.Key("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo < :five" +func KeyLessThan(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyConditionBuilder{ + operandList: []OperandBuilder{keyBuilder, valueBuilder}, + mode: lessThanKeyCond, + } +} + +// LessThan returns a KeyConditionBuilder representing the less than clause +// of the two argument OperandBuilders. The resulting KeyConditionBuilder can be +// used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the less than clause of the key "foo" and the +// // value 5 +// keyCondition := expression.Key("foo").LessThan(expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.Key("foo").LessThan(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo < :five" +func (kb KeyBuilder) LessThan(valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyLessThan(kb, valueBuilder) +} + +// KeyLessThanEqual returns a KeyConditionBuilder representing the less than +// equal to clause of the two argument OperandBuilders. The resulting +// KeyConditionBuilder can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the less than equal to clause of the key +// // "foo" and the value 5 +// keyCondition := expression.KeyLessThanEqual(expression.Key("foo"), expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.KeyLessThanEqual(expression.Key("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo <= :five" +func KeyLessThanEqual(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyConditionBuilder{ + operandList: []OperandBuilder{keyBuilder, valueBuilder}, + mode: lessThanEqualKeyCond, + } +} + +// LessThanEqual returns a KeyConditionBuilder representing the less than +// equal to clause of the two argument OperandBuilders. The resulting +// KeyConditionBuilder can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the less than equal to clause of the key +// // "foo" and the value 5 +// keyCondition := expression.Key("foo").LessThanEqual(expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.Key("foo").LessThanEqual(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo <= :five" +func (kb KeyBuilder) LessThanEqual(valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyLessThanEqual(kb, valueBuilder) +} + +// KeyGreaterThan returns a KeyConditionBuilder representing the greater +// than clause of the two argument OperandBuilders. The resulting +// KeyConditionBuilder can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the greater than clause of the key "foo" and +// // the value 5 +// keyCondition := expression.KeyGreaterThan(expression.Key("foo"), expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.KeyGreaterThan(expression.Key("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo > :five" +func KeyGreaterThan(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyConditionBuilder{ + operandList: []OperandBuilder{keyBuilder, valueBuilder}, + mode: greaterThanKeyCond, + } +} + +// GreaterThan returns a KeyConditionBuilder representing the greater than +// clause of the two argument OperandBuilders. The resulting KeyConditionBuilder +// can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // key condition represents the greater than clause of the key "foo" and +// // the value 5 +// keyCondition := expression.Key("foo").GreaterThan(expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.Key("foo").GreaterThan(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo > :five" +func (kb KeyBuilder) GreaterThan(valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyGreaterThan(kb, valueBuilder) +} + +// KeyGreaterThanEqual returns a KeyConditionBuilder representing the +// greater than equal to clause of the two argument OperandBuilders. The +// resulting KeyConditionBuilder can be used as a part of other Key Condition +// Expressions. +// +// Example: +// +// // keyCondition represents the greater than equal to clause of the key +// // "foo" and the value 5 +// keyCondition := expression.KeyGreaterThanEqual(expression.Key("foo"), expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.KeyGreaterThanEqual(expression.Key("foo"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo >= :five" +func KeyGreaterThanEqual(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyConditionBuilder{ + operandList: []OperandBuilder{keyBuilder, valueBuilder}, + mode: greaterThanEqualKeyCond, + } +} + +// GreaterThanEqual returns a KeyConditionBuilder representing the greater +// than equal to clause of the two argument OperandBuilders. The resulting +// KeyConditionBuilder can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the greater than equal to clause of the key +// // "foo" and the value 5 +// keyCondition := expression.Key("foo").GreaterThanEqual(expression.Value(5)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.Key("foo").GreaterThanEqual(expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "foo >= :five" +func (kb KeyBuilder) GreaterThanEqual(valueBuilder ValueBuilder) KeyConditionBuilder { + return KeyGreaterThanEqual(kb, valueBuilder) +} + +// KeyAnd returns a KeyConditionBuilder representing the logical AND clause +// of the two argument KeyConditionBuilders. The resulting KeyConditionBuilder +// can be used as an argument to the WithKeyCondition() method for the Builder +// struct. +// +// Example: +// +// // keyCondition represents the key condition where the partition key +// // "TeamName" is equal to value "Wildcats" and sort key "Number" is equal +// // to value 1 +// keyCondition := expression.KeyAnd(expression.Key("TeamName").Equal(expression.Value("Wildcats")), expression.Key("Number").Equal(expression.Value(1))) +// +// // Used to make an Builder +// builder := expression.NewBuilder().WithKeyCondition(keyCondition) +// +// Expression Equivalent: +// +// expression.KeyAnd(expression.Key("TeamName").Equal(expression.Value("Wildcats")), expression.Key("Number").Equal(expression.Value(1))) +// // Let #NUMBER, :teamName, and :one be ExpressionAttributeName and +// // ExpressionAttributeValues representing the item attribute "Number", +// // the value "Wildcats", and the value 1 +// "(TeamName = :teamName) AND (#NUMBER = :one)" +func KeyAnd(left, right KeyConditionBuilder) KeyConditionBuilder { + if left.mode != equalKeyCond { + return KeyConditionBuilder{ + mode: invalidKeyCond, + } + } + if right.mode == andKeyCond { + return KeyConditionBuilder{ + mode: invalidKeyCond, + } + } + return KeyConditionBuilder{ + keyConditionList: []KeyConditionBuilder{left, right}, + mode: andKeyCond, + } +} + +// And returns a KeyConditionBuilder representing the logical AND clause of +// the two argument KeyConditionBuilders. The resulting KeyConditionBuilder can +// be used as an argument to the WithKeyCondition() method for the Builder +// struct. +// +// Example: +// +// // keyCondition represents the key condition where the partition key +// // "TeamName" is equal to value "Wildcats" and sort key "Number" is equal +// // to value 1 +// keyCondition := expression.Key("TeamName").Equal(expression.Value("Wildcats")).And(expression.Key("Number").Equal(expression.Value(1))) +// +// // Used to make an Builder +// builder := expression.NewBuilder().WithKeyCondition(keyCondition) +// +// Expression Equivalent: +// +// expression.Key("TeamName").Equal(expression.Value("Wildcats")).And(expression.Key("Number").Equal(expression.Value(1))) +// // Let #NUMBER, :teamName, and :one be ExpressionAttributeName and +// // ExpressionAttributeValues representing the item attribute "Number", +// // the value "Wildcats", and the value 1 +// "(TeamName = :teamName) AND (#NUMBER = :one)" +func (kcb KeyConditionBuilder) And(right KeyConditionBuilder) KeyConditionBuilder { + return KeyAnd(kcb, right) +} + +// KeyBetween returns a KeyConditionBuilder representing the result of the +// BETWEEN function in DynamoDB Key Condition Expressions. The resulting +// KeyConditionBuilder can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the boolean key condition of whether the value +// // of the key "foo" is between values 5 and 10 +// keyCondition := expression.KeyBetween(expression.Key("foo"), expression.Value(5), expression.Value(10)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.KeyBetween(expression.Key("foo"), expression.Value(5), expression.Value(10)) +// // Let :five and :ten be ExpressionAttributeValues representing the +// // values 5 and 10 respectively +// "foo BETWEEN :five AND :ten" +func KeyBetween(keyBuilder KeyBuilder, lower, upper ValueBuilder) KeyConditionBuilder { + return KeyConditionBuilder{ + operandList: []OperandBuilder{keyBuilder, lower, upper}, + mode: betweenKeyCond, + } +} + +// Between returns a KeyConditionBuilder representing the result of the +// BETWEEN function in DynamoDB Key Condition Expressions. The resulting +// KeyConditionBuilder can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the boolean key condition of whether the value +// // of the key "foo" is between values 5 and 10 +// keyCondition := expression.Key("foo").Between(expression.Value(5), expression.Value(10)) +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.Key("foo").Between(expression.Value(5), expression.Value(10)) +// // Let :five and :ten be ExpressionAttributeValues representing the +// // values 5 and 10 respectively +// "foo BETWEEN :five AND :ten" +func (kb KeyBuilder) Between(lower, upper ValueBuilder) KeyConditionBuilder { + return KeyBetween(kb, lower, upper) +} + +// KeyBeginsWith returns a KeyConditionBuilder representing the result of +// the begins_with function in DynamoDB Key Condition Expressions. The resulting +// KeyConditionBuilder can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the boolean key condition of whether the value +// // of the key "foo" is begins with the prefix "bar" +// keyCondition := expression.KeyBeginsWith(expression.Key("foo"), "bar") +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.KeyBeginsWith(expression.Key("foo"), "bar") +// // Let :bar be an ExpressionAttributeValue representing the value "bar" +// "begins_with(foo, :bar)" +func KeyBeginsWith(keyBuilder KeyBuilder, prefix string) KeyConditionBuilder { + valueBuilder := ValueBuilder{ + value: prefix, + } + return KeyConditionBuilder{ + operandList: []OperandBuilder{keyBuilder, valueBuilder}, + mode: beginsWithKeyCond, + } +} + +// BeginsWith returns a KeyConditionBuilder representing the result of the +// begins_with function in DynamoDB Key Condition Expressions. The resulting +// KeyConditionBuilder can be used as a part of other Key Condition Expressions. +// +// Example: +// +// // keyCondition represents the boolean key condition of whether the value +// // of the key "foo" is begins with the prefix "bar" +// keyCondition := expression.Key("foo").BeginsWith("bar") +// +// // Used in another Key Condition Expression +// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition) +// +// Expression Equivalent: +// +// expression.Key("foo").BeginsWith("bar") +// // Let :bar be an ExpressionAttributeValue representing the value "bar" +// "begins_with(foo, :bar)" +func (kb KeyBuilder) BeginsWith(prefix string) KeyConditionBuilder { + return KeyBeginsWith(kb, prefix) +} + +// buildTree builds a tree structure of exprNodes based on the tree +// structure of the input KeyConditionBuilder's child KeyConditions/Operands. +// buildTree() satisfies the treeBuilder interface so KeyConditionBuilder can be +// a part of Expression struct. +func (kcb KeyConditionBuilder) buildTree() (exprNode, error) { + childNodes, err := kcb.buildChildNodes() + if err != nil { + return exprNode{}, err + } + ret := exprNode{ + children: childNodes, + } + + switch kcb.mode { + case equalKeyCond, lessThanKeyCond, lessThanEqualKeyCond, greaterThanKeyCond, greaterThanEqualKeyCond: + return compareBuildKeyCondition(kcb.mode, ret) + case andKeyCond: + return andBuildKeyCondition(kcb, ret) + case betweenKeyCond: + return betweenBuildKeyCondition(ret) + case beginsWithKeyCond: + return beginsWithBuildKeyCondition(ret) + case unsetKeyCond: + return exprNode{}, newUnsetParameterError("buildTree", "KeyConditionBuilder") + case invalidKeyCond: + return exprNode{}, fmt.Errorf("buildKeyCondition error: invalid key condition constructed") + default: + return exprNode{}, fmt.Errorf("buildKeyCondition error: unsupported mode: %v", kcb.mode) + } +} + +// compareBuildKeyCondition is the function to make exprNodes from Compare +// KeyConditionBuilders. compareBuildKeyCondition is only called by the +// buildKeyCondition method. This function assumes that the argument +// KeyConditionBuilder has the right format. +func compareBuildKeyCondition(keyConditionMode keyConditionMode, node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + switch keyConditionMode { + case equalKeyCond: + node.fmtExpr = "$c = $c" + case lessThanKeyCond: + node.fmtExpr = "$c < $c" + case lessThanEqualKeyCond: + node.fmtExpr = "$c <= $c" + case greaterThanKeyCond: + node.fmtExpr = "$c > $c" + case greaterThanEqualKeyCond: + node.fmtExpr = "$c >= $c" + default: + return exprNode{}, fmt.Errorf("build compare key condition error: unsupported mode: %v", keyConditionMode) + } + + return node, nil +} + +// andBuildKeyCondition is the function to make exprNodes from And +// KeyConditionBuilders. andBuildKeyCondition is only called by the +// buildKeyCondition method. This function assumes that the argument +// KeyConditionBuilder has the right format. +func andBuildKeyCondition(keyConditionBuilder KeyConditionBuilder, node exprNode) (exprNode, error) { + if len(keyConditionBuilder.keyConditionList) == 0 && len(keyConditionBuilder.operandList) == 0 { + return exprNode{}, newInvalidParameterError("andBuildKeyCondition", "KeyConditionBuilder") + } + // create a string with escaped characters to substitute them with proper + // aliases during runtime + node.fmtExpr = "($c) AND ($c)" + + return node, nil +} + +// betweenBuildKeyCondition is the function to make exprNodes from Between +// KeyConditionBuilders. betweenBuildKeyCondition is only called by the +// buildKeyCondition method. This function assumes that the argument +// KeyConditionBuilder has the right format. +func betweenBuildKeyCondition(node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "$c BETWEEN $c AND $c" + + return node, nil +} + +// beginsWithBuildKeyCondition is the function to make exprNodes from +// BeginsWith KeyConditionBuilders. beginsWithBuildKeyCondition is only +// called by the buildKeyCondition method. This function assumes that the argument +// KeyConditionBuilder has the right format. +func beginsWithBuildKeyCondition(node exprNode) (exprNode, error) { + // Create a string with special characters that can be substituted later: $c + node.fmtExpr = "begins_with ($c, $c)" + + return node, nil +} + +// buildChildNodes creates the list of the child exprNodes. This avoids +// duplication of code amongst the various buildConditions. +func (kcb KeyConditionBuilder) buildChildNodes() ([]exprNode, error) { + childNodes := make([]exprNode, 0, len(kcb.keyConditionList)+len(kcb.operandList)) + for _, keyCondition := range kcb.keyConditionList { + node, err := keyCondition.buildTree() + if err != nil { + return []exprNode{}, err + } + childNodes = append(childNodes, node) + } + for _, operand := range kcb.operandList { + ope, err := operand.BuildOperand() + if err != nil { + return []exprNode{}, err + } + childNodes = append(childNodes, ope.exprNode) + } + + return childNodes, nil +} diff --git a/feature/dynamodb/expression/key_condition_test.go b/feature/dynamodb/expression/key_condition_test.go new file mode 100644 index 00000000000..5eef7e491a2 --- /dev/null +++ b/feature/dynamodb/expression/key_condition_test.go @@ -0,0 +1,419 @@ +package expression + +import ( + "reflect" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +// keyCondErrorMode will help with error cases and checking error types +type keyCondErrorMode string + +const ( + noKeyConditionError keyCondErrorMode = "" + // unsetKeyCondition error will occur when buildTree() is called on an empty + // KeyConditionBuilder + unsetKeyCondition = "unset parameter: KeyConditionBuilder" + // invalidKeyConditionOperand error will occur when an invalid OperandBuilder is used as + // an argument + invalidKeyConditionOperand = "BuildOperand error" + // invalidKeyConditionFormat error will occur when the first key condition is not an equal + // clause or if more then one And condition is provided + invalidKeyConditionFormat = "buildKeyCondition error: invalid key condition constructed" +) + +func TestKeyCompare(t *testing.T) { + cases := []struct { + name string + input KeyConditionBuilder + expectedNode exprNode + err keyCondErrorMode + }{ + { + name: "key equal", + input: Key("foo").Equal(Value(5)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + { + name: "key less than", + input: Key("foo").LessThan(Value(5)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c < $c", + }, + }, + { + name: "key less than equal", + input: Key("foo").LessThanEqual(Value(5)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c <= $c", + }, + }, + { + name: "key greater than", + input: Key("foo").GreaterThan(Value(5)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c > $c", + }, + }, + { + name: "key greater than equal", + input: Key("foo").GreaterThanEqual(Value(5)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c >= $c", + }, + }, + { + name: "unset KeyConditionBuilder", + input: KeyConditionBuilder{}, + err: unsetKeyCondition, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noKeyConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestKeyBetween(t *testing.T) { + cases := []struct { + name string + input KeyConditionBuilder + expectedNode exprNode + err keyCondErrorMode + }{ + { + name: "key between", + input: Key("foo").Between(Value(5), Value(10)), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "10"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c BETWEEN $c AND $c", + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noKeyConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestKeyBeginsWith(t *testing.T) { + cases := []struct { + name string + input KeyConditionBuilder + expectedNode exprNode + err keyCondErrorMode + }{ + { + name: "key begins with", + input: Key("foo").BeginsWith("bar"), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "begins_with ($c, $c)", + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noKeyConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestKeyAnd(t *testing.T) { + cases := []struct { + name string + input KeyConditionBuilder + expectedNode exprNode + err keyCondErrorMode + }{ + { + name: "key and", + input: Key("foo").Equal(Value(5)).And(Key("bar").BeginsWith("baz")), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + { + children: []exprNode{ + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "baz"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "begins_with ($c, $c)", + }, + }, + fmtExpr: "($c) AND ($c)", + }, + }, + { + name: "first condition is not equal", + input: Key("foo").LessThan(Value(5)).And(Key("bar").BeginsWith("baz")), + err: invalidKeyConditionFormat, + }, + { + name: "more then one condition on key", + input: Key("foo").Equal(Value(5)).And(Key("bar").Equal(Value(1)).And(Key("baz").BeginsWith("yar"))), + err: invalidKeyConditionFormat, + }, + { + name: "operand error", + input: Key("").Equal(Value("yikes")), + err: invalidKeyConditionOperand, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noKeyConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestKeyConditionBuildChildNodes(t *testing.T) { + cases := []struct { + name string + input KeyConditionBuilder + expected []exprNode + err keyCondErrorMode + }{ + { + name: "build child nodes", + input: Key("foo").Equal(Value("bar")).And(Key("baz").LessThan(Value(10))), + expected: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "bar"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + { + children: []exprNode{ + { + names: []string{"baz"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "10"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c < $c", + }, + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildChildNodes() + if c.err != noKeyConditionError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %#v, got %#v", e, a) + } + } + }) + } +} diff --git a/feature/dynamodb/expression/operand.go b/feature/dynamodb/expression/operand.go new file mode 100644 index 00000000000..77caed0615e --- /dev/null +++ b/feature/dynamodb/expression/operand.go @@ -0,0 +1,656 @@ +package expression + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +// ValueBuilder represents an item attribute value operand and implements the +// OperandBuilder interface. Methods and functions in the package take +// ValueBuilder as an argument and establishes relationships between operands. +// ValueBuilder should only be initialized using the function Value(). +// +// Example: +// +// // Create a ValueBuilder representing the string "aValue" +// valueBuilder := expression.Value("aValue") +type ValueBuilder struct { + value interface{} +} + +// NameBuilder represents a name of a top level item attribute or a nested +// attribute. Since NameBuilder represents a DynamoDB Operand, it implements the +// OperandBuilder interface. Methods and functions in the package take +// NameBuilder as an argument and establishes relationships between operands. +// NameBuilder should only be initialized using the function Name(). +// +// Example: +// +// // Create a NameBuilder representing the item attribute "aName" +// nameBuilder := expression.Name("aName") +type NameBuilder struct { + name string +} + +// SizeBuilder represents the output of the function size ("someName"), which +// evaluates to the size of the item attribute defined by "someName". Since +// SizeBuilder represents an operand, SizeBuilder implements the OperandBuilder +// interface. Methods and functions in the package take SizeBuilder as an +// argument and establishes relationships between operands. SizeBuilder should +// only be initialized using the function Size(). +// +// Example: +// +// // Create a SizeBuilder representing the size of the item attribute +// // "aName" +// sizeBuilder := expression.Name("aName").Size() +type SizeBuilder struct { + nameBuilder NameBuilder +} + +// KeyBuilder represents either the partition key or the sort key, both of which +// are top level attributes to some item in DynamoDB. Since KeyBuilder +// represents an operand, KeyBuilder implements the OperandBuilder interface. +// Methods and functions in the package take KeyBuilder as an argument and +// establishes relationships between operands. However, KeyBuilder should only +// be used to describe Key Condition Expressions. KeyBuilder should only be +// initialized using the function Key(). +// +// Example: +// +// // Create a KeyBuilder representing the item key "aKey" +// keyBuilder := expression.Key("aKey") +type KeyBuilder struct { + key string +} + +// setValueMode specifies the type of SetValueBuilder. The default value is +// unsetValue so that an UnsetParameterError when BuildOperand() is called on an +// empty SetValueBuilder. +type setValueMode int + +const ( + unsetValue setValueMode = iota + plusValueMode + minusValueMode + listAppendValueMode + ifNotExistsValueMode +) + +// SetValueBuilder represents the outcome of operator functions supported by the +// DynamoDB Set operation. The operator functions are the following: +// Plus() // Represents the "+" operator +// Minus() // Represents the "-" operator +// ListAppend() +// IfNotExists() +// For documentation on the above functions, +// see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET +// Since SetValueBuilder represents an operand, it implements the OperandBuilder +// interface. SetValueBuilder structs are used as arguments to the Set() +// function. SetValueBuilders should only initialize a SetValueBuilder using the +// functions listed above. +type SetValueBuilder struct { + leftOperand OperandBuilder + rightOperand OperandBuilder + mode setValueMode +} + +// Operand represents an item attribute name or value in DynamoDB. The +// relationship between Operands specified by various builders such as +// ConditionBuilders and UpdateBuilders for example is processed internally to +// write Condition Expressions and Update Expressions respectively. +type Operand struct { + exprNode exprNode +} + +// OperandBuilder represents the idea of Operand which are building blocks to +// DynamoDB Expressions. Package methods and functions can establish +// relationships between operands, representing DynamoDB Expressions. The method +// BuildOperand() is called recursively when the Build() method on the type +// Builder is called. BuildOperand() should never be called externally. +// OperandBuilder and BuildOperand() are exported to allow package functions to +// take an interface as an argument. +type OperandBuilder interface { + BuildOperand() (Operand, error) +} + +// Name creates a NameBuilder. The argument should represent the desired item +// attribute. It is possible to reference nested item attributes by using +// square brackets for lists and dots for maps. For documentation on specifying +// item attributes, +// see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html +// +// Example: +// +// // Specify a top-level attribute +// name := expression.Name("TopLevel") +// // Specify a nested attribute +// nested := expression.Name("Record[6].SongList") +// // Use Name() to create a condition expression +// condition := expression.Name("foo").Equal(expression.Name("bar")) +func Name(name string) NameBuilder { + return NameBuilder{ + name: name, + } +} + +// Value creates a ValueBuilder and sets its value to the argument. The value +// will be marshalled using the attributevalue package, unless it is of +// type types.AttributeValue, where it will be used directly. +// +// Empty slices and maps will be encoded as their respective empty types.AttributeValue +// types. If a NULL value is required, pass a dynamodb.AttributeValue, e.g.: +// emptyList := &types.AttributeValueMemberNULL{Value: true} +// +// Example: +// +// // Use Value() to create a condition expression +// condition := expression.Name("foo").Equal(expression.Value(10)) +// // Use Value() to set the value of a set expression. +// update := Set(expression.Name("greets"), expression.Value(&types.AttributeValueMemberS{Value: "hello"})) +func Value(value interface{}) ValueBuilder { + return ValueBuilder{ + value: value, + } +} + +// Size creates a SizeBuilder representing the size of the item attribute +// specified by the argument NameBuilder. Size() is only valid for certain types +// of item attributes. For documentation, +// see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html +// SizeBuilder is only a valid operand in Condition Expressions and Filter +// Expressions. +// +// Example: +// +// // Use Size() to create a condition expression +// condition := expression.Name("foo").Size().Equal(expression.Value(10)) +// +// Expression Equivalent: +// +// expression.Name("aName").Size() +// "size (aName)" +func (nb NameBuilder) Size() SizeBuilder { + return SizeBuilder{ + nameBuilder: nb, + } +} + +// Size creates a SizeBuilder representing the size of the item attribute +// specified by the argument NameBuilder. Size() is only valid for certain types +// of item attributes. For documentation, +// see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html +// SizeBuilder is only a valid operand in Condition Expressions and Filter +// Expressions. +// +// Example: +// +// // Use Size() to create a condition expression +// condition := expression.Size(expression.Name("foo")).Equal(expression.Value(10)) +// +// Expression Equivalent: +// +// expression.Size(expression.Name("aName")) +// "size (aName)" +func Size(nameBuilder NameBuilder) SizeBuilder { + return nameBuilder.Size() +} + +// Key creates a KeyBuilder. The argument should represent the desired partition +// key or sort key value. KeyBuilders should only be used to specify +// relationships for Key Condition Expressions. When referring to the partition +// key or sort key in any other Expression, use Name(). +// +// Example: +// +// // Use Key() to create a key condition expression +// keyCondition := expression.Key("foo").Equal(expression.Value("bar")) +func Key(key string) KeyBuilder { + return KeyBuilder{ + key: key, + } +} + +// Plus creates a SetValueBuilder to be used in as an argument to Set(). The +// arguments can either be NameBuilders or ValueBuilders. Plus() only supports +// DynamoDB Number types, so the ValueBuilder must be a Number and the +// NameBuilder must specify an item attribute of type Number. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement +// +// Example: +// +// // Use Plus() to set the value of the item attribute "someName" to 5 + 10 +// update, err := expression.Set(expression.Name("someName"), expression.Plus(expression.Value(5), expression.Value(10))) +// +// Expression Equivalent: +// +// expression.Plus(expression.Value(5), expression.Value(10)) +// // let :five and :ten be ExpressionAttributeValues for the values 5 and +// // 10 respectively. +// ":five + :ten" +func Plus(leftOperand, rightOperand OperandBuilder) SetValueBuilder { + return SetValueBuilder{ + leftOperand: leftOperand, + rightOperand: rightOperand, + mode: plusValueMode, + } +} + +// Plus creates a SetValueBuilder to be used in as an argument to Set(). The +// arguments can either be NameBuilders or ValueBuilders. Plus() only supports +// DynamoDB Number types, so the ValueBuilder must be a Number and the +// NameBuilder must specify an item attribute of type Number. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement +// +// Example: +// +// // Use Plus() to set the value of the item attribute "someName" to the +// // numeric value of item attribute "aName" incremented by 10 +// update, err := expression.Set(expression.Name("someName"), expression.Name("aName").Plus(expression.Value(10))) +// +// Expression Equivalent: +// +// expression.Name("aName").Plus(expression.Value(10)) +// // let :ten be ExpressionAttributeValues representing the value 10 +// "aName + :ten" +func (nb NameBuilder) Plus(rightOperand OperandBuilder) SetValueBuilder { + return Plus(nb, rightOperand) +} + +// Plus creates a SetValueBuilder to be used in as an argument to Set(). The +// arguments can either be NameBuilders or ValueBuilders. Plus() only supports +// DynamoDB Number types, so the ValueBuilder must be a Number and the +// NameBuilder must specify an item attribute of type Number. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement +// +// Example: +// +// // Use Plus() to set the value of the item attribute "someName" to 5 + 10 +// update, err := expression.Set(expression.Name("someName"), expression.Value(5).Plus(expression.Value(10))) +// +// Expression Equivalent: +// +// expression.Value(5).Plus(expression.Value(10)) +// // let :five and :ten be ExpressionAttributeValues representing the value +// // 5 and 10 respectively +// ":five + :ten" +func (vb ValueBuilder) Plus(rightOperand OperandBuilder) SetValueBuilder { + return Plus(vb, rightOperand) +} + +// Minus creates a SetValueBuilder to be used in as an argument to Set(). The +// arguments can either be NameBuilders or ValueBuilders. Minus() only supports +// DynamoDB Number types, so the ValueBuilder must be a Number and the +// NameBuilder must specify an item attribute of type Number. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement +// +// Example: +// +// // Use Minus() to set the value of item attribute "someName" to 5 - 10 +// update, err := expression.Set(expression.Name("someName"), expression.Minus(expression.Value(5), expression.Value(10))) +// +// Expression Equivalent: +// +// expression.Minus(expression.Value(5), expression.Value(10)) +// // let :five and :ten be ExpressionAttributeValues for the values 5 and +// // 10 respectively. +// ":five - :ten" +func Minus(leftOperand, rightOperand OperandBuilder) SetValueBuilder { + return SetValueBuilder{ + leftOperand: leftOperand, + rightOperand: rightOperand, + mode: minusValueMode, + } +} + +// Minus creates a SetValueBuilder to be used in as an argument to Set(). The +// arguments can either be NameBuilders or ValueBuilders. Minus() only supports +// DynamoDB Number types, so the ValueBuilder must be a Number and the +// NameBuilder must specify an item attribute of type Number. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement +// +// Example: +// +// // Use Minus() to set the value of item attribute "someName" to the +// // numeric value of "aName" decremented by 10 +// update, err := expression.Set(expression.Name("someName"), expression.Name("aName").Minus(expression.Value(10))) +// +// Expression Equivalent: +// +// expression.Name("aName").Minus(expression.Value(10))) +// // let :ten be ExpressionAttributeValues represent the value 10 +// "aName - :ten" +func (nb NameBuilder) Minus(rightOperand OperandBuilder) SetValueBuilder { + return Minus(nb, rightOperand) +} + +// Minus creates a SetValueBuilder to be used in as an argument to Set(). The +// arguments can either be NameBuilders or ValueBuilders. Minus() only supports +// DynamoDB Number types, so the ValueBuilder must be a Number and the +// NameBuilder must specify an item attribute of type Number. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement +// +// Example: +// +// // Use Minus() to set the value of item attribute "someName" to 5 - 10 +// update, err := expression.Set(expression.Name("someName"), expression.Value(5).Minus(expression.Value(10))) +// +// Expression Equivalent: +// +// expression.Value(5).Minus(expression.Value(10)) +// // let :five and :ten be ExpressionAttributeValues for the values 5 and +// // 10 respectively. +// ":five - :ten" +func (vb ValueBuilder) Minus(rightOperand OperandBuilder) SetValueBuilder { + return Minus(vb, rightOperand) +} + +// ListAppend creates a SetValueBuilder to be used in as an argument to Set(). +// The arguments can either be NameBuilders or ValueBuilders. ListAppend() only +// supports DynamoDB List types, so the ValueBuilder must be a List and the +// NameBuilder must specify an item attribute of type List. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.UpdatingListElements +// +// Example: +// +// // Use ListAppend() to set item attribute "someName" to the item +// // attribute "nameOfList" with "some" and "list" appended to it +// update, err := expression.Set(expression.Name("someName"), expression.ListAppend(expression.Name("nameOfList"), expression.Value([]string{"some", "list"}))) +// +// Expression Equivalent: +// +// expression.ListAppend(expression.Name("nameOfList"), expression.Value([]string{"some", "list"}) +// // let :list be a ExpressionAttributeValue representing the list +// // containing "some" and "list". +// "list_append (nameOfList, :list)" +func ListAppend(leftOperand, rightOperand OperandBuilder) SetValueBuilder { + return SetValueBuilder{ + leftOperand: leftOperand, + rightOperand: rightOperand, + mode: listAppendValueMode, + } +} + +// ListAppend creates a SetValueBuilder to be used in as an argument to Set(). +// The arguments can either be NameBuilders or ValueBuilders. ListAppend() only +// supports DynamoDB List types, so the ValueBuilder must be a List and the +// NameBuilder must specify an item attribute of type List. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.UpdatingListElements +// +// Example: +// +// // Use ListAppend() to set item attribute "someName" to the item +// // attribute "nameOfList" with "some" and "list" appended to it +// update, err := expression.Set(expression.Name("someName"), expression.Name("nameOfList").ListAppend(expression.Value([]string{"some", "list"}))) +// +// Expression Equivalent: +// +// expression.Name("nameOfList").ListAppend(expression.Value([]string{"some", "list"}) +// // let :list be a ExpressionAttributeValue representing the list +// // containing "some" and "list". +// "list_append (nameOfList, :list)" +func (nb NameBuilder) ListAppend(rightOperand OperandBuilder) SetValueBuilder { + return ListAppend(nb, rightOperand) +} + +// ListAppend creates a SetValueBuilder to be used in as an argument to Set(). +// The arguments can either be NameBuilders or ValueBuilders. ListAppend() only +// supports DynamoDB List types, so the ValueBuilder must be a List and the +// NameBuilder must specify an item attribute of type List. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.UpdatingListElements +// +// Example: +// +// // Use ListAppend() to set item attribute "someName" to a string list +// // equal to {"a", "list", "some", "list"} +// update, err := expression.Set(expression.Name("someName"), expression.Value([]string{"a", "list"}).ListAppend(expression.Value([]string{"some", "list"}))) +// +// Expression Equivalent: +// +// expression.Name([]string{"a", "list"}).ListAppend(expression.Value([]string{"some", "list"}) +// // let :list1 and :list2 be a ExpressionAttributeValue representing the +// // list {"a", "list"} and {"some", "list"} respectively +// "list_append (:list1, :list2)" +func (vb ValueBuilder) ListAppend(rightOperand OperandBuilder) SetValueBuilder { + return ListAppend(vb, rightOperand) +} + +// IfNotExists creates a SetValueBuilder to be used in as an argument to Set(). +// The first argument must be a NameBuilder representing the name where the new +// item attribute is created. The second argument can either be a NameBuilder or +// a ValueBuilder. In the case that it is a NameBuilder, the value of the item +// attribute at the name specified becomes the value of the new item attribute. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.PreventingAttributeOverwrites +// +// Example: +// +// // Use IfNotExists() to set item attribute "someName" to value 5 if +// // "someName" does not exist yet. (Prevents overwrite) +// update, err := expression.Set(expression.Name("someName"), expression.IfNotExists(expression.Name("someName"), expression.Value(5))) +// +// Expression Equivalent: +// +// expression.IfNotExists(expression.Name("someName"), expression.Value(5)) +// // let :five be a ExpressionAttributeValue representing the value 5 +// "if_not_exists (someName, :five)" +func IfNotExists(name NameBuilder, setValue OperandBuilder) SetValueBuilder { + return SetValueBuilder{ + leftOperand: name, + rightOperand: setValue, + mode: ifNotExistsValueMode, + } +} + +// IfNotExists creates a SetValueBuilder to be used in as an argument to Set(). +// The first argument must be a NameBuilder representing the name where the new +// item attribute is created. The second argument can either be a NameBuilder or +// a ValueBuilder. In the case that it is a NameBuilder, the value of the item +// attribute at the name specified becomes the value of the new item attribute. +// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.PreventingAttributeOverwrites +// +// Example: +// +// // Use IfNotExists() to set item attribute "someName" to value 5 if +// // "someName" does not exist yet. (Prevents overwrite) +// update, err := expression.Set(expression.Name("someName"), expression.Name("someName").IfNotExists(expression.Value(5))) +// +// Expression Equivalent: +// +// expression.Name("someName").IfNotExists(expression.Value(5)) +// // let :five be a ExpressionAttributeValue representing the value 5 +// "if_not_exists (someName, :five)" +func (nb NameBuilder) IfNotExists(rightOperand OperandBuilder) SetValueBuilder { + return IfNotExists(nb, rightOperand) +} + +// BuildOperand creates an Operand struct which are building blocks to DynamoDB +// Expressions. Package methods and functions can establish relationships +// between operands, representing DynamoDB Expressions. The method +// BuildOperand() is called recursively when the Build() method on the type +// Builder is called. BuildOperand() should never be called externally. +// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved +// words. +// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html +func (nb NameBuilder) BuildOperand() (Operand, error) { + if nb.name == "" { + return Operand{}, newUnsetParameterError("BuildOperand", "NameBuilder") + } + + node := exprNode{ + names: []string{}, + } + + nameSplit := strings.Split(nb.name, ".") + fmtNames := make([]string, 0, len(nameSplit)) + + for _, word := range nameSplit { + var substr string + if word == "" { + return Operand{}, newInvalidParameterError("BuildOperand", "NameBuilder") + } + + if word[len(word)-1] == ']' { + for j, char := range word { + if char == '[' { + substr = word[j:] + word = word[:j] + break + } + } + } + + if word == "" { + return Operand{}, newInvalidParameterError("BuildOperand", "NameBuilder") + } + + // Create a string with special characters that can be substituted later: $p + node.names = append(node.names, word) + fmtNames = append(fmtNames, "$n"+substr) + } + node.fmtExpr = strings.Join(fmtNames, ".") + return Operand{ + exprNode: node, + }, nil +} + +// BuildOperand creates an Operand struct which are building blocks to DynamoDB +// Expressions. Package methods and functions can establish relationships +// between operands, representing DynamoDB Expressions. The method +// BuildOperand() is called recursively when the Build() method on the type +// Builder is called. BuildOperand() should never be called externally. +// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved +// words. +// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html +func (vb ValueBuilder) BuildOperand() (Operand, error) { + var ( + expr types.AttributeValue + err error + ) + + switch v := vb.value.(type) { + case types.AttributeValueMemberS: + expr = &v + case types.AttributeValueMemberN: + expr = &v + case types.AttributeValueMemberB: + expr = &v + case types.AttributeValueMemberSS: + expr = &v + case types.AttributeValueMemberNS: + expr = &v + case types.AttributeValueMemberBS: + expr = &v + case types.AttributeValueMemberM: + expr = &v + case types.AttributeValueMemberL: + expr = &v + case types.AttributeValueMemberNULL: + expr = &v + case types.AttributeValueMemberBOOL: + expr = &v + case types.AttributeValue: + expr = v + default: + expr, err = attributevalue.Marshal(vb.value) + if err != nil { + return Operand{}, newInvalidParameterError("BuildOperand", "ValueBuilder") + } + } + + // Create a string with special characters that can be substituted later: $v + operand := Operand{ + exprNode: exprNode{ + values: []types.AttributeValue{expr}, + fmtExpr: "$v", + }, + } + return operand, nil +} + +// BuildOperand creates an Operand struct which are building blocks to DynamoDB +// Expressions. Package methods and functions can establish relationships +// between operands, representing DynamoDB Expressions. The method +// BuildOperand() is called recursively when the Build() method on the type +// Builder is called. BuildOperand() should never be called externally. +// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved +// words. +// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html +func (sb SizeBuilder) BuildOperand() (Operand, error) { + operand, err := sb.nameBuilder.BuildOperand() + operand.exprNode.fmtExpr = "size (" + operand.exprNode.fmtExpr + ")" + + return operand, err +} + +// BuildOperand creates an Operand struct which are building blocks to DynamoDB +// Expressions. Package methods and functions can establish relationships +// between operands, representing DynamoDB Expressions. The method +// BuildOperand() is called recursively when the Build() method on the type +// Builder is called. BuildOperand() should never be called externally. +// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved +// words. +// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html +func (kb KeyBuilder) BuildOperand() (Operand, error) { + if kb.key == "" { + return Operand{}, newUnsetParameterError("BuildOperand", "KeyBuilder") + } + + ret := Operand{ + exprNode: exprNode{ + names: []string{kb.key}, + fmtExpr: "$n", + }, + } + + return ret, nil +} + +// BuildOperand creates an Operand struct which are building blocks to DynamoDB +// Expressions. Package methods and functions can establish relationships +// between operands, representing DynamoDB Expressions. The method +// BuildOperand() is called recursively when the Build() method on the type +// Builder is called. BuildOperand() should never be called externally. +// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved +// words. +// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html +func (svb SetValueBuilder) BuildOperand() (Operand, error) { + if svb.mode == unsetValue { + return Operand{}, newUnsetParameterError("BuildOperand", "SetValueBuilder") + } + + left, err := svb.leftOperand.BuildOperand() + if err != nil { + return Operand{}, err + } + leftNode := left.exprNode + + right, err := svb.rightOperand.BuildOperand() + if err != nil { + return Operand{}, err + } + rightNode := right.exprNode + + node := exprNode{ + children: []exprNode{leftNode, rightNode}, + } + + switch svb.mode { + case plusValueMode: + node.fmtExpr = "$c + $c" + case minusValueMode: + node.fmtExpr = "$c - $c" + case listAppendValueMode: + node.fmtExpr = "list_append($c, $c)" + case ifNotExistsValueMode: + node.fmtExpr = "if_not_exists($c, $c)" + default: + return Operand{}, fmt.Errorf("build operand error: unsupported mode: %v", svb.mode) + } + + return Operand{ + exprNode: node, + }, nil +} diff --git a/feature/dynamodb/expression/operand_test.go b/feature/dynamodb/expression/operand_test.go new file mode 100644 index 00000000000..da0d6fba52e --- /dev/null +++ b/feature/dynamodb/expression/operand_test.go @@ -0,0 +1,257 @@ +package expression + +import ( + "reflect" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +// opeErrorMode will help with error cases and checking error types +type opeErrorMode string + +const ( + noOperandError opeErrorMode = "" + // unsetName error will occur if an empty string is passed into NameBuilder + unsetName = "unset parameter: NameBuilder" + // invalidName error will occur if a nested name has an empty intermediary + // attribute name (i.e. foo.bar..baz) + invalidName = "invalid parameter: NameBuilder" + // unsetKey error will occur if an empty string is passed into KeyBuilder + unsetKey = "unset parameter: KeyBuilder" +) + +func TestBuildOperand(t *testing.T) { + cases := []struct { + name string + input OperandBuilder + expected exprNode + err opeErrorMode + }{ + { + name: "basic name", + input: Name("foo"), + expected: exprNode{ + names: []string{"foo"}, + fmtExpr: "$n", + }, + }, + { + name: "duplicate name name", + input: Name("foo.foo"), + expected: exprNode{ + names: []string{"foo", "foo"}, + fmtExpr: "$n.$n", + }, + }, + { + name: "basic value", + input: Value(5), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValue as pointer", + input: Value(&types.AttributeValueMemberN{Value: "5"}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberS as value", + input: Value(types.AttributeValueMemberS{Value: "5"}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberN as value", + input: Value(types.AttributeValueMemberN{Value: "5"}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberB as value", + input: Value(types.AttributeValueMemberB{Value: []byte{5}}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberB{Value: []byte{5}}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberSS as value", + input: Value(types.AttributeValueMemberSS{Value: []string{"5", "6"}}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberSS{Value: []string{"5", "6"}}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberNS as value", + input: Value(types.AttributeValueMemberNS{Value: []string{"5", "6"}}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberNS{Value: []string{"5", "6"}}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberBS as value", + input: Value(types.AttributeValueMemberBS{Value: [][]byte{{5}, {6}}}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberBS{Value: [][]byte{{5}, {6}}}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberM as value", + input: Value(types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "bar": &types.AttributeValueMemberS{Value: "baz"}, + }}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "bar": &types.AttributeValueMemberS{Value: "baz"}, + }}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberL as value", + input: Value(types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "5"}, + }}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "5"}, + }}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberNULL as value", + input: Value(types.AttributeValueMemberNULL{Value: true}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberNULL{Value: true}, + }, + fmtExpr: "$v", + }, + }, + { + name: "types.AttributeValueMemberBOOL as value", + input: Value(types.AttributeValueMemberBOOL{Value: true}), + expected: exprNode{ + values: []types.AttributeValue{ + &types.AttributeValueMemberBOOL{Value: true}, + }, + fmtExpr: "$v", + }, + }, + { + name: "nested name", + input: Name("foo.bar"), + expected: exprNode{ + names: []string{"foo", "bar"}, + fmtExpr: "$n.$n", + }, + }, + { + name: "nested name with index", + input: Name("foo.bar[0].baz"), + expected: exprNode{ + names: []string{"foo", "bar", "baz"}, + fmtExpr: "$n.$n[0].$n", + }, + }, + { + name: "basic size", + input: Name("foo").Size(), + expected: exprNode{ + names: []string{"foo"}, + fmtExpr: "size ($n)", + }, + }, + { + name: "key", + input: Key("foo"), + expected: exprNode{ + names: []string{"foo"}, + fmtExpr: "$n", + }, + }, + { + name: "unset key error", + input: Key(""), + expected: exprNode{}, + err: unsetKey, + }, + { + name: "empty name error", + input: Name(""), + expected: exprNode{}, + err: unsetName, + }, + { + name: "invalid name", + input: Name("foo..bar"), + expected: exprNode{}, + err: invalidName, + }, + { + name: "invalid index", + input: Name("[foo]"), + expected: exprNode{}, + err: invalidName, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + operand, err := c.input.BuildOperand() + + if c.err != noOperandError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, operand.exprNode; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} diff --git a/feature/dynamodb/expression/projection.go b/feature/dynamodb/expression/projection.go new file mode 100644 index 00000000000..1eb62002b65 --- /dev/null +++ b/feature/dynamodb/expression/projection.go @@ -0,0 +1,148 @@ +package expression + +import ( + "strings" +) + +// ProjectionBuilder represents Projection Expressions in DynamoDB. +// ProjectionBuilders are the building blocks of Builders. +// More Information at: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html +type ProjectionBuilder struct { + names []NameBuilder +} + +// NamesList returns a ProjectionBuilder representing the list of item +// attribute names specified by the argument NameBuilders. The resulting +// ProjectionBuilder can be used as a part of other ProjectionBuilders or as an +// argument to the WithProjection() method for the Builder struct. +// +// Example: +// +// // projection represents the list of names {"foo", "bar"} +// projection := expression.NamesList(expression.Name("foo"), expression.Name("bar")) +// +// // Used in another Projection Expression +// anotherProjection := expression.AddNames(projection, expression.Name("baz")) +// // Used to make an Builder +// builder := expression.NewBuilder().WithProjection(newProjection) +// +// Expression Equivalent: +// +// expression.NamesList(expression.Name("foo"), expression.Name("bar")) +// "foo, bar" +func NamesList(nameBuilder NameBuilder, namesList ...NameBuilder) ProjectionBuilder { + namesList = append([]NameBuilder{nameBuilder}, namesList...) + return ProjectionBuilder{ + names: namesList, + } +} + +// NamesList returns a ProjectionBuilder representing the list of item +// attribute names specified by the argument NameBuilders. The resulting +// ProjectionBuilder can be used as a part of other ProjectionBuilders or as an +// argument to the WithProjection() method for the Builder struct. +// +// Example: +// +// // projection represents the list of names {"foo", "bar"} +// projection := expression.Name("foo").NamesList(expression.Name("bar")) +// +// // Used in another Projection Expression +// anotherProjection := expression.AddNames(projection, expression.Name("baz")) +// // Used to make an Builder +// builder := expression.NewBuilder().WithProjection(newProjection) +// +// Expression Equivalent: +// +// expression.Name("foo").NamesList(expression.Name("bar")) +// "foo, bar" +func (nb NameBuilder) NamesList(namesList ...NameBuilder) ProjectionBuilder { + return NamesList(nb, namesList...) +} + +// AddNames returns a ProjectionBuilder representing the list of item +// attribute names equivalent to appending all of the argument item attribute +// names to the argument ProjectionBuilder. The resulting ProjectionBuilder can +// be used as a part of other ProjectionBuilders or as an argument to the +// WithProjection() method for the Builder struct. +// +// Example: +// +// // projection represents the list of names {"foo", "bar", "baz", "qux"} +// oldProj := expression.NamesList(expression.Name("foo"), expression.Name("bar")) +// projection := expression.AddNames(oldProj, expression.Name("baz"), expression.Name("qux")) +// +// // Used in another Projection Expression +// anotherProjection := expression.AddNames(projection, expression.Name("quux")) +// // Used to make an Builder +// builder := expression.NewBuilder().WithProjection(newProjection) +// +// Expression Equivalent: +// +// expression.AddNames(expression.NamesList(expression.Name("foo"), expression.Name("bar")), expression.Name("baz"), expression.Name("qux")) +// "foo, bar, baz, qux" +func AddNames(projectionBuilder ProjectionBuilder, namesList ...NameBuilder) ProjectionBuilder { + projectionBuilder.names = append(projectionBuilder.names, namesList...) + return projectionBuilder +} + +// AddNames returns a ProjectionBuilder representing the list of item +// attribute names equivalent to appending all of the argument item attribute +// names to the argument ProjectionBuilder. The resulting ProjectionBuilder can +// be used as a part of other ProjectionBuilders or as an argument to the +// WithProjection() method for the Builder struct. +// +// Example: +// +// // projection represents the list of names {"foo", "bar", "baz", "qux"} +// oldProj := expression.NamesList(expression.Name("foo"), expression.Name("bar")) +// projection := oldProj.AddNames(expression.Name("baz"), expression.Name("qux")) +// +// // Used in another Projection Expression +// anotherProjection := expression.AddNames(projection, expression.Name("quux")) +// // Used to make an Builder +// builder := expression.NewBuilder().WithProjection(newProjection) +// +// Expression Equivalent: +// +// expression.NamesList(expression.Name("foo"), expression.Name("bar")).AddNames(expression.Name("baz"), expression.Name("qux")) +// "foo, bar, baz, qux" +func (pb ProjectionBuilder) AddNames(namesList ...NameBuilder) ProjectionBuilder { + return AddNames(pb, namesList...) +} + +// buildTree builds a tree structure of exprNodes based on the tree +// structure of the input ProjectionBuilder's child NameBuilders. buildTree() +// satisfies the treeBuilder interface so ProjectionBuilder can be a part of +// Builder and Expression struct. +func (pb ProjectionBuilder) buildTree() (exprNode, error) { + if len(pb.names) == 0 { + return exprNode{}, newUnsetParameterError("buildTree", "ProjectionBuilder") + } + + childNodes, err := pb.buildChildNodes() + if err != nil { + return exprNode{}, err + } + ret := exprNode{ + children: childNodes, + } + + ret.fmtExpr = "$c" + strings.Repeat(", $c", len(pb.names)-1) + + return ret, nil +} + +// buildChildNodes creates the list of the child exprNodes. +func (pb ProjectionBuilder) buildChildNodes() ([]exprNode, error) { + childNodes := make([]exprNode, 0, len(pb.names)) + for _, name := range pb.names { + operand, err := name.BuildOperand() + if err != nil { + return []exprNode{}, err + } + childNodes = append(childNodes, operand.exprNode) + } + + return childNodes, nil +} diff --git a/feature/dynamodb/expression/projection_test.go b/feature/dynamodb/expression/projection_test.go new file mode 100644 index 00000000000..4a1e4e152bc --- /dev/null +++ b/feature/dynamodb/expression/projection_test.go @@ -0,0 +1,213 @@ +package expression + +import ( + "reflect" + "strings" + "testing" +) + +// projErrorMode will help with error cases and checking error types +type projErrorMode string + +const ( + noProjError projErrorMode = "" + // invalidProjectionOperand error will occur when an invalid OperandBuilder is + // used as an argument + invalidProjectionOperand = "BuildOperand error" + // unsetProjection error will occur if the argument ProjectionBuilder is unset + unsetProjection = "unset parameter: ProjectionBuilder" +) + +func TestProjectionBuilder(t *testing.T) { + cases := []struct { + name string + input ProjectionBuilder + expectedNode exprNode + err projErrorMode + }{ + { + name: "names list function call", + input: NamesList(Name("foo"), Name("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c, $c", + }, + }, + { + name: "names list method call", + input: Name("foo").NamesList(Name("bar")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c, $c", + }, + }, + { + name: "add name", + input: Name("foo").NamesList(Name("bar")).AddNames(Name("baz"), Name("qux")), + expectedNode: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + { + names: []string{"baz"}, + fmtExpr: "$n", + }, { + names: []string{"qux"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c, $c, $c, $c", + }, + }, + { + name: "invalid operand", + input: NamesList(Name("")), + err: invalidProjectionOperand, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noProjError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestBuildProjection(t *testing.T) { + cases := []struct { + name string + input ProjectionBuilder + expected string + err projErrorMode + }{ + { + name: "build projection 3", + input: NamesList(Name("foo"), Name("bar"), Name("baz")), + expected: "$c, $c, $c", + }, + { + name: "build projection 5", + input: NamesList(Name("foo"), Name("bar"), Name("baz")).AddNames(Name("qux"), Name("quux")), + expected: "$c, $c, $c, $c, $c", + }, + { + name: "empty ProjectionBuilder", + input: ProjectionBuilder{}, + err: unsetProjection, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noProjError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expected, actual.fmtExpr; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestBuildProjectionChildNodes(t *testing.T) { + cases := []struct { + name string + input ProjectionBuilder + expected []exprNode + err projErrorMode + }{ + { + name: "build child nodes", + input: NamesList(Name("foo"), Name("bar"), Name("baz")), + expected: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + { + names: []string{"baz"}, + fmtExpr: "$n", + }, + }, + }, + { + name: "operand error", + input: NamesList(Name("")), + err: invalidProjectionOperand, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noProjError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + if e, a := c.expected, actual.children; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} diff --git a/feature/dynamodb/expression/update.go b/feature/dynamodb/expression/update.go new file mode 100644 index 00000000000..b82d3d4f523 --- /dev/null +++ b/feature/dynamodb/expression/update.go @@ -0,0 +1,391 @@ +package expression + +import ( + "fmt" + "sort" + "strings" +) + +// operationMode specifies the types of update operations that the +// updateBuilder is going to represent. The const is in a string to use the +// const value as a map key and as a string when creating the formatted +// expression for the exprNodes. +type operationMode string + +const ( + setOperation operationMode = "SET" + removeOperation = "REMOVE" + addOperation = "ADD" + deleteOperation = "DELETE" +) + +// Implementing the Sort interface +type modeList []operationMode + +func (ml modeList) Len() int { + return len(ml) +} + +func (ml modeList) Less(i, j int) bool { + return string(ml[i]) < string(ml[j]) +} + +func (ml modeList) Swap(i, j int) { + ml[i], ml[j] = ml[j], ml[i] +} + +// UpdateBuilder represents Update Expressions in DynamoDB. UpdateBuilders +// are the building blocks of the Builder struct. Note that there are different +// update operations in DynamoDB and an UpdateBuilder can represent multiple +// update operations. +// More Information at: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html +type UpdateBuilder struct { + operationList map[operationMode][]operationBuilder +} + +// operationBuilder represents specific update actions (SET, REMOVE, ADD, +// DELETE). The mode specifies what type of update action the +// operationBuilder represents. +type operationBuilder struct { + name NameBuilder + value OperandBuilder + mode operationMode +} + +// buildOperation builds an exprNode from an operationBuilder. buildOperation +// is called recursively by buildTree in order to create a tree structure +// of exprNodes representing the parent/child relationships between +// UpdateBuilders and operationBuilders. +func (ob operationBuilder) buildOperation() (exprNode, error) { + pathChild, err := ob.name.BuildOperand() + if err != nil { + return exprNode{}, err + } + + node := exprNode{ + children: []exprNode{pathChild.exprNode}, + fmtExpr: "$c", + } + + if ob.mode == removeOperation { + return node, nil + } + + valueChild, err := ob.value.BuildOperand() + if err != nil { + return exprNode{}, err + } + node.children = append(node.children, valueChild.exprNode) + + switch ob.mode { + case setOperation: + node.fmtExpr += " = $c" + case addOperation, deleteOperation: + node.fmtExpr += " $c" + default: + return exprNode{}, fmt.Errorf("build update error: build operation error: unsupported mode: %v", ob.mode) + } + + return node, nil +} + +// Delete returns an UpdateBuilder representing one Delete operation for +// DynamoDB Update Expressions. The argument name should specify the item +// attribute and the argument value should specify the value to be deleted. The +// resulting UpdateBuilder can be used as an argument to the WithUpdate() method +// for the Builder struct. +// +// Example: +// +// // update represents the delete operation to delete the string value +// // "subsetToDelete" from the item attribute "pathToList" +// update := expression.Delete(expression.Name("pathToList"), expression.Value("subsetToDelete")) +// +// // Adding more update methods +// anotherUpdate := update.Remove(expression.Name("someName")) +// // Creating a Builder +// builder := Update(update) +// +// Expression Equivalent: +// +// expression.Delete(expression.Name("pathToList"), expression.Value("subsetToDelete")) +// // let :del be an ExpressionAttributeValue representing the value +// // "subsetToDelete" +// "DELETE pathToList :del" +func Delete(name NameBuilder, value ValueBuilder) UpdateBuilder { + emptyUpdateBuilder := UpdateBuilder{} + return emptyUpdateBuilder.Delete(name, value) +} + +// Delete adds a Delete operation to the argument UpdateBuilder. The +// argument name should specify the item attribute and the argument value should +// specify the value to be deleted. The resulting UpdateBuilder can be used as +// an argument to the WithUpdate() method for the Builder struct. +// +// Example: +// +// // Let update represent an already existing update expression. Delete() +// // adds the operation to delete the value "subsetToDelete" from the item +// // attribute "pathToList" +// update := update.Delete(expression.Name("pathToList"), expression.Value("subsetToDelete")) +// +// // Adding more update methods +// anotherUpdate := update.Remove(expression.Name("someName")) +// // Creating a Builder +// builder := Update(update) +// +// Expression Equivalent: +// +// Delete(expression.Name("pathToList"), expression.Value("subsetToDelete")) +// // let :del be an ExpressionAttributeValue representing the value +// // "subsetToDelete" +// "DELETE pathToList :del" +func (ub UpdateBuilder) Delete(name NameBuilder, value ValueBuilder) UpdateBuilder { + if ub.operationList == nil { + ub.operationList = map[operationMode][]operationBuilder{} + } + ub.operationList[deleteOperation] = append(ub.operationList[deleteOperation], operationBuilder{ + name: name, + value: value, + mode: deleteOperation, + }) + return ub +} + +// Add returns an UpdateBuilder representing the Add operation for DynamoDB +// Update Expressions. The argument name should specify the item attribute and +// the argument value should specify the value to be added. The resulting +// UpdateBuilder can be used as an argument to the WithUpdate() method for the +// Builder struct. +// +// Example: +// +// // update represents the add operation to add the value 5 to the item +// // attribute "aPath" +// update := expression.Add(expression.Name("aPath"), expression.Value(5)) +// +// // Adding more update methods +// anotherUpdate := update.Remove(expression.Name("someName")) +// // Creating a Builder +// builder := Update(update) +// +// Expression Equivalent: +// +// expression.Add(expression.Name("aPath"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "ADD aPath :5" +func Add(name NameBuilder, value ValueBuilder) UpdateBuilder { + emptyUpdateBuilder := UpdateBuilder{} + return emptyUpdateBuilder.Add(name, value) +} + +// Add adds an Add operation to the argument UpdateBuilder. The argument +// name should specify the item attribute and the argument value should specify +// the value to be added. The resulting UpdateBuilder can be used as an argument +// to the WithUpdate() method for the Builder struct. +// +// Example: +// +// // Let update represent an already existing update expression. Add() adds +// // the operation to add the value 5 to the item attribute "aPath" +// update := update.Add(expression.Name("aPath"), expression.Value(5)) +// +// // Adding more update methods +// anotherUpdate := update.Remove(expression.Name("someName")) +// // Creating a Builder +// builder := Update(update) +// +// Expression Equivalent: +// +// Add(expression.Name("aPath"), expression.Value(5)) +// // Let :five be an ExpressionAttributeValue representing the value 5 +// "ADD aPath :5" +func (ub UpdateBuilder) Add(name NameBuilder, value ValueBuilder) UpdateBuilder { + if ub.operationList == nil { + ub.operationList = map[operationMode][]operationBuilder{} + } + ub.operationList[addOperation] = append(ub.operationList[addOperation], operationBuilder{ + name: name, + value: value, + mode: addOperation, + }) + return ub +} + +// Remove returns an UpdateBuilder representing the Remove operation for +// DynamoDB Update Expressions. The argument name should specify the item +// attribute to delete. The resulting UpdateBuilder can be used as an argument +// to the WithUpdate() method for the Builder struct. +// +// Example: +// +// // update represents the remove operation to remove the item attribute +// // "itemToRemove" +// update := expression.Remove(expression.Name("itemToRemove")) +// +// // Adding more update methods +// anotherUpdate := update.Remove(expression.Name("someName")) +// // Creating a Builder +// builder := Update(update) +// +// Expression Equivalent: +// +// expression.Remove(expression.Name("itemToRemove")) +// "REMOVE itemToRemove" +func Remove(name NameBuilder) UpdateBuilder { + emptyUpdateBuilder := UpdateBuilder{} + return emptyUpdateBuilder.Remove(name) +} + +// Remove adds a Remove operation to the argument UpdateBuilder. The +// argument name should specify the item attribute to delete. The resulting +// UpdateBuilder can be used as an argument to the WithUpdate() method for the +// Builder struct. +// +// Example: +// +// // Let update represent an already existing update expression. Remove() +// // adds the operation to remove the item attribute "itemToRemove" +// update := update.Remove(expression.Name("itemToRemove")) +// +// // Adding more update methods +// anotherUpdate := update.Remove(expression.Name("someName")) +// // Creating a Builder +// builder := Update(update) +// +// Expression Equivalent: +// +// Remove(expression.Name("itemToRemove")) +// "REMOVE itemToRemove" +func (ub UpdateBuilder) Remove(name NameBuilder) UpdateBuilder { + if ub.operationList == nil { + ub.operationList = map[operationMode][]operationBuilder{} + } + ub.operationList[removeOperation] = append(ub.operationList[removeOperation], operationBuilder{ + name: name, + mode: removeOperation, + }) + return ub +} + +// Set returns an UpdateBuilder representing the Set operation for DynamoDB +// Update Expressions. The argument name should specify the item attribute to +// modify. The argument OperandBuilder should specify the value to modify the +// the item attribute to. The resulting UpdateBuilder can be used as an argument +// to the WithUpdate() method for the Builder struct. +// +// Example: +// +// // update represents the set operation to set the item attribute +// // "itemToSet" to the value "setValue" if the item attribute does not +// // exist yet. (conditional write) +// update := expression.Set(expression.Name("itemToSet"), expression.IfNotExists(expression.Name("itemToSet"), expression.Value("setValue"))) +// +// // Adding more update methods +// anotherUpdate := update.Remove(expression.Name("someName")) +// // Creating a Builder +// builder := Update(update) +// +// Expression Equivalent: +// +// expression.Set(expression.Name("itemToSet"), expression.IfNotExists(expression.Name("itemToSet"), expression.Value("setValue"))) +// // Let :val be an ExpressionAttributeValue representing the value +// // "setValue" +// "SET itemToSet = :val" +func Set(name NameBuilder, operandBuilder OperandBuilder) UpdateBuilder { + emptyUpdateBuilder := UpdateBuilder{} + return emptyUpdateBuilder.Set(name, operandBuilder) +} + +// Set adds a Set operation to the argument UpdateBuilder. The argument name +// should specify the item attribute to modify. The argument OperandBuilder +// should specify the value to modify the the item attribute to. The resulting +// UpdateBuilder can be used as an argument to the WithUpdate() method for the +// Builder struct. +// +// Example: +// +// // Let update represent an already existing update expression. Set() adds +// // the operation to to set the item attribute "itemToSet" to the value +// // "setValue" if the item attribute does not exist yet. (conditional +// // write) +// update := update.Set(expression.Name("itemToSet"), expression.IfNotExists(expression.Name("itemToSet"), expression.Value("setValue"))) +// +// // Adding more update methods +// anotherUpdate := update.Remove(expression.Name("someName")) +// // Creating a Builder +// builder := Update(update) +// +// Expression Equivalent: +// +// Set(expression.Name("itemToSet"), expression.IfNotExists(expression.Name("itemToSet"), expression.Value("setValue"))) +// // Let :val be an ExpressionAttributeValue representing the value +// // "setValue" +// "SET itemToSet = :val" +func (ub UpdateBuilder) Set(name NameBuilder, operandBuilder OperandBuilder) UpdateBuilder { + if ub.operationList == nil { + ub.operationList = map[operationMode][]operationBuilder{} + } + ub.operationList[setOperation] = append(ub.operationList[setOperation], operationBuilder{ + name: name, + value: operandBuilder, + mode: setOperation, + }) + return ub +} + +// buildTree builds a tree structure of exprNodes based on the tree +// structure of the input UpdateBuilder's child UpdateBuilders/Operands. +// buildTree() satisfies the TreeBuilder interface so ProjectionBuilder can be a +// part of Expression struct. +func (ub UpdateBuilder) buildTree() (exprNode, error) { + if ub.operationList == nil { + return exprNode{}, newUnsetParameterError("buildTree", "UpdateBuilder") + } + ret := exprNode{ + children: []exprNode{}, + } + + modes := modeList{} + + for mode := range ub.operationList { + modes = append(modes, mode) + } + + sort.Sort(modes) + + for _, key := range modes { + ret.fmtExpr += string(key) + " $c\n" + + childNode, err := buildChildNodes(ub.operationList[key]) + if err != nil { + return exprNode{}, err + } + + ret.children = append(ret.children, childNode) + } + + return ret, nil +} + +// buildChildNodes creates the list of the child exprNodes. +func buildChildNodes(operationBuilderList []operationBuilder) (exprNode, error) { + if len(operationBuilderList) == 0 { + return exprNode{}, fmt.Errorf("buildChildNodes error: operationBuilder list is empty") + } + + node := exprNode{ + children: make([]exprNode, 0, len(operationBuilderList)), + fmtExpr: "$c" + strings.Repeat(", $c", len(operationBuilderList)-1), + } + + for _, val := range operationBuilderList { + valNode, err := val.buildOperation() + if err != nil { + return exprNode{}, err + } + node.children = append(node.children, valNode) + } + + return node, nil +} diff --git a/feature/dynamodb/expression/update_test.go b/feature/dynamodb/expression/update_test.go new file mode 100644 index 00000000000..917e63d2276 --- /dev/null +++ b/feature/dynamodb/expression/update_test.go @@ -0,0 +1,730 @@ +package expression + +import ( + "reflect" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +// updateErrorMode will help with error cases and checking error types +type updateErrorMode string + +const ( + noUpdateError updateErrorMode = "" + invalidUpdateOperand = "BuildOperand error" + unsetSetValue = "unset parameter: SetValueBuilder" + unsetUpdate = "unset parameter: UpdateBuilder" + emptyOperationBuilderList = "operationBuilder list is empty" +) + +func TestBuildOperation(t *testing.T) { + cases := []struct { + name string + input operationBuilder + expected exprNode + err updateErrorMode + }{ + { + name: "set operation", + input: operationBuilder{ + name: Name("foo"), + value: Value(5), + mode: setOperation, + }, + expected: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + { + name: "add operation", + input: operationBuilder{ + name: Name("foo"), + value: Value(5), + mode: addOperation, + }, + expected: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c $c", + }, + }, + { + name: "remove operation", + input: operationBuilder{ + name: Name("foo"), + mode: removeOperation, + }, + expected: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c", + }, + }, + { + name: "invalid operand", + input: operationBuilder{ + name: Name(""), + mode: removeOperation, + }, + err: invalidUpdateOperand, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildOperation() + if c.err != noUpdateError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestUpdateTree(t *testing.T) { + cases := []struct { + name string + input UpdateBuilder + expectedNode exprNode + err updateErrorMode + }{ + { + name: "set update", + input: Set(Name("foo"), Value(5)), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + fmtExpr: "$c", + }, + }, + fmtExpr: "SET $c\n", + }, + }, + { + name: "remove update", + input: Remove(Name("foo")), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c", + }, + }, + fmtExpr: "$c", + }, + }, + fmtExpr: "REMOVE $c\n", + }, + }, + { + name: "add update", + input: Add(Name("foo"), Value(5)), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c $c", + }, + }, + fmtExpr: "$c", + }, + }, + fmtExpr: "ADD $c\n", + }, + }, + { + name: "delete update", + input: Delete(Name("foo"), Value(5)), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c $c", + }, + }, + fmtExpr: "$c", + }, + }, + fmtExpr: "DELETE $c\n", + }, + }, + { + name: "multiple sets", + input: Set(Name("foo"), Value(5)).Set(Name("bar"), Value(6)).Set(Name("baz"), Name("qux")), + expectedNode: exprNode{ + fmtExpr: "SET $c\n", + children: []exprNode{ + { + fmtExpr: "$c, $c, $c", + children: []exprNode{ + { + fmtExpr: "$c = $c", + children: []exprNode{ + { + fmtExpr: "$n", + names: []string{"foo"}, + }, + { + fmtExpr: "$v", + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + }, + }, + }, + { + fmtExpr: "$c = $c", + children: []exprNode{ + { + fmtExpr: "$n", + names: []string{"bar"}, + }, + { + fmtExpr: "$v", + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "6"}, + }, + }, + }, + }, + { + fmtExpr: "$c = $c", + children: []exprNode{ + { + fmtExpr: "$n", + names: []string{"baz"}, + }, + { + fmtExpr: "$n", + names: []string{"qux"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "compound update", + input: Add(Name("foo"), Value(5)).Set(Name("foo"), Value(5)).Delete(Name("foo"), Value(5)).Remove(Name("foo")), + expectedNode: exprNode{ + children: []exprNode{ + { + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c $c", + }, + }, + fmtExpr: "$c", + }, + { + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c $c", + }, + }, + fmtExpr: "$c", + }, + { + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c", + }, + }, + fmtExpr: "$c", + }, + { + children: []exprNode{ + { + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + }, + fmtExpr: "$c = $c", + }, + }, + fmtExpr: "$c", + }, + }, + fmtExpr: "ADD $c\nDELETE $c\nREMOVE $c\nSET $c\n", + }, + }, + { + name: "empty UpdateBuilder", + input: UpdateBuilder{}, + err: unsetUpdate, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.buildTree() + if c.err != noUpdateError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestSetValueBuilder(t *testing.T) { + cases := []struct { + name string + input SetValueBuilder + expected exprNode + err updateErrorMode + }{ + { + name: "name plus name", + input: Name("foo").Plus(Name("bar")), + expected: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c + $c", + }, + }, + { + name: "name minus name", + input: Name("foo").Minus(Name("bar")), + expected: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c - $c", + }, + }, + { + name: "list append name and name", + input: Name("foo").ListAppend(Name("bar")), + expected: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "list_append($c, $c)", + }, + }, + { + name: "if not exists name and name", + input: Name("foo").IfNotExists(Name("bar")), + expected: exprNode{ + children: []exprNode{ + { + names: []string{"foo"}, + fmtExpr: "$n", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "if_not_exists($c, $c)", + }, + }, + { + name: "value plus name", + input: Value(5).Plus(Name("bar")), + expected: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c + $c", + }, + }, + { + name: "value minus name", + input: Value(5).Minus(Name("bar")), + expected: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + fmtExpr: "$v", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "$c - $c", + }, + }, + { + name: "list append list and name", + input: Value([]int{1, 2, 3}).ListAppend(Name("bar")), + expected: exprNode{ + children: []exprNode{ + { + values: []types.AttributeValue{ + &types.AttributeValueMemberL{ + Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + &types.AttributeValueMemberN{Value: "3"}, + }, + }, + }, + fmtExpr: "$v", + }, + { + names: []string{"bar"}, + fmtExpr: "$n", + }, + }, + fmtExpr: "list_append($c, $c)", + }, + }, + { + name: "unset SetValueBuilder", + input: SetValueBuilder{}, + err: unsetSetValue, + }, + { + name: "invalid operand error", + input: Name("").Plus(Name("foo")), + err: invalidUpdateOperand, + }, + { + name: "invalid operand error", + input: Name("foo").Plus(Name("")), + err: invalidUpdateOperand, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := c.input.BuildOperand() + if c.err != noUpdateError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, actual.exprNode; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +} + +func TestUpdateBuildChildNodes(t *testing.T) { + cases := []struct { + name string + input []operationBuilder + expected exprNode + err updateErrorMode + }{ + { + name: "set operand builder", + input: []operationBuilder{ + { + mode: setOperation, + name: NameBuilder{ + name: "foo", + }, + value: ValueBuilder{ + value: 5, + }, + }, + { + mode: setOperation, + name: NameBuilder{ + name: "bar", + }, + value: ValueBuilder{ + value: 6, + }, + }, + { + mode: setOperation, + name: NameBuilder{ + name: "baz", + }, + value: ValueBuilder{ + value: 7, + }, + }, + { + mode: setOperation, + name: NameBuilder{ + name: "qux", + }, + value: ValueBuilder{ + value: 8, + }, + }, + }, + expected: exprNode{ + fmtExpr: "$c, $c, $c, $c", + children: []exprNode{ + { + fmtExpr: "$c = $c", + children: []exprNode{ + { + fmtExpr: "$n", + names: []string{"foo"}, + }, + { + fmtExpr: "$v", + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "5"}, + }, + }, + }, + }, + { + fmtExpr: "$c = $c", + children: []exprNode{ + { + fmtExpr: "$n", + names: []string{"bar"}, + }, + { + fmtExpr: "$v", + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "6"}, + }, + }, + }, + }, + { + fmtExpr: "$c = $c", + children: []exprNode{ + { + fmtExpr: "$n", + names: []string{"baz"}, + }, + { + fmtExpr: "$v", + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "7"}, + }, + }, + }, + }, + { + fmtExpr: "$c = $c", + children: []exprNode{ + { + fmtExpr: "$n", + names: []string{"qux"}, + }, + { + fmtExpr: "$v", + values: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "8"}, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty operationBuilder list", + input: []operationBuilder{}, + err: emptyOperationBuilderList, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, err := buildChildNodes(c.input) + if c.err != noUpdateError { + if err == nil { + t.Errorf("expect error %q, got no error", c.err) + } else { + if e, a := string(c.err), err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %q error message to be in %q", e, a) + } + } + } else { + if err != nil { + t.Errorf("expect no error, got unexpected Error %q", err) + } + + if e, a := c.expected, actual; !reflect.DeepEqual(a, e) { + t.Errorf("expect %v, got %v", e, a) + } + } + }) + } +}