Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
098c215
Convert AttributeValue from class to readonly struct with union layou…
Im5tu Mar 8, 2026
1500733
Migrate all DynamoDB operations and Bedrock conversation store to Att…
Im5tu Mar 8, 2026
21be57e
Update DynamoDB source generator to emit AttributeValue factory metho…
Im5tu Mar 8, 2026
f09cfcc
Update all tests to use AttributeValue factory methods and struct sem…
Im5tu Mar 8, 2026
26846ec
Remove typed benchmark methods that belong to WT6 (perf/typed-dynamo)
Im5tu Mar 9, 2026
0e4d906
Remove typed benchmarks and tests that depend on WT6 features
Im5tu Mar 10, 2026
b893beb
Merge branch 'main' into perf/attribute-value
Im5tu Mar 10, 2026
1a07628
Fix TypedExtensionTests to use AttributeValue factory methods after m…
Im5tu Mar 10, 2026
3cea139
Fix codegen issues: struct null checks, attribute names, format strin…
Im5tu Mar 11, 2026
05cdf4b
Merge main into perf/attribute-value
Im5tu Mar 11, 2026
9d3a102
fix: Add null validation to AttributeValue factory methods
Im5tu Mar 11, 2026
341b3ef
Merge main into perf/attribute-value
Im5tu Mar 11, 2026
013c962
fix: Throw JsonException for invalid AttributeValue JSON and update n…
Im5tu Mar 11, 2026
27b3b27
fix: Use culture-invariant parsing for DynamoDB numeric values
Im5tu Mar 11, 2026
929a848
fix: Use DynamoDB attribute name for record lookups in type handlers
Im5tu Mar 11, 2026
02ec8c3
fix: Throw JsonException for unrecognized DynamoDB attribute types
Im5tu Mar 11, 2026
4886f86
fix: Preserve NULL elements in nullable collections and handle null d…
Im5tu Mar 11, 2026
161a2f3
fix: Validate NULL attribute boolean and EndObject in DynamoResponseR…
Im5tu Mar 11, 2026
8efbf3b
fix: Harden AttributeValueJsonConverter with EndObject and NULL valid…
Im5tu Mar 11, 2026
a574b04
chore: Uncomment 100-item query benchmarks for apples-to-apples compa…
Im5tu Mar 12, 2026
6c3530a
chore: Comment out 1-item query benchmarks
Im5tu Mar 12, 2026
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 @@ -68,9 +68,9 @@ public DynamoConversationStore(IDynamoClient dynamoClient, DynamoConversationSto
[_configuration.PartitionKeyName] = _configuration.ConversationPkFormat(conversationId),
[_configuration.SortKeyName] = _configuration.ConversationSkValue,
[IdAttribute] = conversationId,
[CreatedAtAttribute] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() },
[UpdatedAtAttribute] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() },
[MessageCountAttribute] = new AttributeValue { N = "0" }
[CreatedAtAttribute] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString()),
[UpdatedAtAttribute] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString()),
[MessageCountAttribute] = AttributeValue.Number("0")
};

if (metadata is not null)
Expand All @@ -81,7 +81,7 @@ public DynamoConversationStore(IDynamoClient dynamoClient, DynamoConversationSto
if (_configuration.DefaultTtl.HasValue)
{
var expiresAt = now.Add(_configuration.DefaultTtl.Value).ToUnixTimeSeconds();
item[_configuration.TtlAttributeName] = new AttributeValue { N = expiresAt.ToString() };
item[_configuration.TtlAttributeName] = AttributeValue.Number(expiresAt.ToString());
}

var request = new PutItemRequest
Expand Down Expand Up @@ -261,9 +261,9 @@ private async Task<ErrorOr<IReadOnlyList<ConversationMessage>>> AddMessagesInter
[_configuration.SortKeyName] = _configuration.MessageSkFormat(conversationId, sequenceNumber),
[IdAttribute] = messageId,
[ConversationIdAttribute] = conversationId,
[SequenceNumberAttribute] = new AttributeValue { N = sequenceNumber.ToString() },
[SequenceNumberAttribute] = AttributeValue.Number(sequenceNumber.ToString()),
[RoleAttribute] = role.ToString(),
[CreatedAtAttribute] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() }
[CreatedAtAttribute] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString())
};

// Serialize content blocks
Expand All @@ -275,14 +275,14 @@ private async Task<ErrorOr<IReadOnlyList<ConversationMessage>>> AddMessagesInter
return serialized.Errors;
contentList.Add(serialized.Value);
}
messageItem[ContentAttribute] = new AttributeValue { L = contentList };
messageItem[ContentAttribute] = AttributeValue.FromList(contentList);

// Add token usage if present
if (tokenUsage is not null)
{
messageItem[InputTokensAttribute] = new AttributeValue { N = tokenUsage.InputTokens.ToString() };
messageItem[OutputTokensAttribute] = new AttributeValue { N = tokenUsage.OutputTokens.ToString() };
messageItem[TokensAttribute] = new AttributeValue { N = tokenUsage.TotalTokens.ToString() };
messageItem[InputTokensAttribute] = AttributeValue.Number(tokenUsage.InputTokens.ToString());
messageItem[OutputTokensAttribute] = AttributeValue.Number(tokenUsage.OutputTokens.ToString());
messageItem[TokensAttribute] = AttributeValue.Number(tokenUsage.TotalTokens.ToString());

totalInputTokens += tokenUsage.InputTokens;
totalOutputTokens += tokenUsage.OutputTokens;
Expand All @@ -298,18 +298,18 @@ private async Task<ErrorOr<IReadOnlyList<ConversationMessage>>> AddMessagesInter
var tagValues = new List<AttributeValue>();
foreach (var value in kvp.Value)
{
tagValues.Add(new AttributeValue { S = value });
tagValues.Add(AttributeValue.String(value));
}
tagsMap[kvp.Key] = new AttributeValue { L = tagValues };
tagsMap[kvp.Key] = AttributeValue.FromList(tagValues);
}
messageItem[ExtractedTagsAttribute] = new AttributeValue { M = tagsMap };
messageItem[ExtractedTagsAttribute] = AttributeValue.FromMap(tagsMap);
}

// Inherit conversation TTL
if (_configuration.DefaultTtl.HasValue)
{
var expiresAt = now.Add(_configuration.DefaultTtl.Value).ToUnixTimeSeconds();
messageItem[_configuration.TtlAttributeName] = new AttributeValue { N = expiresAt.ToString() };
messageItem[_configuration.TtlAttributeName] = AttributeValue.Number(expiresAt.ToString());
}

transactItems.Add(new TransactWriteItem
Expand Down Expand Up @@ -343,19 +343,19 @@ private async Task<ErrorOr<IReadOnlyList<ConversationMessage>>> AddMessagesInter

var updateExpressionValues = new Dictionary<string, AttributeValue>
{
[":msgCount"] = new AttributeValue { N = messageList.Count.ToString() },
[":updatedAt"] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() }
[":msgCount"] = AttributeValue.Number(messageList.Count.ToString()),
[":updatedAt"] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString())
};

if (totalTokens > 0)
{
updateParts.Add($"{TotalInputTokensAttribute} = if_not_exists({TotalInputTokensAttribute}, :zero) + :inputTokens");
updateParts.Add($"{TotalOutputTokensAttribute} = if_not_exists({TotalOutputTokensAttribute}, :zero) + :outputTokens");
updateParts.Add($"{TotalTokensAttribute} = if_not_exists({TotalTokensAttribute}, :zero) + :totalTokens");
updateExpressionValues[":zero"] = new AttributeValue { N = "0" };
updateExpressionValues[":inputTokens"] = new AttributeValue { N = totalInputTokens.ToString() };
updateExpressionValues[":outputTokens"] = new AttributeValue { N = totalOutputTokens.ToString() };
updateExpressionValues[":totalTokens"] = new AttributeValue { N = totalTokens.ToString() };
updateExpressionValues[":zero"] = AttributeValue.Number("0");
updateExpressionValues[":inputTokens"] = AttributeValue.Number(totalInputTokens.ToString());
updateExpressionValues[":outputTokens"] = AttributeValue.Number(totalOutputTokens.ToString());
updateExpressionValues[":totalTokens"] = AttributeValue.Number(totalTokens.ToString());
}

transactItems.Add(new TransactWriteItem
Expand Down Expand Up @@ -396,7 +396,7 @@ private async Task<ErrorOr<IReadOnlyList<ConversationMessage>>> AddMessagesInter
var updateParts = new List<string> { $"{UpdatedAtAttribute} = :updatedAt" };
var expressionAttributeValues = new Dictionary<string, AttributeValue>
{
[":updatedAt"] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() }
[":updatedAt"] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString())
};
var expressionAttributeNames = new Dictionary<string, string>();

Expand All @@ -415,7 +415,7 @@ private async Task<ErrorOr<IReadOnlyList<ConversationMessage>>> AddMessagesInter
if (metadata.Tags.Count > 0)
{
updateParts.Add($"{TagsAttribute} = :tags");
expressionAttributeValues[":tags"] = new AttributeValue { SS = metadata.Tags };
expressionAttributeValues[":tags"] = AttributeValue.FromStringSet(metadata.Tags);
}

if (metadata.CustomData.Count > 0)
Expand All @@ -427,7 +427,7 @@ private async Task<ErrorOr<IReadOnlyList<ConversationMessage>>> AddMessagesInter
{
customDataMap[kvp.Key] = kvp.Value;
}
expressionAttributeValues[":customData"] = new AttributeValue { M = customDataMap };
expressionAttributeValues[":customData"] = AttributeValue.FromMap(customDataMap);
}

var updateRequest = new UpdateItemRequest
Expand Down Expand Up @@ -488,8 +488,8 @@ public async Task<ErrorOr<Deleted>> DeleteConversationAsync(string conversationI
{
allItems.Add(new Dictionary<string, AttributeValue>
{
[_configuration.PartitionKeyName] = item[_configuration.PartitionKeyName]!,
[_configuration.SortKeyName] = item[_configuration.SortKeyName]!
[_configuration.PartitionKeyName] = item[_configuration.PartitionKeyName]!.Value,
[_configuration.SortKeyName] = item[_configuration.SortKeyName]!.Value
});
}

Expand Down Expand Up @@ -535,7 +535,7 @@ private static void SerializeMetadata(ConversationMetadata metadata, Dictionary<
item[ModelIdAttribute] = metadata.ModelId;

if (metadata.Tags.Count > 0)
item[TagsAttribute] = new AttributeValue { SS = metadata.Tags };
item[TagsAttribute] = AttributeValue.FromStringSet(metadata.Tags);

if (metadata.CustomData.Count > 0)
{
Expand All @@ -544,7 +544,7 @@ private static void SerializeMetadata(ConversationMetadata metadata, Dictionary<
{
customDataMap[kvp.Key] = kvp.Value;
}
item[CustomDataAttribute] = new AttributeValue { M = customDataMap };
item[CustomDataAttribute] = AttributeValue.FromMap(customDataMap);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,11 @@ public static ErrorOr<AttributeValue> Serialize(ContentBlock block)
{
if (block.Text is not null)
{
return new AttributeValue
return AttributeValue.FromMap(new Dictionary<string, AttributeValue>
{
M = new Dictionary<string, AttributeValue>
{
[TypeAttribute] = TextType,
["text"] = block.Text
}
};
[TypeAttribute] = TextType,
["text"] = block.Text
});
}

if (block.Image is not null)
Expand All @@ -55,7 +52,7 @@ public static ErrorOr<AttributeValue> Serialize(ContentBlock block)
imageMap["s3BucketOwner"] = block.Image.Source.S3Location.BucketOwner;
}

return new AttributeValue { M = imageMap };
return AttributeValue.FromMap(imageMap);
}

if (block.Document is not null)
Expand All @@ -78,7 +75,7 @@ public static ErrorOr<AttributeValue> Serialize(ContentBlock block)
docMap["s3BucketOwner"] = block.Document.Source.S3Location.BucketOwner;
}

return new AttributeValue { M = docMap };
return AttributeValue.FromMap(docMap);
}

if (block.ToolUse is not null)
Expand All @@ -91,7 +88,7 @@ public static ErrorOr<AttributeValue> Serialize(ContentBlock block)
["input"] = block.ToolUse.Input.GetRawText()
};

return new AttributeValue { M = toolUseMap };
return AttributeValue.FromMap(toolUseMap);
}

if (block.ToolResult is not null)
Expand All @@ -109,13 +106,13 @@ public static ErrorOr<AttributeValue> Serialize(ContentBlock block)
{
[TypeAttribute] = ToolResultType,
["toolUseId"] = block.ToolResult.ToolUseId,
["content"] = new AttributeValue { L = contentList }
["content"] = AttributeValue.FromList(contentList)
};

if (block.ToolResult.Status is not null)
toolResultMap["status"] = block.ToolResult.Status;

return new AttributeValue { M = toolResultMap };
return AttributeValue.FromMap(toolResultMap);
}

return Error.Validation(ConversationErrorCodes.ContentBlockEmpty, "ContentBlock must have at least one content type set");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ private void GenerateConcreteToDynamoRecord(CodeBuilder builder, DynamoTypeInfo
var typeNameField = (dynamoModelAttr?.TypeName != "Type" ? dynamoModelAttr?.TypeName : null)
?? GetInheritedDynamoModelAttribute(type)?.TypeName
?? "Type";
builder.AppendLine($"record[\"{typeNameField}\"] = new AttributeValue {{ S = \"{type.FullName}\" }};");
builder.AppendLine($"record[\"{typeNameField}\"] = AttributeValue.String(\"{type.FullName}\");");
}

// Add property mappings, excluding properties already emitted as GSI keys
Expand All @@ -187,8 +187,8 @@ private void GeneratePrimaryKeyAssignment(CodeBuilder builder, DynamoTypeInfo ty
var pkCode = GenerateKeyCode(type, dynamoAttr.PK, "PK");
var skCode = GenerateKeyCode(type, dynamoAttr.SK, "SK");

builder.AppendLine($"record[\"{dynamoAttr.PKName}\"] = new AttributeValue {{ S = {pkCode} }};");
builder.AppendLine($"record[\"{dynamoAttr.SKName}\"] = new AttributeValue {{ S = {skCode} }};");
builder.AppendLine($"record[\"{dynamoAttr.PKName}\"] = AttributeValue.String({pkCode});");
builder.AppendLine($"record[\"{dynamoAttr.SKName}\"] = AttributeValue.String({skCode});");
}

private void GenerateGSIKeyAssignment(CodeBuilder builder, DynamoTypeInfo type, GSIAttributeInfo gsiAttr)
Expand All @@ -208,7 +208,7 @@ private void GenerateGSIKeyAssignment(CodeBuilder builder, DynamoTypeInfo type,
else
{
var pkCode = GenerateKeyCode(type, gsiAttr.PK, gsiAttr.PKName!);
builder.AppendLine($"record[\"{gsiAttr.PKName}\"] = new AttributeValue {{ S = {pkCode} }};");
builder.AppendLine($"record[\"{gsiAttr.PKName}\"] = AttributeValue.String({pkCode});");
}

// Generate SK assignment - conditional if single nullable property, otherwise direct
Expand All @@ -219,7 +219,7 @@ private void GenerateGSIKeyAssignment(CodeBuilder builder, DynamoTypeInfo type,
else
{
var skCode = GenerateKeyCode(type, gsiAttr.SK, gsiAttr.SKName!);
builder.AppendLine($"record[\"{gsiAttr.SKName}\"] = new AttributeValue {{ S = {skCode} }};");
builder.AppendLine($"record[\"{gsiAttr.SKName}\"] = AttributeValue.String({skCode});");
}
}

Expand Down Expand Up @@ -773,7 +773,7 @@ private void GenerateConditionalGSIKeyAssignment(CodeBuilder builder, string att
}

builder.OpenBraceWithLine($"if ({nullCheck})");
builder.AppendLine($"record[\"{attributeName}\"] = new AttributeValue {{ S = {valueCode} }};");
builder.AppendLine($"record[\"{attributeName}\"] = AttributeValue.String({valueCode});");
builder.CloseBrace();
}

Expand Down
Loading
Loading