Repository: gofr-dev/gofr

Issue:
Surreal Update Method not working
**Describe the bug**
SurrealDb's Update method not working as expected.

https://github.com/gofr-dev/gofr/blob/development/pkg/gofr/datasource/surrealdb/surrealdb.go#L552





Discussion:

Solution patch:
diff --git a/pkg/gofr/datasource/surrealdb/surrealdb.go b/pkg/gofr/datasource/surrealdb/surrealdb.go
index 83237415..2f8a3718 100644
--- a/pkg/gofr/datasource/surrealdb/surrealdb.go
+++ b/pkg/gofr/datasource/surrealdb/surrealdb.go
@@ -31,6 +31,8 @@ var (
 	errNoResult                 = errors.New("no result found in query response")
 	errInvalidResult            = errors.New("unexpected result format: expected []any")
 	errUnexpectedResult         = errors.New("unexpected result type: expected []any")
+	errSurrealDBUpdate          = errors.New("surrealdb update operation failed")
+	errQueryFailed              = errors.New("query failed with unexpected status")
 )
 
 const (
@@ -515,17 +517,17 @@ func (c *Client) Create(ctx context.Context, table string, data any) (map[string
 	return c.extractRecord(res.Result)
 }
 
-// Update modifies an existing record in the specified table.
+// Update modifies an existing record in the specified table using a generic MERGE update.
+// It constructs an update query with a RETURN * clause so that the updated record is returned.
 func (c *Client) Update(ctx context.Context, table, id string, data any) (any, error) {
 	if c.db == nil {
 		return nil, errNotConnected
 	}
 
-	query := fmt.Sprintf("UPDATE %s SET", table)
-	span := c.addTrace(ctx, "Update", query)
+	updateQuery := fmt.Sprintf("UPDATE %s:%s MERGE $data RETURN *", table, id)
+	span := c.addTrace(ctx, "Update", fmt.Sprintf("%s:%s", table, id))
 
 	logMessage := fmt.Sprintf("Updating record with ID %q in table %q", id, table)
-
 	startTime := time.Now()
 	defer c.sendOperationStats(&QueryLog{
 		Query:         logMessage,
@@ -534,35 +536,100 @@ func (c *Client) Update(ctx context.Context, table, id string, data any) (any, e
 		Database:      c.config.Database,
 		Collection:    table,
 		Data:          data,
-		Update:        data,
 		Span:          span,
 	}, startTime)
 
-	dataMap := data.(map[string]any)
-
 	var updateResult DBResponse
+	if err := c.db.Send(&updateResult, "query", updateQuery, map[string]any{"data": data}); err != nil {
+		return nil, fmt.Errorf("update operation failed: %w", err)
+	}
+
+	if updateResult.Error != nil {
+		return nil, fmt.Errorf("%w: %s", errSurrealDBUpdate, updateResult.Error.Message)
+	}
+
+	// Handle the nested response structure
+	return c.processUpdateResult(updateResult.Result)
+}
+
+// processUpdateResult handles the processing of update operation results.
+func (c *Client) processUpdateResult(result any) (any, error) {
+	resultSlice, err := validateResultSlice(result)
+	if err != nil {
+		return nil, err
+	}
 
-	updateQuery := fmt.Sprintf(`
-        UPDATE %s:%s SET 
-        name = $name, 
-        age = $age, 
-        email = $email
-        RETURN *`, table, id)
-
-	if err := c.db.Send(&updateResult, "query", updateQuery, map[string]any{
-		"name":  dataMap["name"],
-		"age":   dataMap["age"],
-		"email": dataMap["email"],
-	}); err != nil {
+	responseItem, err := validateResponseItem(resultSlice[0])
+	if err != nil {
+		return nil, err
+	}
+
+	resultData, err := validateAndExtractResult(responseItem)
+	if err != nil {
 		return nil, err
 	}
 
-	resultSlice, ok := updateResult.Result.([]any)
-	if !ok || len(resultSlice) == 0 {
+	return c.processResultData(resultData)
+}
+
+// validateResultSlice validates the initial result slice.
+func validateResultSlice(result any) ([]any, error) {
+	resultSlice, ok := result.([]any)
+	if !ok {
+		return nil, fmt.Errorf("%w: expected []any, got %T", errUnexpectedResult, result)
+	}
+
+	if len(resultSlice) == 0 {
 		return nil, errNoRecord
 	}
 
-	return resultSlice[0], nil
+	return resultSlice, nil
+}
+
+// validateResponseItem validates the response item and converts it to a string map.
+func validateResponseItem(item any) (map[string]any, error) {
+	responseItem, ok := item.(map[any]any)
+	if !ok {
+		return nil, fmt.Errorf("%w: expected map[any]any in response", errUnexpectedResult)
+	}
+
+	response := make(map[string]any)
+
+	for k, v := range responseItem {
+		if keyStr, ok := k.(string); ok {
+			response[keyStr] = v
+		}
+	}
+
+	if status, ok := response["status"].(string); !ok || status != statusOK {
+		return nil, fmt.Errorf("%w: %v", errQueryFailed, response["status"])
+	}
+
+	return response, nil
+}
+
+// validateAndExtractResult validates and extracts the result data.
+func validateAndExtractResult(response map[string]any) (any, error) {
+	resultData, exists := response["result"]
+	if !exists {
+		return nil, errNoRecord
+	}
+
+	return resultData, nil
+}
+
+// processResultData handles both single record and array responses.
+func (c *Client) processResultData(resultData any) (any, error) {
+	switch data := resultData.(type) {
+	case []any:
+		if len(data) == 0 {
+			return nil, errNoRecord
+		}
+
+		return c.extractRecord(data[0])
+	default:
+		return c.extractRecord(data)
+	}
 }
 
 // Insert inserts a new record into the specified table in SurrealDB.



Test patch:
diff --git a/pkg/gofr/datasource/surrealdb/surrealdb_test.go b/pkg/gofr/datasource/surrealdb/surrealdb_test.go
index d1ff849d..a87b0d78 100644
--- a/pkg/gofr/datasource/surrealdb/surrealdb_test.go
+++ b/pkg/gofr/datasource/surrealdb/surrealdb_test.go
@@ -3,6 +3,7 @@ package surrealdb
 import (
 	"context"
 	"errors"
+	"fmt"
 	"math"
 	"testing"
 
@@ -274,20 +275,22 @@ func Test_Update(t *testing.T) {
 			"email": "test@example.com",
 		}
 
-		expectedQuery := `
-        UPDATE users:123 SET 
-        name = $name, 
-        age = $age, 
-        email = $email
-        RETURN *`
+		expectedQuery := fmt.Sprintf("UPDATE %s:%s MERGE $data RETURN *", "users", "123")
+
+		queryVars := map[string]any{
+			"data": data,
+		}
 
 		updateResponse := DBResponse{
 			Result: []any{
 				map[any]any{
-					"id":    "user:123",
-					"name":  "updated",
-					"age":   25,
-					"email": "test@example.com",
+					"status": statusOK,
+					"result": map[any]any{
+						"id":    "user:123",
+						"name":  "updated",
+						"age":   25,
+						"email": "test@example.com",
+					},
 				},
 			},
 		}
@@ -297,14 +300,14 @@ func Test_Update(t *testing.T) {
 		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1))
 
 		mockConn.EXPECT().
-			Send(gomock.Any(), "query", expectedQuery, data).
+			Send(gomock.Any(), "query", expectedQuery, queryVars).
 			Return(nil).
 			SetArg(0, updateResponse)
 
 		result, err := client.Update(ctx, "users", "123", data)
 		require.NoError(t, err)
 
-		resultMap, ok := result.(map[any]any)
+		resultMap, ok := result.(map[string]any)
 		require.True(t, ok)
 		assert.Equal(t, "updated", resultMap["name"])
 		assert.Equal(t, 25, resultMap["age"])
@@ -428,6 +431,8 @@ func Test_Insert(t *testing.T) {
 		}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any())
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1))
 		mockConn.EXPECT().
 			Send(gomock.Any(), "insert", "users", data).
 			Return(nil).
@@ -462,6 +467,8 @@ func Test_Insert(t *testing.T) {
 		}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any())
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1))
 		mockConn.EXPECT().
 			Send(gomock.Any(), "insert", "users", data).
 			Return(nil).
@@ -504,6 +511,8 @@ func Test_Delete(t *testing.T) {
 		}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any())
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1))
 		mockConn.EXPECT().
 			Send(gomock.Any(), "query", expectedQuery, nil).
 			Return(nil).
@@ -540,6 +549,8 @@ func Test_Delete(t *testing.T) {
 		}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any())
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1))
 		mockConn.EXPECT().
 			Send(gomock.Any(), "query", expectedQuery, nil).
 			Return(nil).
@@ -642,10 +653,18 @@ func Test_executeQuery(t *testing.T) {
 		ctx := context.Background()
 		queryResult := []QueryResult{{
 			Status: statusOK,
-			Result: []any{},
+			Result: []any{
+				map[any]any{
+					"id":    "test:1",
+					"name":  "test",
+					"email": "test@example.com",
+				},
+			},
 		}}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1)).MaxTimes(2)
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any()).MaxTimes(2)
 		mockConn.EXPECT().
 			Send(gomock.Any(), "query", "TEST QUERY", nil).
 			Return(nil).
@@ -671,6 +690,8 @@ func Test_executeQuery(t *testing.T) {
 		ctx := context.Background()
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1)).MaxTimes(2)
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any()).MaxTimes(2)
 		mockConn.EXPECT().
 			Send(gomock.Any(), "query", "TEST QUERY", nil).
 			Return(errorDatabase)
@@ -700,10 +721,18 @@ func Test_NamespaceOperations(t *testing.T) {
 		ctx := context.Background()
 		queryResult := []QueryResult{{
 			Status: statusOK,
-			Result: []any{},
+			Result: []any{
+				map[any]any{
+					"id":    "test:1",
+					"name":  "test",
+					"email": "test@example.com",
+				},
+			},
 		}}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1)).MaxTimes(2)
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any()).MaxTimes(2)
 		mockConn.EXPECT().
 			Send(gomock.Any(), "query", "DEFINE NAMESPACE test_namespace;", nil).
 			Return(nil).
@@ -721,6 +750,8 @@ func Test_NamespaceOperations(t *testing.T) {
 		}}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1)).MaxTimes(2)
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any()).MaxTimes(2)
 		mockConn.EXPECT().
 			Send(gomock.Any(), "query", "REMOVE NAMESPACE test_namespace;", nil).
 			Return(nil).
@@ -751,10 +782,18 @@ func Test_DatabaseOperations(t *testing.T) {
 		ctx := context.Background()
 		queryResult := []QueryResult{{
 			Status: statusOK,
-			Result: []any{},
+			Result: []any{
+				map[any]any{
+					"id":    "test:1",
+					"name":  "test",
+					"email": "test@example.com",
+				},
+			},
 		}}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1)).MaxTimes(2)
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any()).MaxTimes(2)
 		mockConn.EXPECT().
 			Send(gomock.Any(), "query", "DEFINE DATABASE test_database;", nil).
 			Return(nil).
@@ -772,6 +811,8 @@ func Test_DatabaseOperations(t *testing.T) {
 		}}
 
 		mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
+		mockMetrics.EXPECT().SetGauge("app_surrealdb_open_connections", float64(1)).MaxTimes(2)
+		mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_surrealdb_stats", gomock.Any(), gomock.Any()).MaxTimes(2)
 		mockConn.EXPECT().
 			Send(gomock.Any(), "query", "REMOVE DATABASE test_database;", nil).
 			Return(nil).