Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

DDB Mapper filter expressions (codegen components) #1424

Merged
merged 3 commits into from
Oct 3, 2024

Conversation

ianbotsf
Copy link
Contributor

Issue #

Related to #472

Description of changes

The other half of #1401, this change adds codegen support for Filter expressions and runtime/codegen support for Key Condition expressions required by Query operations.

Key areas of change:

  • MemberCodegenBehavior has been expanded with new behaviors for expressions and behavior derivation has been spruced up to a new rules-based system
  • OperationRenderer now generates the logic for instantiating an expression visitor, applying it to the various operation expressions, and setting the expression literal and attributes on the low-level request
  • BuilderRenderer now generates DSL methods for annotated types
  • QueryTest is now fixed up to remove the last of the hacks necessary to run queries

Key additions:

  • KeyFilter and SortKeyFilter are new types to support Key Condition expressions in Query operations. KeyFilter supports partition key equality comparisons for literal values only. SortKeyFilter contains a subset of Filter methods suitable for use on sort keys (again, for literal values only).
  • ScanTest now exists for the Scan operation

Aside from that, a lot of small moves, renames, and refactors to deal with the new changes.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@ianbotsf ianbotsf requested a review from a team as a code owner September 29, 2024 22:57
Copy link

A new generated diff is ready to view.

  • No codegen difference in the AWS SDK

Comment on lines +26 to +27
public val Base: String = "${Hl.Base}.expressions"
public val Internal: String = "$Base.internal"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is there a difference here between Hl.Base and Base?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes there is, Base refers to the value defined on L26. Hl.Base is defined on L13

Comment on lines +427 to +428
@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("isInRangeUShort")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is the suppression still needed even though we change the JVM name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately yes. Without the suppression, the compiler warns that @JvmName is inapplicable to this declaration. Without the @JvmName, the compiler warns that a platform type conflict will occur. This is a known compiler bug which should be fixed in an upcoming release.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like that was fixed in 2.0.20 which is now released, we should just have to upgrade


MemberCodegenBehavior.ListMapAll -> {
val llListType = llMember.type as? TypeRef ?: error("`ListMapAll` member is required to be a TypeRef")
val hlListType = llListType.copy(genericArgs = listOf(TypeVar("T")), nullable = llListType.nullable)
val hlListType = llListType.copy(genericArgs = listOf(TypeVar("T")))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Just double checking that this doesn't require nullable anymore

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It never did require setting nullable. This is a copy call which carries forward all existing values unless overridden. The previous code setting nullable = llListType.nullable is redundant because that's already the value of nullable. Not sure why I wrote it that way originally but I noticed it was unnecessary when extracting nullable out to a separate val on L21.

Rule("keyConditionExpression", Types.Kotlin.String, ExpressionLiteral(KeyCondition)),
Rule("filterExpression", Types.Kotlin.String, ExpressionLiteral(Filter)),

// TODO add support for remaining expression types
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

// TODO add support for remaining expression types
// Expression types

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression types which follow the TODO are still part of the logical group // Expression literals started above.

write("#Lvar #L: #T = null", ctx.attributes.visibility, member.name, member.type.nullable())
// TODO add DSL methods for structure members

ctx.info("For member $builderName.${member.name} dslInfo = $dslInfo")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Is this a stray log or intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was helpful for debugging but can probably be removed now.

Comment on lines +26 to +27
public val Base: String = "${Hl.Base}.expressions"
public val Internal: String = "$Base.internal"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes there is, Base refers to the value defined on L26. Hl.Base is defined on L13


MemberCodegenBehavior.ListMapAll -> {
val llListType = llMember.type as? TypeRef ?: error("`ListMapAll` member is required to be a TypeRef")
val hlListType = llListType.copy(genericArgs = listOf(TypeVar("T")), nullable = llListType.nullable)
val hlListType = llListType.copy(genericArgs = listOf(TypeVar("T")))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

}
private val requestMembers = operation
.request
.also { ctx.info("For type ${it.lowLevelName}:") }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "For request type", "For response type"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I omitted that because the type name already contains "request"/"response". (These are the low-level operation input/output types like ScanRequest and QueryResponse.)

members(MemberCodegenBehavior.Hoist) { write("#L: #T, ", name, type) }
write("schema: #T,", MapperTypes.Items.itemSchema("T"))
requestMembers(MemberCodegenBehavior.Hoist) { write("#L: #T, ", name, type) }
write("schema: #T", MapperTypes.Items.itemSchema("T"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you intend to drop the trailing comma here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. No I shouldn't have dropped it since this parameter declaration is on its own line.

* { sortKey eq 42 }
* ```
*
* This example creates an expression which checks whether an attribute named `foo` is equal to the value `42`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checks whether an attribute named foo

foo is not defined in this example, I'm assuming it's the name of the sortKey but can it be made clearer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another copy-paste error. I'll rewrite the explanation and check the rest of the block for similar mistakes.

Comment on lines 56 to 59
* ```kotlin
* // Checks whether the value of the sort key is between 40 and 60 (inclusive)
* sortKey isIn 40..60
* ```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: provide an example of isBetween too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still missing this example, but I think it can be left out based on our discussion in https://github.com/awslabs/aws-sdk-kotlin/pull/1424/files/7a1d42de3ce73bbe512ad28acca861ef9e2150b2#r1784632503

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think it is there in the latest revision. (SortKeyFilter.kt:L60-63)

I initially couldn't find it either when I searched the files on GH with my browser. But I forgot that GH hides large diffs and I had to load SortKeyFilter.kt before I found it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah I see it now. I did explicitly load the file but maybe was looking at a previous commit. Sorry about that!

Comment on lines +363 to +376
/**
* Creates a range expression for verifying the sort key is between two other expressions
* @param min The lower bound expression
* @param max The upper bound expression (inclusive)
*/
public fun SortKey.isBetween(min: LiteralExpr, max: LiteralExpr): SortKeyExpr

/**
* Creates a range expression for verifying the sort key is between two other expressions
* @param min The lower bound value
* @param max The upper bound value (inclusive)
*/
public fun SortKey.isBetween(min: ByteArray, max: ByteArray): SortKeyExpr =
isBetween(LiteralExpr(min), LiteralExpr(max))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: this is missing support for isBetween with strings, is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent question!

Under the hood, both isBetween and isIn(ClosedRange<*>) get converted into BETWEEN expressions in DynamoDB's expression syntax. (A DynamoDB IN operator also exists but it's only used for checking if a member is present in a set/list, which we make available via isIn(Collection<*>).)

I made the decision both in Filter and SortKeyFilter to use different names for different argument types to closely mirror familiar Kotlin idioms. My reasoning was that, in idiomatic Kotlin, one would typically perform a range check with the in operator (something like if (foo in 40..60)). Unfortunately, Kotlin's ranges only work on Comparable types and ByteArray doesn't implement that interface. There's no way to use in to do something like foo in someByteArray..someOtherByteArray—that fails to compile. Neither would there be some way to reference attribute paths in an expression (e.g., foo in attr("bar")..attr("baz")) since those aren't comparable either.

So I needed a two-arg version of this check for LiteralExpr and ByteArray accepting a min and max. The name "in" no longer seemed appropriate since Kotlin's in is a binary operator and this is really a trinary operation. Kotlin has no good equivalent so I decided that "between" made more sense for the two-arg methods.

Now, each of the ClosedRange/single-arg methods could be written as isBetween/min+max methods but it seemed unnecessary and I'd rather hear demand for that than add it preemptively. But the inverse is not true: none of the existing isBetween/min+max methods can be rewritten as a ClosedRange/single-arg method.

OK, now that the explanations and justifications are over, the question is "does this make sense?". Do these choices make the DSL easier or harder for users to understand/use?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline and decided the current naming schema makes sense for now.

Comment on lines +387 to +394
/**
* Creates a range expression for verifying the sort key is in the given range
* @param range The range to check
*/
@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("isInRangeString")
public infix fun SortKey.isIn(range: ClosedRange<String>): SortKeyExpr =
isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Do we want to support isIn for binary types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Longer answer above but ClosedRange requires Comparable type arguments and ByteArray is not comparable.

@@ -30,3 +40,10 @@ public data class Member(val name: String, val type: Type, val mutable: Boolean
}
}
}

/**
* Gets the low-level [Structure] equivalent for this high-level structure
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Gets the low-level [Member] equivalent for this high-level Member

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, copy-paste error.

…ing, add runtime check for acceptable partition key value types, remove unused function, re-add accidentally-removed trailing comma from codegen
Copy link

github-actions bot commented Oct 2, 2024

A new generated diff is ready to view.

  • No codegen difference in the AWS SDK

Copy link

sonarcloud bot commented Oct 3, 2024

Copy link

github-actions bot commented Oct 3, 2024

A new generated diff is ready to view.

  • No codegen difference in the AWS SDK

@ianbotsf ianbotsf merged commit e080cea into feat-ddb-mapper Oct 3, 2024
11 checks passed
@ianbotsf ianbotsf deleted the ddbmapper-filter-expressions-codegen branch October 3, 2024 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants