fix storage issues
This commit is contained in:
243
main.odin
243
main.odin
@@ -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^
|
||||
|
||||
Reference in New Issue
Block a user