Compare commits

..

2 Commits

Author SHA1 Message Date
Hein
987a2a7faf fix(db): convert slices to PostgreSQL array literals in queries
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -32m17s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -31m49s
Build , Vet Test, and Lint / Build (push) Successful in -31m53s
Build , Vet Test, and Lint / Lint Code (push) Successful in -31m11s
Tests / Unit Tests (push) Successful in -32m31s
Tests / Integration Tests (push) Failing after -32m46s
2026-05-07 14:33:35 +02:00
157788b73b fix(todo): document issue with GormResult.LastInsertId() not returning correct ID
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -32m18s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -31m19s
Build , Vet Test, and Lint / Lint Code (push) Successful in -31m5s
Build , Vet Test, and Lint / Build (push) Successful in -31m53s
Tests / Integration Tests (push) Failing after -32m52s
Tests / Unit Tests (push) Successful in -32m36s
2026-05-05 09:52:31 +02:00
6 changed files with 115 additions and 5 deletions

View File

@@ -1529,7 +1529,7 @@ func (b *BunUpdateQuery) SetMap(values map[string]interface{}) common.UpdateQuer
// Skip primary key updates
continue
}
b.query = b.query.Set(column+" = ?", value)
b.query = b.query.Set(column+" = ?", common.ConvertSliceForBun(value))
}
return b
}

View File

@@ -3,6 +3,7 @@ package common
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/bitechdev/ResolveSpec/pkg/logger"
@@ -261,3 +262,48 @@ func GetTableNameFromModel(model interface{}) string {
// This handles cases like "MasterTaskItem" -> "mastertaskitem"
return strings.ToLower(modelType.Name())
}
// ConvertSliceForBun converts []interface{} values to PostgreSQL array literal strings.
// BUN's fallback appender for []interface{} is JSON encoding, which produces "[]" —
// invalid PostgreSQL array syntax. PostgreSQL expects "{}" for empty arrays and
// "{elem1,elem2}" for non-empty ones. All other value types are returned unchanged.
func ConvertSliceForBun(value interface{}) interface{} {
arr, ok := value.([]interface{})
if !ok {
return value
}
if len(arr) == 0 {
return "{}"
}
parts := make([]string, len(arr))
for i, elem := range arr {
switch e := elem.(type) {
case string:
needsQuote := e == "" || strings.ContainsAny(e, `,"\\{}`+"\t\n\r ")
if needsQuote {
e = strings.ReplaceAll(e, `\`, `\\`)
e = strings.ReplaceAll(e, `"`, `""`)
parts[i] = `"` + e + `"`
} else {
parts[i] = e
}
case float64:
if e == float64(int64(e)) {
parts[i] = strconv.FormatInt(int64(e), 10)
} else {
parts[i] = strconv.FormatFloat(e, 'f', -1, 64)
}
case bool:
if e {
parts[i] = "t"
} else {
parts[i] = "f"
}
case nil:
parts[i] = "NULL"
default:
parts[i] = fmt.Sprintf("%v", e)
}
}
return "{" + strings.Join(parts, ",") + "}"
}

View File

@@ -106,3 +106,66 @@ func TestExtractTagValue(t *testing.T) {
})
}
}
func TestConvertSliceForBun(t *testing.T) {
tests := []struct {
name string
input interface{}
expected interface{}
}{
{
name: "empty slice produces empty pg array",
input: []interface{}{},
expected: "{}",
},
{
name: "string elements",
input: []interface{}{"a", "b", "c"},
expected: "{a,b,c}",
},
{
name: "string element needing quotes",
input: []interface{}{"hello world", "ok"},
expected: `{"hello world",ok}`,
},
{
name: "string with comma",
input: []interface{}{"a,b"},
expected: `{"a,b"}`,
},
{
name: "integer elements (JSON float64)",
input: []interface{}{float64(1), float64(2), float64(3)},
expected: "{1,2,3}",
},
{
name: "bool elements",
input: []interface{}{true, false},
expected: "{t,f}",
},
{
name: "nil input passthrough",
input: nil,
expected: nil,
},
{
name: "string input passthrough",
input: "hello",
expected: "hello",
},
{
name: "int input passthrough",
input: 42,
expected: 42,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ConvertSliceForBun(tt.input)
if result != tt.expected {
t.Errorf("ConvertSliceForBun(%v) = %v; want %v", tt.input, result, tt.expected)
}
})
}
}

View File

@@ -269,7 +269,7 @@ func (p *NestedCUDProcessor) processInsert(
query := p.db.NewInsert().Table(tableName)
for key, value := range data {
query = query.Value(key, value)
query = query.Value(key, ConvertSliceForBun(value))
}
pkName := reflection.GetPrimaryKeyName(tableName)
// Add RETURNING clause to get the inserted ID

View File

@@ -603,7 +603,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
// Standard processing without nested relations
query := h.db.NewInsert().Table(tableName)
for key, value := range v {
query = query.Value(key, value)
query = query.Value(key, common.ConvertSliceForBun(value))
}
result, err := query.Exec(ctx)
if err != nil {
@@ -669,7 +669,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
for _, item := range v {
txQuery := tx.NewInsert().Table(tableName)
for key, value := range item {
txQuery = txQuery.Value(key, value)
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
}
if _, err := txQuery.Exec(ctx); err != nil {
return err
@@ -747,7 +747,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
if itemMap, ok := item.(map[string]interface{}); ok {
txQuery := tx.NewInsert().Table(tableName)
for key, value := range itemMap {
txQuery = txQuery.Value(key, value)
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
}
if _, err := txQuery.Exec(ctx); err != nil {
return err

View File

@@ -92,6 +92,7 @@ See [`resolvespec-python/todo.md`](./resolvespec-python/todo.md) for detailed Py
- [ ] Long preload alias names may exceed PostgreSQL identifier limit
- [ ] Some edge cases in computed column handling
- [ ] `GormResult.LastInsertId()` (`pkg/common/adapters/database/gorm.go:936`) always returns `0, nil` — GORM does not expose last insert ID via `sql.Result` for most dialects. Auto-generated IDs from GORM inserts are not propagated back through `LastInsertId`, which breaks the ID-retrieval path in `recursive_crud.go`. Fix: read the ID back from the model struct after `Create()` using reflection, or use GORM's `Statement.LastInsertId`.
---