diff --git a/dynamodb/json.odin b/dynamodb/json.odin index ad46044..ac3ff10 100644 --- a/dynamodb/json.odin +++ b/dynamodb/json.odin @@ -322,17 +322,25 @@ serialize_item_to_builder :: proc(b: ^strings.Builder, item: Item) { serialize_attribute_value :: proc(b: ^strings.Builder, attr: Attribute_Value) { switch v in attr { case String: - fmt.sbprintf(b, `{"S":"%s"}`, string(v)) + strings.write_string(b, `{"S":"`) + strings.write_string(b, string(v)) + strings.write_string(b, `"}`) case DDB_Number: num_str := format_ddb_number(v) - fmt.sbprintf(b, `{"N":"%s"}`, num_str) + strings.write_string(b, `{"N":"`) + strings.write_string(b, num_str) + strings.write_string(b, `"}`) case Binary: - fmt.sbprintf(b, `{"B":"%s"}`, string(v)) + strings.write_string(b, `{"B":"`) + strings.write_string(b, string(v)) + strings.write_string(b, `"}`) case Bool: - fmt.sbprintf(b, `{"BOOL":%v}`, bool(v)) + strings.write_string(b, `{"BOOL":`) + if bool(v) { strings.write_string(b, "true") } else { strings.write_string(b, "false") } + strings.write_string(b, "}") case Null: strings.write_string(b, `{"NULL":true}`) diff --git a/dynamodb/storage.odin b/dynamodb/storage.odin index 3907cf7..3e95293 100644 --- a/dynamodb/storage.odin +++ b/dynamodb/storage.odin @@ -532,6 +532,10 @@ get_table_metadata :: proc(engine: ^Storage_Engine, table_name: string) -> (Tabl return {}, .Serialization_Error } + // table_name is not stored in the serialized blob (it IS the RocksDB key), + // so we populate it here from the argument we already have. + metadata.table_name = strings.clone(table_name, engine.allocator) + return metadata, .None } diff --git a/dynamodb/transact.odin b/dynamodb/transact.odin index efa0632..8a907d9 100644 --- a/dynamodb/transact.odin +++ b/dynamodb/transact.odin @@ -532,9 +532,9 @@ update_item_batch :: proc( } defer item_destroy(&existing_item) - // Apply update plan - if !execute_update_plan(&existing_item, plan) { - return .Invalid_Key + // Apply update plan. + if exec_err := execute_update_plan(&existing_item, plan); exec_err != .None { + return .Validation_Error } // Encode updated item diff --git a/dynamodb/update.odin b/dynamodb/update.odin index 6b1d761..b05951b 100644 --- a/dynamodb/update.odin +++ b/dynamodb/update.odin @@ -594,7 +594,23 @@ is_clause_keyword :: proc(tok: string) -> bool { // Execute Update Plan — apply mutations to an Item (in-place) // ============================================================================ -execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { +// Reasons an update plan can fail at execution time. +// All of these map to ValidationException at the HTTP layer. +Update_Exec_Error :: enum { + None, + // SET x = source +/- val: source attribute does not exist in the item + Operand_Not_Found, + // SET x = source +/- val: source or value attribute is not a Number + Operand_Not_Number, + // SET x = list_append(source, val): source attribute is not a List + Operand_Not_List, + // ADD path val: existing attribute is not a Number, String_Set, or Number_Set + Add_Type_Mismatch, + // ADD path val: value type does not match the existing set type + Add_Value_Type_Mismatch, +} + +execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> Update_Exec_Error { // Execute SET actions for &action in plan.sets { switch action.value_kind { @@ -613,11 +629,11 @@ execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { if src, found := item[action.source]; found { existing = src } else { - return false // source attribute not found + return .Operand_Not_Found } result, add_ok := numeric_add(existing, action.value) if !add_ok { - return false + return .Operand_Not_Number } if old, found := item[action.path]; found { old_copy := old @@ -632,11 +648,11 @@ execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { if src, found := item[action.source]; found { existing = src } else { - return false + return .Operand_Not_Found } result, sub_ok := numeric_subtract(existing, action.value) if !sub_ok { - return false + return .Operand_Not_Number } if old, found := item[action.path]; found { old_copy := old @@ -664,7 +680,7 @@ execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { if l, is_list := src.(List); is_list { existing_list = ([]Attribute_Value)(l) } else { - return false + return .Operand_Not_List } } else { existing_list = {} @@ -674,7 +690,7 @@ execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { if l, is_list := action.value.(List); is_list { append_list = ([]Attribute_Value)(l) } else { - return false + return .Operand_Not_List } new_list := make([]Attribute_Value, len(existing_list) + len(append_list)) @@ -711,7 +727,7 @@ execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { case DDB_Number: result, add_ok := numeric_add(existing, action.value) if !add_ok { - return false + return .Operand_Not_Number } old_copy := existing attr_value_destroy(&old_copy) @@ -727,7 +743,7 @@ execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { delete_key(item, action.path) item[strings.clone(action.path)] = String_Set(merged) } else { - return false + return .Add_Value_Type_Mismatch } case DDB_Number_Set: @@ -738,11 +754,11 @@ execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { delete_key(item, action.path) item[strings.clone(action.path)] = DDB_Number_Set(merged) } else { - return false + return .Add_Value_Type_Mismatch } case: - return false + return .Add_Type_Mismatch } } else { // Attribute doesn't exist — create it @@ -786,7 +802,7 @@ execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool { } } - return true + return .None } // ============================================================================ diff --git a/dynamodb/update_item.odin b/dynamodb/update_item.odin index 987272c..55d3c74 100644 --- a/dynamodb/update_item.odin +++ b/dynamodb/update_item.odin @@ -73,14 +73,14 @@ update_item :: proc( return nil, nil, .RocksDB_Error } - // Apply update plan - if !execute_update_plan(&existing_item, plan) { + // Apply update plan. + if exec_err := execute_update_plan(&existing_item, plan); exec_err != .None { item_destroy(&existing_item) if old, has := old_item.?; has { old_copy := old item_destroy(&old_copy) } - return nil, nil, .Invalid_Key + return nil, nil, .Validation_Error } // Validate key attributes are still present and correct type diff --git a/main.odin b/main.odin index 7613d15..80c22df 100644 --- a/main.odin +++ b/main.odin @@ -219,6 +219,7 @@ handle_delete_table :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Req make_error_response(response, .ValidationException, "Invalid request or missing TableName") return } + defer delete(table_name) err := dynamodb.delete_table(engine, table_name) if err != .None { @@ -267,16 +268,22 @@ handle_describe_table :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R for ks, i in metadata.key_schema { if i > 0 do strings.write_string(&builder, ",") - fmt.sbprintf(&builder, `{"AttributeName":"%s","KeyType":"%s"}`, - ks.attribute_name, dynamodb.key_type_to_string(ks.key_type)) + strings.write_string(&builder, `{"AttributeName":"`) + strings.write_string(&builder, ks.attribute_name) + strings.write_string(&builder, `","KeyType":"`) + strings.write_string(&builder, dynamodb.key_type_to_string(ks.key_type)) + strings.write_string(&builder, `"}`) } strings.write_string(&builder, `],"AttributeDefinitions":[`) for ad, i in metadata.attribute_definitions { if i > 0 do strings.write_string(&builder, ",") - fmt.sbprintf(&builder, `{"AttributeName":"%s","AttributeType":"%s"}`, - ad.attribute_name, dynamodb.scalar_type_to_string(ad.attribute_type)) + strings.write_string(&builder, `{"AttributeName":"`) + strings.write_string(&builder, ad.attribute_name) + strings.write_string(&builder, `","AttributeType":"`) + strings.write_string(&builder, dynamodb.scalar_type_to_string(ad.attribute_type)) + strings.write_string(&builder, `"}`) } strings.write_string(&builder, `]`) @@ -291,8 +298,11 @@ handle_describe_table :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R strings.write_string(&builder, `","KeySchema":[`) for ks, ki in gsi.key_schema { if ki > 0 do strings.write_string(&builder, ",") - fmt.sbprintf(&builder, `{"AttributeName":"%s","KeyType":"%s"}`, - ks.attribute_name, dynamodb.key_type_to_string(ks.key_type)) + strings.write_string(&builder, `{"AttributeName":"`) + strings.write_string(&builder, ks.attribute_name) + strings.write_string(&builder, `","KeyType":"`) + strings.write_string(&builder, dynamodb.key_type_to_string(ks.key_type)) + strings.write_string(&builder, `"}`) } strings.write_string(&builder, `],"Projection":{"ProjectionType":"`) strings.write_string(&builder, projection_type_to_string(gsi.projection.projection_type)) @@ -455,6 +465,7 @@ handle_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request make_error_response(response, .ValidationException, "Invalid request or missing TableName") return } + defer delete(table_name) key, key_ok := dynamodb.parse_key_from_request(request.body) if !key_ok { @@ -493,6 +504,7 @@ handle_delete_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Requ make_error_response(response, .ValidationException, "Invalid request or missing TableName") return } + defer delete(table_name) key, key_ok := dynamodb.parse_key_from_request(request.body) if !key_ok { @@ -939,9 +951,13 @@ handle_batch_write_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP item_json := dynamodb.serialize_item(req.item) switch req.type { case .Put: - fmt.sbprintf(&builder, `{"PutRequest":{"Item":%s}}`, item_json) + strings.write_string(&builder, `{"PutRequest":{"Item":`) + strings.write_string(&builder, item_json) + strings.write_string(&builder, "}}") case .Delete: - fmt.sbprintf(&builder, `{"DeleteRequest":{"Key":%s}}`, item_json) + strings.write_string(&builder, `{"DeleteRequest":{"Key":`) + strings.write_string(&builder, item_json) + strings.write_string(&builder, "}}") } } @@ -1086,7 +1102,9 @@ handle_batch_get_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R if ti > 0 { strings.write_string(&builder, ",") } - fmt.sbprintf(&builder, `"%s":{"Keys":[`, table_req.table_name) + strings.write_string(&builder, `"`) + strings.write_string(&builder, table_req.table_name) + strings.write_string(&builder, `":{"Keys":["`) for key, ki in table_req.keys { if ki > 0 { @@ -1650,6 +1668,8 @@ handle_storage_error :: proc(response: ^HTTP_Response, err: dynamodb.Storage_Err 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 .Validation_Error: + make_error_response(response, .ValidationException, "Invalid request: type mismatch or incompatible operand") case .Serialization_Error: make_error_response(response, .InternalServerError, "Internal serialization error") case .RocksDB_Error: