fix cloned string cleanup

This commit is contained in:
2026-02-17 02:03:40 -05:00
parent a6bf357228
commit 225a1533cc
9 changed files with 113 additions and 59 deletions

View File

@@ -16,6 +16,7 @@
package dynamodb package dynamodb
import "core:encoding/json" import "core:encoding/json"
import "core:strings"
// ============================================================================ // ============================================================================
// Condition Evaluation Result // Condition Evaluation Result
@@ -54,7 +55,7 @@ parse_condition_expression_string :: proc(request_body: []byte) -> (expr: string
return return
} }
expr = string(ce_str) expr = strings.clone(string(ce_str))
ok = true ok = true
return return
} }
@@ -88,6 +89,7 @@ evaluate_condition_expression :: proc(
if !has_condition { if !has_condition {
return .Passed // No condition → always pass return .Passed // No condition → always pass
} }
defer delete(condition_str)
// Parse the condition into a filter tree (same grammar as FilterExpression) // Parse the condition into a filter tree (same grammar as FilterExpression)
filter_node, parse_ok := parse_filter_expression(condition_str, attr_names, attr_values) filter_node, parse_ok := parse_filter_expression(condition_str, attr_names, attr_values)

View File

@@ -480,7 +480,7 @@ parse_key_condition_expression_string :: proc(request_body: []byte) -> (expr: st
return return
} }
expr = string(kce_str) expr = strings.clone(string(kce_str))
ok = true ok = true
return return
} }
@@ -488,6 +488,7 @@ parse_key_condition_expression_string :: proc(request_body: []byte) -> (expr: st
// Convenience: parse a complete Query key condition from request body // Convenience: parse a complete Query key condition from request body
parse_query_key_condition :: proc(request_body: []byte) -> (kc: Key_Condition, ok: bool) { parse_query_key_condition :: proc(request_body: []byte) -> (kc: Key_Condition, ok: bool) {
expression := parse_key_condition_expression_string(request_body) or_return expression := parse_key_condition_expression_string(request_body) or_return
defer delete(expression)
attr_names := parse_expression_attribute_names(request_body) attr_names := parse_expression_attribute_names(request_body)
defer { defer {

View File

@@ -817,7 +817,7 @@ parse_filter_expression_string :: proc(request_body: []byte) -> (expr: string, o
return return
} }
expr = string(fe_str) expr = strings.clone(string(fe_str))
ok = true ok = true
return return
} }

View File

@@ -431,7 +431,7 @@ parse_table_name :: proc(request_body: []byte) -> (string, bool) {
return "", false return "", false
} }
return string(table_name_str), true return strings.clone(string(table_name_str)), true
} }
// Parse Item field from request body // Parse Item field from request body

View File

@@ -54,6 +54,10 @@ Cancellation_Reason :: struct {
} }
transact_write_action_destroy :: proc(action: ^Transact_Write_Action) { transact_write_action_destroy :: proc(action: ^Transact_Write_Action) {
delete(action.table_name)
if ce, has := action.condition_expr.?; has {
delete(ce)
}
if item, has := action.item.?; has { if item, has := action.item.?; has {
item_copy := item item_copy := item
item_destroy(&item_copy) item_destroy(&item_copy)
@@ -618,8 +622,12 @@ Transact_Get_Result :: struct {
} }
transact_get_action_destroy :: proc(action: ^Transact_Get_Action) { transact_get_action_destroy :: proc(action: ^Transact_Get_Action) {
delete(action.table_name)
item_destroy(&action.key) item_destroy(&action.key)
if proj, has := action.projection.?; has { if proj, has := action.projection.?; has {
for path in proj {
delete(path)
}
delete(proj) delete(proj)
} }
} }

View File

@@ -930,7 +930,7 @@ parse_update_expression_string :: proc(request_body: []byte) -> (expr: string, o
return return
} }
expr = string(ue_str) expr = strings.clone(string(ue_str))
ok = true ok = true
return return
} }
@@ -939,24 +939,24 @@ parse_update_expression_string :: proc(request_body: []byte) -> (expr: string, o
parse_return_values :: proc(request_body: []byte) -> string { parse_return_values :: proc(request_body: []byte) -> string {
data, parse_err := json.parse(request_body, allocator = context.temp_allocator) data, parse_err := json.parse(request_body, allocator = context.temp_allocator)
if parse_err != nil { if parse_err != nil {
return "NONE" return strings.clone("NONE")
} }
defer json.destroy_value(data) defer json.destroy_value(data)
root, root_ok := data.(json.Object) root, root_ok := data.(json.Object)
if !root_ok { if !root_ok {
return "NONE" return strings.clone("NONE")
} }
rv_val, found := root["ReturnValues"] rv_val, found := root["ReturnValues"]
if !found { if !found {
return "NONE" return strings.clone("NONE")
} }
rv_str, str_ok := rv_val.(json.String) rv_str, str_ok := rv_val.(json.String)
if !str_ok { if !str_ok {
return "NONE" return strings.clone("NONE")
} }
return string(rv_str) return strings.clone(string(rv_str))
} }

View File

@@ -259,7 +259,7 @@ parse_index_name :: proc(request_body: []byte) -> Maybe(string) {
return nil return nil
} }
return string(idx_str) return strings.clone(string(idx_str))
} }
// ============================================================================ // ============================================================================

119
main.odin
View File

@@ -241,6 +241,7 @@ handle_describe_table :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R
make_error_response(response, .ValidationException, "Invalid request or missing TableName") make_error_response(response, .ValidationException, "Invalid request or missing TableName")
return return
} }
defer delete(table_name)
metadata, err := dynamodb.get_table_metadata(engine, table_name) metadata, err := dynamodb.get_table_metadata(engine, table_name)
if err != .None { if err != .None {
@@ -340,6 +341,7 @@ handle_put_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request
make_error_response(response, .ValidationException, "Invalid request or missing TableName") make_error_response(response, .ValidationException, "Invalid request or missing TableName")
return return
} }
defer delete(table_name)
item, item_ok := dynamodb.parse_item_from_request(request.body) item, item_ok := dynamodb.parse_item_from_request(request.body)
if !item_ok { if !item_ok {
@@ -349,8 +351,9 @@ handle_put_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request
defer dynamodb.item_destroy(&item) defer dynamodb.item_destroy(&item)
// ---- ConditionExpression evaluation ---- // ---- ConditionExpression evaluation ----
_, has_condition := dynamodb.parse_condition_expression_string(request.body) cond_str, has_condition := dynamodb.parse_condition_expression_string(request.body)
if has_condition { if has_condition {
defer delete(cond_str)
// Parse shared expression attributes // Parse shared expression attributes
attr_names := dynamodb.parse_expression_attribute_names(request.body) attr_names := dynamodb.parse_expression_attribute_names(request.body)
defer { defer {
@@ -468,9 +471,17 @@ handle_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request
if item_val, has_item := item.?; has_item { if item_val, has_item := item.?; has_item {
defer dynamodb.item_destroy(&item_val) defer dynamodb.item_destroy(&item_val)
item_json := dynamodb.serialize_item(item_val)
resp := fmt.aprintf(`{"Item":%s}`, item_json) // Build response directly to avoid intermediate string allocations
response_set_body(response, transmute([]byte)resp) builder := strings.builder_make(context.allocator)
defer strings.builder_destroy(&builder)
strings.write_string(&builder, `{"Item":`)
dynamodb.serialize_item_to_builder(&builder, item_val)
strings.write_string(&builder, `}`)
resp_body := strings.clone(strings.to_string(builder))
response_set_body(response, transmute([]byte)resp_body)
} else { } else {
response_set_body(response, transmute([]byte)string("{}")) response_set_body(response, transmute([]byte)string("{}"))
} }
@@ -571,6 +582,7 @@ handle_update_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Requ
make_error_response(response, .ValidationException, "Invalid request or missing TableName") make_error_response(response, .ValidationException, "Invalid request or missing TableName")
return return
} }
defer delete(table_name)
// Parse Key // Parse Key
key_item, key_ok := dynamodb.parse_key_from_request(request.body) key_item, key_ok := dynamodb.parse_key_from_request(request.body)
@@ -586,6 +598,7 @@ handle_update_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Requ
make_error_response(response, .ValidationException, "Missing or invalid UpdateExpression") make_error_response(response, .ValidationException, "Missing or invalid UpdateExpression")
return return
} }
defer delete(update_expr)
// Parse ExpressionAttributeNames and ExpressionAttributeValues // Parse ExpressionAttributeNames and ExpressionAttributeValues
attr_names := dynamodb.parse_expression_attribute_names(request.body) attr_names := dynamodb.parse_expression_attribute_names(request.body)
@@ -661,6 +674,7 @@ handle_update_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Requ
// Parse ReturnValues // Parse ReturnValues
return_values := dynamodb.parse_return_values(request.body) return_values := dynamodb.parse_return_values(request.body)
defer delete(return_values)
// Execute update // Execute update
old_item, new_item, err := dynamodb.update_item(engine, table_name, key_item, &plan) old_item, new_item, err := dynamodb.update_item(engine, table_name, key_item, &plan)
@@ -680,51 +694,59 @@ handle_update_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Requ
} }
// Build response based on ReturnValues // Build response based on ReturnValues
builder := strings.builder_make(context.allocator)
defer strings.builder_destroy(&builder)
switch return_values { switch return_values {
case "ALL_NEW": case "ALL_NEW":
if new_val, has := new_item.?; has { if new_val, has := new_item.?; has {
item_json := dynamodb.serialize_item(new_val) strings.write_string(&builder, `{"Attributes":`)
resp := fmt.aprintf(`{"Attributes":%s}`, item_json) dynamodb.serialize_item_to_builder(&builder, new_val)
response_set_body(response, transmute([]byte)resp) strings.write_string(&builder, `}`)
} else { } else {
response_set_body(response, transmute([]byte)string("{}")) strings.write_string(&builder, `{}`)
} }
case "ALL_OLD": case "ALL_OLD":
if old, has := old_item.?; has { if old, has := old_item.?; has {
item_json := dynamodb.serialize_item(old) strings.write_string(&builder, `{"Attributes":`)
resp := fmt.aprintf(`{"Attributes":%s}`, item_json) dynamodb.serialize_item_to_builder(&builder, old)
response_set_body(response, transmute([]byte)resp) strings.write_string(&builder, `}`)
} else { } else {
response_set_body(response, transmute([]byte)string("{}")) strings.write_string(&builder, `{}`)
} }
case "UPDATED_NEW": case "UPDATED_NEW":
if new_val, has := new_item.?; has { if new_val, has := new_item.?; has {
filtered := filter_updated_attributes(new_val, &plan) filtered := filter_updated_attributes(new_val, &plan)
defer dynamodb.item_destroy(&filtered) defer dynamodb.item_destroy(&filtered)
item_json := dynamodb.serialize_item(filtered)
resp := fmt.aprintf(`{"Attributes":%s}`, item_json) strings.write_string(&builder, `{"Attributes":`)
response_set_body(response, transmute([]byte)resp) dynamodb.serialize_item_to_builder(&builder, filtered)
strings.write_string(&builder, `}`)
} else { } else {
response_set_body(response, transmute([]byte)string("{}")) strings.write_string(&builder, `{}`)
} }
case "UPDATED_OLD": case "UPDATED_OLD":
if old, has := old_item.?; has { if old, has := old_item.?; has {
filtered := filter_updated_attributes(old, &plan) filtered := filter_updated_attributes(old, &plan)
defer dynamodb.item_destroy(&filtered) defer dynamodb.item_destroy(&filtered)
item_json := dynamodb.serialize_item(filtered)
resp := fmt.aprintf(`{"Attributes":%s}`, item_json) strings.write_string(&builder, `{"Attributes":`)
response_set_body(response, transmute([]byte)resp) dynamodb.serialize_item_to_builder(&builder, filtered)
strings.write_string(&builder, `}`)
} else { } else {
response_set_body(response, transmute([]byte)string("{}")) strings.write_string(&builder, `{}`)
} }
case: case:
// "NONE" or default // "NONE" or default
response_set_body(response, transmute([]byte)string("{}")) strings.write_string(&builder, `{}`)
} }
resp_body := strings.clone(strings.to_string(builder))
response_set_body(response, transmute([]byte)resp_body)
} }
handle_batch_write_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) { handle_batch_write_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
@@ -872,7 +894,7 @@ handle_batch_write_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP
} }
append(&table_requests, dynamodb.Batch_Write_Table_Request{ append(&table_requests, dynamodb.Batch_Write_Table_Request{
table_name = string(table_name), table_name = strings.clone(string(table_name)),
requests = requests[:], requests = requests[:],
}) })
} }
@@ -1010,7 +1032,7 @@ handle_batch_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R
} }
append(&table_requests, dynamodb.Batch_Get_Table_Request{ append(&table_requests, dynamodb.Batch_Get_Table_Request{
table_name = string(table_name), table_name = strings.clone(string(table_name)),
keys = keys[:], keys = keys[:],
}) })
} }
@@ -1037,7 +1059,9 @@ handle_batch_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R
defer dynamodb.batch_get_result_destroy(&result) defer dynamodb.batch_get_result_destroy(&result)
// Build response // Build response
builder := strings.builder_make() builder := strings.builder_make(context.allocator)
defer strings.builder_destroy(&builder)
strings.write_string(&builder, `{"Responses":{`) strings.write_string(&builder, `{"Responses":{`)
for table_result, ti in result.responses { for table_result, ti in result.responses {
@@ -1050,8 +1074,7 @@ handle_batch_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R
if ii > 0 { if ii > 0 {
strings.write_string(&builder, ",") strings.write_string(&builder, ",")
} }
item_json := dynamodb.serialize_item(item) dynamodb.serialize_item_to_builder(&builder, item)
strings.write_string(&builder, item_json)
} }
strings.write_string(&builder, "]") strings.write_string(&builder, "]")
@@ -1069,8 +1092,7 @@ handle_batch_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R
if ki > 0 { if ki > 0 {
strings.write_string(&builder, ",") strings.write_string(&builder, ",")
} }
key_json := dynamodb.serialize_item(key) dynamodb.serialize_item_to_builder(&builder, key)
strings.write_string(&builder, key_json)
} }
strings.write_string(&builder, "]}") strings.write_string(&builder, "]}")
@@ -1078,7 +1100,8 @@ handle_batch_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R
strings.write_string(&builder, "}}") strings.write_string(&builder, "}}")
resp_body := strings.to_string(builder) // clone the god damn string
resp_body := strings.clone(strings.to_string(builder))
response_set_body(response, transmute([]byte)resp_body) response_set_body(response, transmute([]byte)resp_body)
} }
@@ -1093,9 +1116,15 @@ handle_query :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, r
make_error_response(response, .ValidationException, "Invalid request or missing TableName") make_error_response(response, .ValidationException, "Invalid request or missing TableName")
return return
} }
defer delete(table_name)
// Grab index name from request body // Grab index name from request body
index_name := parse_index_name(request.body) index_name := parse_index_name(request.body)
defer {
if idx, has := index_name.?; has {
delete(idx)
}
}
// Fetch table metadata early for ExclusiveStartKey parsing // Fetch table metadata early for ExclusiveStartKey parsing
metadata, meta_err := dynamodb.get_table_metadata(engine, table_name) metadata, meta_err := dynamodb.get_table_metadata(engine, table_name)
@@ -1300,9 +1329,15 @@ handle_scan :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, re
make_error_response(response, .ValidationException, "Invalid request or missing TableName") make_error_response(response, .ValidationException, "Invalid request or missing TableName")
return return
} }
defer delete(table_name)
// Grab index name from request body // Grab index name from request body
index_name := parse_index_name(request.body) index_name := parse_index_name(request.body)
defer {
if idx, has := index_name.?; has {
delete(idx)
}
}
metadata, meta_err := dynamodb.get_table_metadata(engine, table_name) metadata, meta_err := dynamodb.get_table_metadata(engine, table_name)
if meta_err != .None { if meta_err != .None {
@@ -1482,6 +1517,7 @@ apply_filter_to_items :: proc(
if !has_filter { if !has_filter {
return items, true return items, true
} }
defer delete(filter_expr)
filter_node, filter_ok := dynamodb.parse_filter_expression(filter_expr, attr_names, attr_values) filter_node, filter_ok := dynamodb.parse_filter_expression(filter_expr, attr_names, attr_values)
if !filter_ok || filter_node == nil { if !filter_ok || filter_node == nil {
@@ -1517,13 +1553,15 @@ write_items_response_with_pagination_ex :: proc(
metadata: ^dynamodb.Table_Metadata, metadata: ^dynamodb.Table_Metadata,
scanned_count: int, scanned_count: int,
) { ) {
builder := strings.builder_make() builder := strings.builder_make(context.allocator)
defer strings.builder_destroy(&builder)
strings.write_string(&builder, `{"Items":[`) strings.write_string(&builder, `{"Items":[`)
// Use serialize_item_to_builder directly an NOT serialize_item
for item, i in items { for item, i in items {
if i > 0 do strings.write_string(&builder, ",") if i > 0 do strings.write_string(&builder, ",")
item_json := dynamodb.serialize_item(item) dynamodb.serialize_item_to_builder(&builder, item)
strings.write_string(&builder, item_json)
} }
strings.write_string(&builder, `],"Count":`) strings.write_string(&builder, `],"Count":`)
@@ -1541,7 +1579,8 @@ write_items_response_with_pagination_ex :: proc(
strings.write_string(&builder, "}") strings.write_string(&builder, "}")
resp_body := strings.to_string(builder) // We have to Clone the string before passing to response_set_body
resp_body := strings.clone(strings.to_string(builder))
response_set_body(response, transmute([]byte)resp_body) response_set_body(response, transmute([]byte)resp_body)
} }
@@ -1561,13 +1600,15 @@ write_items_response_with_pagination :: proc(
last_evaluated_key_binary: Maybe([]byte), last_evaluated_key_binary: Maybe([]byte),
metadata: ^dynamodb.Table_Metadata, metadata: ^dynamodb.Table_Metadata,
) { ) {
builder := strings.builder_make() builder := strings.builder_make(context.allocator)
defer strings.builder_destroy(&builder)
strings.write_string(&builder, `{"Items":[`) strings.write_string(&builder, `{"Items":[`)
// Use serialize_item_to_builder directly so we always get the correct response payload
for item, i in items { for item, i in items {
if i > 0 do strings.write_string(&builder, ",") if i > 0 do strings.write_string(&builder, ",")
item_json := dynamodb.serialize_item(item) dynamodb.serialize_item_to_builder(&builder, item)
strings.write_string(&builder, item_json)
} }
strings.write_string(&builder, `],"Count":`) strings.write_string(&builder, `],"Count":`)
@@ -1575,20 +1616,18 @@ write_items_response_with_pagination :: proc(
strings.write_string(&builder, `,"ScannedCount":`) strings.write_string(&builder, `,"ScannedCount":`)
fmt.sbprintf(&builder, "%d", len(items)) fmt.sbprintf(&builder, "%d", len(items))
// Emit LastEvaluatedKey if the storage layer produced one
if binary_key, has_last := last_evaluated_key_binary.?; has_last { if binary_key, has_last := last_evaluated_key_binary.?; has_last {
lek_json, lek_ok := dynamodb.serialize_last_evaluated_key(binary_key, metadata) lek_json, lek_ok := dynamodb.serialize_last_evaluated_key(binary_key, metadata)
if lek_ok { if lek_ok {
strings.write_string(&builder, `,"LastEvaluatedKey":`) strings.write_string(&builder, `,"LastEvaluatedKey":`)
strings.write_string(&builder, lek_json) strings.write_string(&builder, lek_json)
} }
// If decoding fails we still return the items — just without a pagination token.
// The client will assume the scan/query is complete.
} }
strings.write_string(&builder, "}") strings.write_string(&builder, "}")
resp_body := strings.to_string(builder) // We have to Clone the string before passing to response_set_body
resp_body := strings.clone(strings.to_string(builder))
response_set_body(response, transmute([]byte)resp_body) response_set_body(response, transmute([]byte)resp_body)
} }

View File

@@ -267,7 +267,7 @@ parse_transact_put_action :: proc(
if !tn_ok { if !tn_ok {
return {}, false return {}, false
} }
action.table_name = string(tn_str) action.table_name = strings.clone(string(tn_str))
// Item // Item
item_val, item_found := obj["Item"] item_val, item_found := obj["Item"]
@@ -301,7 +301,7 @@ parse_transact_key_action :: proc(
if !tn_ok { if !tn_ok {
return {}, false return {}, false
} }
action.table_name = string(tn_str) action.table_name = strings.clone(string(tn_str))
// Key // Key
key_val, key_found := obj["Key"] key_val, key_found := obj["Key"]
@@ -335,7 +335,7 @@ parse_transact_update_action :: proc(
if !tn_ok { if !tn_ok {
return {}, false return {}, false
} }
action.table_name = string(tn_str) action.table_name = strings.clone(string(tn_str))
// Key // Key
key_val, key_found := obj["Key"] key_val, key_found := obj["Key"]
@@ -483,7 +483,9 @@ handle_transact_get_items :: proc(
} }
// Build response // Build response
builder := strings.builder_make() builder := strings.builder_make(context.allocator)
defer strings.builder_destroy(&builder)
strings.write_string(&builder, `{"Responses":[`) strings.write_string(&builder, `{"Responses":[`)
for maybe_item, i in result.items { for maybe_item, i in result.items {
@@ -492,8 +494,9 @@ handle_transact_get_items :: proc(
} }
if item, has_item := maybe_item.?; has_item { if item, has_item := maybe_item.?; has_item {
item_json := dynamodb.serialize_item(item) strings.write_string(&builder, `{"Item":`)
fmt.sbprintf(&builder, `{{"Item":%s}}`, item_json) dynamodb.serialize_item_to_builder(&builder, item)
strings.write_string(&builder, `}`)
} else { } else {
strings.write_string(&builder, "{}") strings.write_string(&builder, "{}")
} }
@@ -501,7 +504,8 @@ handle_transact_get_items :: proc(
strings.write_string(&builder, "]}") strings.write_string(&builder, "]}")
resp_body := strings.to_string(builder) // Clone the string or we gonna have issues again
resp_body := strings.clone(strings.to_string(builder))
response_set_body(response, transmute([]byte)resp_body) response_set_body(response, transmute([]byte)resp_body)
} }
@@ -519,7 +523,7 @@ parse_transact_get_action :: proc(obj: json.Object) -> (dynamodb.Transact_Get_Ac
if !tn_ok { if !tn_ok {
return {}, false return {}, false
} }
action.table_name = string(tn_str) action.table_name = strings.clone(string(tn_str))
// Key // Key
key_val, key_found := obj["Key"] key_val, key_found := obj["Key"]