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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public object MapperPkg {
public val CollectionValues: String = "$Values.collections"
public val ScalarValues: String = "$Values.scalars"
public val SmithyTypeValues: String = "$Values.smithytypes"

@InternalSdkApi
public object Expressions {
public val Base: String = "${Hl.Base}.expressions"
public val Internal: String = "$Base.internal"
Comment on lines +27 to +28
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

}
}

@InternalSdkApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ public object MapperTypes {
public val ManualPagination: TypeRef = TypeRef(MapperPkg.Hl.Annotations, "ManualPagination")
}

public object Expressions {
public val BooleanExpr: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "BooleanExpr")
public val Filter: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "Filter")
public val KeyFilter: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "KeyFilter")

public object Internal {
public val FilterImpl: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Internal, "FilterImpl")
public val ParameterizingExpressionVisitor: TypeRef =
TypeRef(MapperPkg.Hl.Expressions.Internal, "ParameterizingExpressionVisitor")
public val toExpression: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Internal, "toExpression")
}
}

public object Internal {
public val withWrappedClient: TypeRef = TypeRef(MapperPkg.Hl.Internal, "withWrappedClient")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,12 @@
package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model

import aws.sdk.kotlin.hll.codegen.model.Operation
import aws.sdk.kotlin.hll.codegen.model.Structure
import aws.smithy.kotlin.runtime.collections.AttributeKey

/**
* Defines [AttributeKey] instances that relate to the data model of low-level to high-level codegen
*/
internal object ModelAttributes {
/**
* For a given high-level [Operation], this attribute key identifies the associated low-level [Operation]
*/
val LowLevelOperation: AttributeKey<Operation> = AttributeKey("aws.sdk.kotlin.ddbmapper#LowLevelOperation")

/**
* For a given high-level [Structure], this attribute key identifies the associated low-level [Structure]
*/
val LowLevelStructure: AttributeKey<Structure> = AttributeKey("aws.sdk.kotlin.ddbmapper#LowLevelStructure")

internal object MapperAttributes {
/**
* For a given [Operation], this attribute key contains relevant pagination members (if applicable) in the request
* and response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model

import aws.sdk.kotlin.hll.codegen.model.Member
import aws.sdk.kotlin.hll.codegen.model.TypeRef
import aws.sdk.kotlin.hll.codegen.model.Types
import aws.sdk.kotlin.hll.codegen.model.nullable
import aws.sdk.kotlin.hll.codegen.model.*
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperPkg
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes

private val attrMapTypes = setOf(MapperTypes.AttributeMap, MapperTypes.AttributeMap.nullable())
private val attrMapListTypes = Types.Kotlin.list(MapperTypes.AttributeMap).let { setOf(it, it.nullable()) }
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.ExpressionArgumentsType.*
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.ExpressionLiteralType.*
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.MemberCodegenBehavior.*

/**
* Describes a behavior to apply for a given [Member] in a low-level structure when generating code for an equivalent
Expand Down Expand Up @@ -60,56 +57,105 @@ internal sealed interface MemberCodegenBehavior {
* structure).
*/
data object Hoist : MemberCodegenBehavior

/**
* Indicates that a member is a string expression parameter which should be replaced by an expression DSL
* @param type The type of expression this member models
*/
data class ExpressionLiteral(val type: ExpressionLiteralType) : MemberCodegenBehavior

/**
* Indicates that a member is a map of expression arguments which should be automatically handled by an expression
* DSL
* @param type The type of expression arguments this member models
*/
data class ExpressionArguments(val type: ExpressionArgumentsType) : MemberCodegenBehavior
}

/**
* Identifies a type of expression literal supported by DynamoDB APIs
*/
internal enum class ExpressionLiteralType {
Condition,
Filter,
KeyCondition,
Projection,
Update,
}

/**
* Identifies a type of expression arguments supported by DynamoDB APIs
*/
internal enum class ExpressionArgumentsType {
AttributeNames,
AttributeValues,
}

/**
* Identifies a [MemberCodegenBehavior] for this [Member] by way of various heuristics
*/
internal val Member.codegenBehavior: MemberCodegenBehavior
get() = when {
this in unsupportedMembers -> MemberCodegenBehavior.Drop
type in attrMapTypes -> if (name == "key") MemberCodegenBehavior.MapKeys else MemberCodegenBehavior.MapAll
type in attrMapListTypes -> MemberCodegenBehavior.ListMapAll
isTableName || isIndexName -> MemberCodegenBehavior.Hoist
else -> MemberCodegenBehavior.PassThrough
}
get() = rules.firstNotNullOfOrNull { it.matchedBehaviorOrNull(this) } ?: PassThrough

private val Member.isTableName: Boolean
get() = name == "tableName" && type == Types.Kotlin.StringNullable
private fun llType(name: String) = TypeRef(MapperPkg.Ll.Model, name)

private val Member.isIndexName: Boolean
get() = name == "indexName" && type == Types.Kotlin.StringNullable
private data class Rule(
val namePredicate: (String) -> Boolean,
val typePredicate: (TypeRef) -> Boolean,
val behavior: MemberCodegenBehavior,
) {
constructor(name: String, type: TypeRef, behavior: MemberCodegenBehavior) :
this(name::equals, type::isEquivalentTo, behavior)

private fun llType(name: String) = TypeRef(MapperPkg.Ll.Model, name)
constructor(name: Regex, type: TypeRef, behavior: MemberCodegenBehavior) :
this(name::matches, type::isEquivalentTo, behavior)

private val unsupportedMembers = listOf(
// superseded by ConditionExpression
Member("conditionalOperator", llType("ConditionalOperator")),
Member("expected", Types.Kotlin.stringMap(llType("ExpectedAttributeValue"))),

// superseded by FilterExpression
Member("queryFilter", Types.Kotlin.stringMap(llType("Condition"))),
Member("scanFilter", Types.Kotlin.stringMap(llType("Condition"))),

// superseded by KeyConditionExpression
Member("keyConditions", Types.Kotlin.stringMap(llType("Condition"))),

// superseded by ProjectionExpression
Member("attributesToGet", Types.Kotlin.list(Types.Kotlin.String)),

// superseded by UpdateExpression
Member("attributeUpdates", Types.Kotlin.stringMap(llType("AttributeValueUpdate"))),

// TODO add support for expressions
Member("expressionAttributeNames", Types.Kotlin.stringMap(Types.Kotlin.String)),
Member("expressionAttributeValues", MapperTypes.AttributeMap),
Member("conditionExpression", Types.Kotlin.String),
Member("projectionExpression", Types.Kotlin.String),
Member("updateExpression", Types.Kotlin.String),
).map { member ->
if (member.type is TypeRef) {
member.copy(type = member.type.nullable())
} else {
member
}
}.toSet()
fun matchedBehaviorOrNull(member: Member) = if (matches(member)) behavior else null
fun matches(member: Member) = namePredicate(member.name) && typePredicate(member.type as TypeRef)
}

private fun Type.isEquivalentTo(other: Type): Boolean = when (this) {
is TypeVar -> other is TypeVar && shortName == other.shortName
is TypeRef ->
other is TypeRef &&
fullName == other.fullName &&
genericArgs.size == other.genericArgs.size &&
genericArgs.zip(other.genericArgs).all { (thisArg, otherArg) -> thisArg.isEquivalentTo(otherArg) }
}

/**
* Priority-ordered list of dispositions to apply to members found in structures. The first element from this list that
* successfully matches with a member will be chosen.
*/
private val rules = listOf(
// Deprecated expression members not to be carried forward into HLL
Rule("conditionalOperator", llType("ConditionalOperator"), Drop),
Rule("expected", Types.Kotlin.stringMap(llType("ExpectedAttributeValue")), Drop),
Rule("queryFilter", Types.Kotlin.stringMap(llType("Condition")), Drop),
Rule("scanFilter", Types.Kotlin.stringMap(llType("Condition")), Drop),
Rule("keyConditions", Types.Kotlin.stringMap(llType("Condition")), Drop),
Rule("attributesToGet", Types.Kotlin.list(Types.Kotlin.String), Drop),
Rule("attributeUpdates", Types.Kotlin.stringMap(llType("AttributeValueUpdate")), Drop),

// Hoisted members
Rule("tableName", Types.Kotlin.String, Hoist),
Rule("indexName", Types.Kotlin.String, Hoist),

// Expression literals
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.

Rule("conditionExpression", Types.Kotlin.String, Drop),
Rule("projectionExpression", Types.Kotlin.String, Drop),
Rule("updateExpression", Types.Kotlin.String, Drop),

// Expression arguments
Rule("expressionAttributeNames", Types.Kotlin.stringMap(Types.Kotlin.String), ExpressionArguments(AttributeNames)),
Rule("expressionAttributeValues", MapperTypes.AttributeMap, ExpressionArguments(AttributeValues)),

// Mappable members
Rule(".*".toRegex(), Types.Kotlin.list(MapperTypes.AttributeMap), ListMapAll),
Rule("key", MapperTypes.AttributeMap, MapKeys),
Rule(".*".toRegex(), MapperTypes.AttributeMap, MapAll),
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model

import aws.sdk.kotlin.hll.codegen.model.ModelAttributes
import aws.sdk.kotlin.hll.codegen.model.Operation
import aws.sdk.kotlin.hll.codegen.util.plus
import aws.smithy.kotlin.runtime.collections.get

/**
* Gets the low-level [Operation] equivalent for this high-level operation
*/
internal val Operation.lowLevel: Operation
get() = attributes[ModelAttributes.LowLevelOperation]

/**
* Derives a high-level [Operation] equivalent for this low-level operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ internal data class PaginationMembers(
* property returns `null`.
*/
internal val Operation.paginationInfo: PaginationMembers?
get() = attributes.getOrNull(ModelAttributes.PaginationInfo)
get() = attributes.getOrNull(MapperAttributes.PaginationInfo)

/**
* A codegen plugin that adds DDB-specific pagination info to operations
*/
internal class DdbPaginationPlugin : ModelParsingPlugin {
override fun postProcessOperation(operation: Operation): Operation {
val paginationMembers = PaginationMembers.forOperationOrNull(operation) ?: return operation
val newAttributes = operation.attributes + (ModelAttributes.PaginationInfo to paginationMembers)
val newAttributes = operation.attributes + (MapperAttributes.PaginationInfo to paginationMembers)
return operation.copy(attributes = newAttributes)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,7 @@ package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model

import aws.sdk.kotlin.hll.codegen.model.*
import aws.sdk.kotlin.hll.codegen.util.plus
import aws.smithy.kotlin.runtime.collections.get

/**
* Gets the low-level [Structure] equivalent for this high-level structure
*/
internal val Structure.lowLevel: Structure
get() = attributes[ModelAttributes.LowLevelStructure]
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes

/**
* Derives a high-level [Structure] equivalent for this low-level structure
Expand All @@ -24,20 +18,54 @@ internal fun Structure.toHighLevel(pkg: String): Structure {
val hlType = TypeRef(pkg, llStructure.type.shortName, listOf(TypeVar("T")))

val hlMembers = llStructure.members.mapNotNull { llMember ->
when (llMember.codegenBehavior) {
MemberCodegenBehavior.PassThrough -> llMember
val nullable = llMember.type.nullable

val hlMember = when (val behavior = llMember.codegenBehavior) {
MemberCodegenBehavior.PassThrough, is MemberCodegenBehavior.ExpressionArguments -> llMember

MemberCodegenBehavior.MapAll, MemberCodegenBehavior.MapKeys ->
llMember.copy(type = TypeVar("T", llMember.type.nullable))
llMember.copy(type = TypeVar("T", nullable))

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.

llMember.copy(type = hlListType)
}

is MemberCodegenBehavior.ExpressionLiteral -> {
val expressionType = when (behavior.type) {
ExpressionLiteralType.Filter -> MapperTypes.Expressions.BooleanExpr
ExpressionLiteralType.KeyCondition -> MapperTypes.Expressions.KeyFilter

// TODO add support for other expression types
else -> return@mapNotNull null
}.nullable(nullable)

val dslInfo = when (behavior.type) {
ExpressionLiteralType.Filter -> DslInfo(
interfaceType = MapperTypes.Expressions.Filter,
implType = MapperTypes.Expressions.Internal.FilterImpl,
implSingleton = true,
)

// KeyCondition doesn't use a top-level DSL (SortKeyCondition is nested)
ExpressionLiteralType.KeyCondition -> null

// TODO add support for other expression types
else -> return@mapNotNull null
}

llMember.copy(
name = llMember.name.removeSuffix("Expression"),
type = expressionType,
attributes = llMember.attributes + (ModelAttributes.DslInfo to dslInfo),
)
}

else -> null
}

hlMember?.copy(attributes = hlMember.attributes + (ModelAttributes.LowLevelMember to llMember))
}.toSet()

val hlAttributes = llStructure.attributes + (ModelAttributes.LowLevelStructure to llStructure)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.rendering

import aws.sdk.kotlin.hll.codegen.core.CodeGenerator
import aws.sdk.kotlin.hll.codegen.model.*
import aws.sdk.kotlin.hll.codegen.rendering.BuilderRenderer
import aws.sdk.kotlin.hll.codegen.rendering.RenderContext
import aws.sdk.kotlin.hll.codegen.rendering.RenderOptions
import aws.sdk.kotlin.hll.codegen.rendering.Visibility
import aws.sdk.kotlin.hll.codegen.rendering.*
import aws.sdk.kotlin.hll.codegen.util.plus

/**
Expand Down
Loading
Loading