fix storage issues

This commit is contained in:
2026-02-15 20:57:16 -05:00
parent 280ce15b07
commit b510c000ec
5 changed files with 577 additions and 188 deletions

243
main.odin
View File

@@ -75,7 +75,7 @@ handle_dynamodb_request :: proc(ctx: rawptr, request: ^HTTP_Request, request_all
// Get X-Amz-Target header to determine operation
target := request_get_header(request, "X-Amz-Target")
if target == nil {
return make_error_response(&response, .ValidationException, "Missing X-Amz-Target header")
return make_error_response(&response, .SerializationException, "Missing X-Amz-Target header")
}
operation := dynamodb.operation_from_target(target.?)
@@ -96,6 +96,8 @@ handle_dynamodb_request :: proc(ctx: rawptr, request: ^HTTP_Request, request_all
handle_get_item(engine, request, &response)
case .DeleteItem:
handle_delete_item(engine, request, &response)
case .UpdateItem:
handle_update_item(engine, request, &response)
case .Query:
handle_query(engine, request, &response)
case .Scan:
@@ -117,14 +119,14 @@ handle_create_table :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Req
// Parse JSON body
data, parse_err := json.parse(request.body, allocator = context.allocator)
if parse_err != nil {
make_error_response(response, .ValidationException, "Invalid JSON")
make_error_response(response, .SerializationException, "Invalid JSON")
return
}
defer json.destroy_value(data)
root, ok := data.(json.Object)
if !ok {
make_error_response(response, .ValidationException, "Request must be an object")
make_error_response(response, .SerializationException, "Request must be an object")
return
}
@@ -225,7 +227,7 @@ handle_describe_table :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R
}
return
}
defer dynamodb.table_metadata_destroy(&metadata, context.allocator)
defer dynamodb.table_metadata_destroy(&metadata, engine.allocator)
// Build response with key schema
builder := strings.builder_make()
@@ -265,7 +267,6 @@ handle_list_tables :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Requ
make_error_response(response, .InternalServerError, "Failed to list tables")
return
}
// tables are owned by engine allocator — just read them, don't free
builder := strings.builder_make()
strings.write_string(&builder, `{"TableNames":[`)
@@ -301,16 +302,7 @@ handle_put_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request
err := dynamodb.put_item(engine, table_name, item)
if err != .None {
#partial switch err {
case .Table_Not_Found:
make_error_response(response, .ResourceNotFoundException, "Table not found")
case .Missing_Key_Attribute:
make_error_response(response, .ValidationException, "Item missing required key attribute")
case .Invalid_Key:
make_error_response(response, .ValidationException, "Invalid key format")
case:
make_error_response(response, .InternalServerError, "Failed to put item")
}
handle_storage_error(response, err)
return
}
@@ -333,16 +325,7 @@ handle_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request
item, err := dynamodb.get_item(engine, table_name, key)
if err != .None {
#partial switch err {
case .Table_Not_Found:
make_error_response(response, .ResourceNotFoundException, "Table not found")
case .Missing_Key_Attribute:
make_error_response(response, .ValidationException, "Key missing required attributes")
case .Invalid_Key:
make_error_response(response, .ValidationException, "Invalid key format")
case:
make_error_response(response, .InternalServerError, "Failed to get item")
}
handle_storage_error(response, err)
return
}
@@ -372,22 +355,21 @@ handle_delete_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Requ
err := dynamodb.delete_item(engine, table_name, key)
if err != .None {
#partial switch err {
case .Table_Not_Found:
make_error_response(response, .ResourceNotFoundException, "Table not found")
case .Missing_Key_Attribute:
make_error_response(response, .ValidationException, "Key missing required attributes")
case .Invalid_Key:
make_error_response(response, .ValidationException, "Invalid key format")
case:
make_error_response(response, .InternalServerError, "Failed to delete item")
}
handle_storage_error(response, err)
return
}
response_set_body(response, transmute([]byte)string("{}"))
}
// UpdateItem — minimal stub: supports SET for scalar attributes
handle_update_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
// TODO: Implement UpdateExpression parsing (SET x = :val, REMOVE y, etc.)
// For now, return a clear error so callers know it's not yet supported.
make_error_response(response, .ValidationException,
"UpdateItem is not yet supported. Use PutItem to replace the full item.")
}
// ============================================================================
// Query and Scan Operations
// ============================================================================
@@ -399,6 +381,14 @@ handle_query :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, r
return
}
// ---- Fetch table metadata early so we can parse ExclusiveStartKey ----
metadata, meta_err := dynamodb.get_table_metadata(engine, table_name)
if meta_err != .None {
handle_storage_error(response, meta_err)
return
}
defer dynamodb.table_metadata_destroy(&metadata, engine.allocator)
// Parse KeyConditionExpression
kc, kc_ok := dynamodb.parse_query_key_condition(request.body)
if !kc_ok {
@@ -425,45 +415,35 @@ handle_query :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, r
limit = 100
}
// TODO: Parse ExclusiveStartKey properly (requires metadata for type info)
exclusive_start_key: Maybe([]byte) = nil
result, err := dynamodb.query(engine, table_name, pk_owned, exclusive_start_key, limit)
if err != .None {
#partial switch err {
case .Table_Not_Found:
make_error_response(response, .ResourceNotFoundException, "Table not found")
case:
make_error_response(response, .InternalServerError, "Query failed")
// ---- Parse ExclusiveStartKey with proper type handling ----
exclusive_start_key, esk_ok := dynamodb.parse_exclusive_start_key(
request.body, table_name, metadata.key_schema,
)
if !esk_ok {
make_error_response(response, .ValidationException, "Invalid ExclusiveStartKey")
return
}
defer {
if esk, has_esk := exclusive_start_key.?; has_esk {
delete(esk)
}
}
// ---- Pass sort key condition through to storage layer ----
sk_condition: Maybe(dynamodb.Sort_Key_Condition) = nil
if skc, has_skc := kc.sk_condition.?; has_skc {
sk_condition = skc
}
result, err := dynamodb.query(engine, table_name, pk_owned, exclusive_start_key, limit, sk_condition)
if err != .None {
handle_storage_error(response, err)
return
}
defer dynamodb.query_result_destroy(&result)
// Build response
builder := strings.builder_make()
strings.write_string(&builder, `{"Items":[`)
for item, i in result.items {
if i > 0 do strings.write_string(&builder, ",")
item_json := dynamodb.serialize_item(item)
strings.write_string(&builder, item_json)
}
strings.write_string(&builder, `],"Count":`)
fmt.sbprintf(&builder, "%d", len(result.items))
strings.write_string(&builder, `,"ScannedCount":`)
fmt.sbprintf(&builder, "%d", len(result.items))
// TODO: Add LastEvaluatedKey when pagination is fully wired
if last_key, has_last := result.last_evaluated_key.?; has_last {
_ = last_key
}
strings.write_string(&builder, "}")
resp_body := strings.to_string(builder)
response_set_body(response, transmute([]byte)resp_body)
// Build response with proper pagination
write_items_response_with_pagination(response, result.items, result.last_evaluated_key, &metadata)
}
handle_scan :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
@@ -473,52 +453,84 @@ handle_scan :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, re
return
}
// ---- Fetch table metadata early so we can parse ExclusiveStartKey ----
metadata, meta_err := dynamodb.get_table_metadata(engine, table_name)
if meta_err != .None {
handle_storage_error(response, meta_err)
return
}
defer dynamodb.table_metadata_destroy(&metadata, engine.allocator)
// Parse Limit (default to 100 if not specified)
limit := dynamodb.parse_limit(request.body)
if limit == 0 {
limit = 100
}
// Parse ExclusiveStartKey if present
// For now, we'll implement basic scan without ExclusiveStartKey parsing
// TODO: Parse ExclusiveStartKey from request body and convert to binary key
exclusive_start_key: Maybe([]byte) = nil
// ---- Parse ExclusiveStartKey with proper type handling ----
exclusive_start_key, esk_ok := dynamodb.parse_exclusive_start_key(
request.body, table_name, metadata.key_schema,
)
if !esk_ok {
make_error_response(response, .ValidationException, "Invalid ExclusiveStartKey")
return
}
defer {
if esk, has_esk := exclusive_start_key.?; has_esk {
delete(esk)
}
}
// Perform scan
result, err := dynamodb.scan(engine, table_name, exclusive_start_key, limit)
if err != .None {
#partial switch err {
case .Table_Not_Found:
make_error_response(response, .ResourceNotFoundException, "Table not found")
case:
make_error_response(response, .InternalServerError, "Failed to scan table")
}
handle_storage_error(response, err)
return
}
defer dynamodb.scan_result_destroy(&result)
// Build response
// Build response with proper pagination
write_items_response_with_pagination(response, result.items, result.last_evaluated_key, &metadata)
}
// ============================================================================
// Shared Pagination Response Builder
//
// Mirrors the Zig writeItemsResponseWithPagination helper:
// - Serializes Items array
// - Emits Count / ScannedCount
// - Decodes binary last_evaluated_key → DynamoDB JSON LastEvaluatedKey
// ============================================================================
write_items_response_with_pagination :: proc(
response: ^HTTP_Response,
items: []dynamodb.Item,
last_evaluated_key_binary: Maybe([]byte),
metadata: ^dynamodb.Table_Metadata,
) {
builder := strings.builder_make()
strings.write_string(&builder, `{"Items":[`)
for item, i in result.items {
for item, i in items {
if i > 0 do strings.write_string(&builder, ",")
item_json := dynamodb.serialize_item(item)
strings.write_string(&builder, item_json)
}
strings.write_string(&builder, `],"Count":`)
fmt.sbprintf(&builder, "%d", len(result.items))
fmt.sbprintf(&builder, "%d", len(items))
strings.write_string(&builder, `,"ScannedCount":`)
fmt.sbprintf(&builder, "%d", len(result.items))
fmt.sbprintf(&builder, "%d", len(items))
// Add LastEvaluatedKey if present (pagination)
if last_key, has_last := result.last_evaluated_key.?; has_last {
// TODO: Convert binary key back to DynamoDB JSON format
// For now, we'll just include it as base64 (not DynamoDB-compatible yet)
_ = last_key
// When fully implemented, this should decode the key and serialize as:
// ,"LastEvaluatedKey":{"pk":{"S":"value"},"sk":{"N":"123"}}
// Emit LastEvaluatedKey if the storage layer produced one
if binary_key, has_last := last_evaluated_key_binary.?; has_last {
lek_json, lek_ok := dynamodb.serialize_last_evaluated_key(binary_key, metadata)
if lek_ok {
strings.write_string(&builder, `,"LastEvaluatedKey":`)
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, "}")
@@ -527,6 +539,36 @@ handle_scan :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, re
response_set_body(response, transmute([]byte)resp_body)
}
// ============================================================================
// Centralized Storage Error → DynamoDB Error mapping
//
// Maps storage errors to the correct DynamoDB error type AND HTTP status code.
// DynamoDB uses:
// 400 — ValidationException, ResourceNotFoundException, ResourceInUseException, etc.
// 500 — InternalServerError
// ============================================================================
handle_storage_error :: proc(response: ^HTTP_Response, err: dynamodb.Storage_Error) {
#partial switch err {
case .Table_Not_Found:
make_error_response(response, .ResourceNotFoundException, "Requested resource not found")
case .Table_Already_Exists:
make_error_response(response, .ResourceInUseException, "Table already exists")
case .Missing_Key_Attribute:
make_error_response(response, .ValidationException, "One or more required key attributes are missing")
case .Invalid_Key:
make_error_response(response, .ValidationException, "Invalid key: type mismatch or malformed key value")
case .Serialization_Error:
make_error_response(response, .InternalServerError, "Internal serialization error")
case .RocksDB_Error:
make_error_response(response, .InternalServerError, "Internal storage error")
case .Out_Of_Memory:
make_error_response(response, .InternalServerError, "Internal memory error")
case:
make_error_response(response, .InternalServerError, "Unexpected error")
}
}
// ============================================================================
// Schema Parsing Helpers
// ============================================================================
@@ -560,7 +602,6 @@ parse_key_schema :: proc(root: json.Object) -> ([]dynamodb.Key_Schema_Element, K
for elem, i in key_schema_array {
elem_obj, elem_ok := elem.(json.Object)
if !elem_ok {
// Cleanup
for j in 0..<i {
delete(key_schema[j].attribute_name)
}
@@ -702,8 +743,8 @@ parse_attribute_definitions :: proc(root: json.Object) -> ([]dynamodb.Attribute_
}
// Get AttributeName
attr_name_val, name_found := elem_obj["AttributeName"]
if !name_found {
attr_name_val, attr_found := elem_obj["AttributeName"]
if !attr_found {
for j in 0..<i {
delete(attr_defs[j].attribute_name)
}
@@ -794,10 +835,24 @@ validate_key_attributes_defined :: proc(key_schema: []dynamodb.Key_Schema_Elemen
// ============================================================================
// Error Response Helper
//
// Maps DynamoDB error types to correct HTTP status codes:
// 400 — ValidationException, ResourceNotFoundException, ResourceInUseException,
// ConditionalCheckFailedException, SerializationException
// 500 — InternalServerError
// ============================================================================
make_error_response :: proc(response: ^HTTP_Response, err_type: dynamodb.DynamoDB_Error_Type, message: string) -> HTTP_Response {
response_set_status(response, .Bad_Request)
status: HTTP_Status
#partial switch err_type {
case .InternalServerError:
status = .Internal_Server_Error
case:
status = .Bad_Request
}
response_set_status(response, status)
error_body := dynamodb.error_to_response(err_type, message)
response_set_body(response, transmute([]byte)error_body)
return response^