mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-05-08 12:55:11 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
987a2a7faf | ||
| 157788b73b | |||
|
|
fb051b5577 | ||
|
|
cc9c4337fd | ||
|
|
0aaeff63a2 | ||
|
|
325769be4e |
@@ -597,6 +597,19 @@ func (b *BunSelectQuery) PreloadRelation(relation string, apply ...func(common.S
|
|||||||
if !b.skipAutoDetect {
|
if !b.skipAutoDetect {
|
||||||
model := b.query.GetModel()
|
model := b.query.GetModel()
|
||||||
if model != nil && model.Value() != nil {
|
if model != nil && model.Value() != nil {
|
||||||
|
// Guard against relations that don't exist on the model. Without this,
|
||||||
|
// bun panics inside Count/Scan with `model=X does not have relation="Y"`.
|
||||||
|
// Only validate the root segment so nested paths (e.g. "PRM.CHILD") still
|
||||||
|
// fall through to bun's native resolution.
|
||||||
|
rootRelation := relation
|
||||||
|
if idx := strings.Index(rootRelation, "."); idx >= 0 {
|
||||||
|
rootRelation = rootRelation[:idx]
|
||||||
|
}
|
||||||
|
if reflection.GetRelationType(model.Value(), rootRelation) == reflection.RelationUnknown {
|
||||||
|
logger.Warn("Skipping preload '%s': relation '%s' is not declared on model %T", relation, rootRelation, model.Value())
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
relType := reflection.GetRelationType(model.Value(), relation)
|
relType := reflection.GetRelationType(model.Value(), relation)
|
||||||
|
|
||||||
// Log the detected relationship type
|
// Log the detected relationship type
|
||||||
@@ -1516,7 +1529,7 @@ func (b *BunUpdateQuery) SetMap(values map[string]interface{}) common.UpdateQuer
|
|||||||
// Skip primary key updates
|
// Skip primary key updates
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b.query = b.query.Set(column+" = ?", value)
|
b.query = b.query.Set(column+" = ?", common.ConvertSliceForBun(value))
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package common
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
@@ -261,3 +262,48 @@ func GetTableNameFromModel(model interface{}) string {
|
|||||||
// This handles cases like "MasterTaskItem" -> "mastertaskitem"
|
// This handles cases like "MasterTaskItem" -> "mastertaskitem"
|
||||||
return strings.ToLower(modelType.Name())
|
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, ",") + "}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ func (p *NestedCUDProcessor) processInsert(
|
|||||||
query := p.db.NewInsert().Table(tableName)
|
query := p.db.NewInsert().Table(tableName)
|
||||||
|
|
||||||
for key, value := range data {
|
for key, value := range data {
|
||||||
query = query.Value(key, value)
|
query = query.Value(key, ConvertSliceForBun(value))
|
||||||
}
|
}
|
||||||
pkName := reflection.GetPrimaryKeyName(tableName)
|
pkName := reflection.GetPrimaryKeyName(tableName)
|
||||||
// Add RETURNING clause to get the inserted ID
|
// Add RETURNING clause to get the inserted ID
|
||||||
|
|||||||
@@ -1244,6 +1244,16 @@ func setFieldValue(field reflect.Value, value interface{}) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle map[string]interface{} → nested struct (e.g. relation fields like AFN, DEF)
|
||||||
|
if m, ok := value.(map[string]interface{}); ok {
|
||||||
|
if field.CanAddr() {
|
||||||
|
if err := MapToStruct(m, field.Addr().Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: Try to find a "Val" field (for SqlNull types) and set it directly
|
// Fallback: Try to find a "Val" field (for SqlNull types) and set it directly
|
||||||
valField := field.FieldByName("Val")
|
valField := field.FieldByName("Val")
|
||||||
if valField.IsValid() && valField.CanSet() {
|
if valField.IsValid() && valField.CanSet() {
|
||||||
|
|||||||
@@ -221,6 +221,124 @@ func TestMapToStruct_AllSqlTypes(t *testing.T) {
|
|||||||
t.Logf(" - SqlJSONB (Tags): %v", tagsValue)
|
t.Logf(" - SqlJSONB (Tags): %v", tagsValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMapToStruct_NestedStructPointer tests that a map[string]interface{} value is
|
||||||
|
// correctly converted into a pointer-to-struct field (e.g. AFN *ModelCoreActionfunction).
|
||||||
|
func TestMapToStruct_NestedStructPointer(t *testing.T) {
|
||||||
|
type Inner struct {
|
||||||
|
ID spectypes.SqlInt32 `bun:"rid_inner,pk" json:"rid_inner"`
|
||||||
|
Name spectypes.SqlString `bun:"name" json:"name"`
|
||||||
|
}
|
||||||
|
type Outer struct {
|
||||||
|
ID spectypes.SqlInt32 `bun:"rid_outer,pk" json:"rid_outer"`
|
||||||
|
Inner *Inner `json:"inner,omitempty" bun:"rel:has-one"`
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMap := map[string]interface{}{
|
||||||
|
"rid_outer": int64(1),
|
||||||
|
"inner": map[string]interface{}{
|
||||||
|
"rid_inner": int64(42),
|
||||||
|
"name": "hello",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Outer
|
||||||
|
err := reflection.MapToStruct(dataMap, &result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MapToStruct() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.ID.Valid || result.ID.Val != 1 {
|
||||||
|
t.Errorf("ID = %v, want 1", result.ID)
|
||||||
|
}
|
||||||
|
if result.Inner == nil {
|
||||||
|
t.Fatal("Inner is nil, want non-nil")
|
||||||
|
}
|
||||||
|
if !result.Inner.ID.Valid || result.Inner.ID.Val != 42 {
|
||||||
|
t.Errorf("Inner.ID = %v, want 42", result.Inner.ID)
|
||||||
|
}
|
||||||
|
if !result.Inner.Name.Valid || result.Inner.Name.Val != "hello" {
|
||||||
|
t.Errorf("Inner.Name = %v, want 'hello'", result.Inner.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMapToStruct_NestedStructNilPointer tests that a nil map value leaves the pointer nil.
|
||||||
|
func TestMapToStruct_NestedStructNilPointer(t *testing.T) {
|
||||||
|
type Inner struct {
|
||||||
|
ID spectypes.SqlInt32 `bun:"rid_inner,pk" json:"rid_inner"`
|
||||||
|
}
|
||||||
|
type Outer struct {
|
||||||
|
ID spectypes.SqlInt32 `bun:"rid_outer,pk" json:"rid_outer"`
|
||||||
|
Inner *Inner `json:"inner,omitempty" bun:"rel:has-one"`
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMap := map[string]interface{}{
|
||||||
|
"rid_outer": int64(5),
|
||||||
|
"inner": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Outer
|
||||||
|
err := reflection.MapToStruct(dataMap, &result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MapToStruct() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Inner != nil {
|
||||||
|
t.Errorf("Inner = %v, want nil", result.Inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMapToStruct_NestedStructWithSpectypes mirrors the real-world case of
|
||||||
|
// ModelCoreActionoption.AFN being populated from map[string]interface{}.
|
||||||
|
func TestMapToStruct_NestedStructWithSpectypes(t *testing.T) {
|
||||||
|
type ActionFunction struct {
|
||||||
|
Ridactionfunction spectypes.SqlInt32 `bun:"rid_actionfunction,pk" json:"rid_actionfunction"`
|
||||||
|
Functionname spectypes.SqlString `bun:"functionname" json:"functionname"`
|
||||||
|
Fntype spectypes.SqlString `bun:"fntype" json:"fntype"`
|
||||||
|
}
|
||||||
|
type ActionOption struct {
|
||||||
|
Ridactionoption spectypes.SqlInt32 `bun:"rid_actionoption,pk" json:"rid_actionoption"`
|
||||||
|
Ridactionfunction spectypes.SqlInt32 `bun:"rid_actionfunction" json:"rid_actionfunction"`
|
||||||
|
Description spectypes.SqlString `bun:"description" json:"description"`
|
||||||
|
AFN *ActionFunction `json:"AFN,omitempty" bun:"rel:has-one"`
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMap := map[string]interface{}{
|
||||||
|
"rid_actionoption": int64(10),
|
||||||
|
"rid_actionfunction": int64(99),
|
||||||
|
"description": "test option",
|
||||||
|
"AFN": map[string]interface{}{
|
||||||
|
"rid_actionfunction": int64(99),
|
||||||
|
"functionname": "MyFunction",
|
||||||
|
"fntype": "action",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var result ActionOption
|
||||||
|
err := reflection.MapToStruct(dataMap, &result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MapToStruct() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Ridactionoption.Valid || result.Ridactionoption.Val != 10 {
|
||||||
|
t.Errorf("Ridactionoption = %v, want 10", result.Ridactionoption)
|
||||||
|
}
|
||||||
|
if !result.Description.Valid || result.Description.Val != "test option" {
|
||||||
|
t.Errorf("Description = %v, want 'test option'", result.Description)
|
||||||
|
}
|
||||||
|
if result.AFN == nil {
|
||||||
|
t.Fatal("AFN is nil, want non-nil")
|
||||||
|
}
|
||||||
|
if !result.AFN.Ridactionfunction.Valid || result.AFN.Ridactionfunction.Val != 99 {
|
||||||
|
t.Errorf("AFN.Ridactionfunction = %v, want 99", result.AFN.Ridactionfunction)
|
||||||
|
}
|
||||||
|
if !result.AFN.Functionname.Valid || result.AFN.Functionname.Val != "MyFunction" {
|
||||||
|
t.Errorf("AFN.Functionname = %v, want 'MyFunction'", result.AFN.Functionname)
|
||||||
|
}
|
||||||
|
if !result.AFN.Fntype.Valid || result.AFN.Fntype.Val != "action" {
|
||||||
|
t.Errorf("AFN.Fntype = %v, want 'action'", result.AFN.Fntype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMapToStruct_SqlNull_NilValues(t *testing.T) {
|
func TestMapToStruct_SqlNull_NilValues(t *testing.T) {
|
||||||
// Test that SqlNull types handle nil values correctly
|
// Test that SqlNull types handle nil values correctly
|
||||||
type TestModel struct {
|
type TestModel struct {
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
// Standard processing without nested relations
|
// Standard processing without nested relations
|
||||||
query := h.db.NewInsert().Table(tableName)
|
query := h.db.NewInsert().Table(tableName)
|
||||||
for key, value := range v {
|
for key, value := range v {
|
||||||
query = query.Value(key, value)
|
query = query.Value(key, common.ConvertSliceForBun(value))
|
||||||
}
|
}
|
||||||
result, err := query.Exec(ctx)
|
result, err := query.Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -669,7 +669,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
for _, item := range v {
|
for _, item := range v {
|
||||||
txQuery := tx.NewInsert().Table(tableName)
|
txQuery := tx.NewInsert().Table(tableName)
|
||||||
for key, value := range item {
|
for key, value := range item {
|
||||||
txQuery = txQuery.Value(key, value)
|
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
||||||
}
|
}
|
||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
return err
|
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 {
|
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||||
txQuery := tx.NewInsert().Table(tableName)
|
txQuery := tx.NewInsert().Table(tableName)
|
||||||
for key, value := range itemMap {
|
for key, value := range itemMap {
|
||||||
txQuery = txQuery.Value(key, value)
|
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
||||||
}
|
}
|
||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
755
pkg/spectypes/sql_array_types.go
Normal file
755
pkg/spectypes/sql_array_types.go
Normal file
@@ -0,0 +1,755 @@
|
|||||||
|
package spectypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parsePostgresArrayElements parses a PostgreSQL array literal (e.g. `{a,"b,c",d}`)
|
||||||
|
// into a slice of raw string elements. Each element retains its unquoted/unescaped value.
|
||||||
|
func parsePostgresArrayElements(s string) ([]string, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" || strings.EqualFold(s, "null") || strings.EqualFold(s, "NULL") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(s, "{") || !strings.HasSuffix(s, "}") {
|
||||||
|
return nil, fmt.Errorf("not a valid PostgreSQL array literal: %q", s)
|
||||||
|
}
|
||||||
|
inner := s[1 : len(s)-1]
|
||||||
|
if inner == "" {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
var cur strings.Builder
|
||||||
|
inQuotes := false
|
||||||
|
i := 0
|
||||||
|
for i < len(inner) {
|
||||||
|
c := inner[i]
|
||||||
|
switch {
|
||||||
|
case c == '"' && !inQuotes:
|
||||||
|
inQuotes = true
|
||||||
|
case c == '"' && inQuotes:
|
||||||
|
if i+1 < len(inner) && inner[i+1] == '"' {
|
||||||
|
cur.WriteByte('"')
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
inQuotes = false
|
||||||
|
}
|
||||||
|
case c == '\\' && inQuotes:
|
||||||
|
if i+1 < len(inner) {
|
||||||
|
cur.WriteByte(inner[i+1])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case c == ',' && !inQuotes:
|
||||||
|
result = append(result, cur.String())
|
||||||
|
cur.Reset()
|
||||||
|
default:
|
||||||
|
cur.WriteByte(c)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
result = append(result, cur.String())
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatPostgresStringArray formats a []string back into a PostgreSQL array literal.
|
||||||
|
func formatPostgresStringArray(vals []string) string {
|
||||||
|
if vals == nil {
|
||||||
|
return "NULL"
|
||||||
|
}
|
||||||
|
parts := make([]string, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
// Quote if value contains comma, double-quote, backslash, braces, whitespace, or is empty.
|
||||||
|
needsQuote := v == "" || strings.ContainsAny(v, `,"\\{}`+"\t\n\r ")
|
||||||
|
if needsQuote {
|
||||||
|
v = strings.ReplaceAll(v, `\`, `\\`)
|
||||||
|
v = strings.ReplaceAll(v, `"`, `""`)
|
||||||
|
parts[i] = `"` + v + `"`
|
||||||
|
} else {
|
||||||
|
parts[i] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ",") + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlStringArray ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// SqlStringArray is a nullable PostgreSQL text[] / varchar[] array.
|
||||||
|
type SqlStringArray struct {
|
||||||
|
Val []string
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlStringArray) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlStringArray: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
elems, err := parsePostgresArrayElements(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = elems
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlStringArray) Value() (driver.Value, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return formatPostgresStringArray(a.Val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlStringArray) MarshalJSON() ([]byte, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlStringArray) UnmarshalJSON(b []byte) error {
|
||||||
|
s := strings.TrimSpace(string(b))
|
||||||
|
if s == "null" {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []string
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = vals
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlStringArray(v []string) SqlStringArray {
|
||||||
|
return SqlStringArray{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlInt16Array ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SqlInt16Array struct {
|
||||||
|
Val []int16
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt16Array) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlInt16Array: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
elems, err := parsePostgresArrayElements(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = make([]int16, len(elems))
|
||||||
|
for i, e := range elems {
|
||||||
|
n, err := strconv.ParseInt(strings.TrimSpace(e), 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SqlInt16Array: element %d %q: %w", i, e, err)
|
||||||
|
}
|
||||||
|
a.Val[i] = int16(n)
|
||||||
|
}
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt16Array) Value() (driver.Value, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := make([]string, len(a.Val))
|
||||||
|
for i, v := range a.Val {
|
||||||
|
parts[i] = strconv.FormatInt(int64(v), 10)
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ",") + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt16Array) MarshalJSON() ([]byte, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt16Array) UnmarshalJSON(b []byte) error {
|
||||||
|
if strings.TrimSpace(string(b)) == "null" {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []int16
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = vals
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlInt16Array(v []int16) SqlInt16Array {
|
||||||
|
return SqlInt16Array{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlInt32Array ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SqlInt32Array struct {
|
||||||
|
Val []int32
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt32Array) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlInt32Array: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
elems, err := parsePostgresArrayElements(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = make([]int32, len(elems))
|
||||||
|
for i, e := range elems {
|
||||||
|
n, err := strconv.ParseInt(strings.TrimSpace(e), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SqlInt32Array: element %d %q: %w", i, e, err)
|
||||||
|
}
|
||||||
|
a.Val[i] = int32(n)
|
||||||
|
}
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt32Array) Value() (driver.Value, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := make([]string, len(a.Val))
|
||||||
|
for i, v := range a.Val {
|
||||||
|
parts[i] = strconv.FormatInt(int64(v), 10)
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ",") + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt32Array) MarshalJSON() ([]byte, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt32Array) UnmarshalJSON(b []byte) error {
|
||||||
|
if strings.TrimSpace(string(b)) == "null" {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []int32
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = vals
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlInt32Array(v []int32) SqlInt32Array {
|
||||||
|
return SqlInt32Array{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlInt64Array ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SqlInt64Array struct {
|
||||||
|
Val []int64
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt64Array) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlInt64Array: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
elems, err := parsePostgresArrayElements(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = make([]int64, len(elems))
|
||||||
|
for i, e := range elems {
|
||||||
|
n, err := strconv.ParseInt(strings.TrimSpace(e), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SqlInt64Array: element %d %q: %w", i, e, err)
|
||||||
|
}
|
||||||
|
a.Val[i] = n
|
||||||
|
}
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt64Array) Value() (driver.Value, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := make([]string, len(a.Val))
|
||||||
|
for i, v := range a.Val {
|
||||||
|
parts[i] = strconv.FormatInt(v, 10)
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ",") + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt64Array) MarshalJSON() ([]byte, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt64Array) UnmarshalJSON(b []byte) error {
|
||||||
|
if strings.TrimSpace(string(b)) == "null" {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []int64
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = vals
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlInt64Array(v []int64) SqlInt64Array {
|
||||||
|
return SqlInt64Array{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlFloat32Array ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SqlFloat32Array struct {
|
||||||
|
Val []float32
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat32Array) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlFloat32Array: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
elems, err := parsePostgresArrayElements(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = make([]float32, len(elems))
|
||||||
|
for i, e := range elems {
|
||||||
|
f, err := strconv.ParseFloat(strings.TrimSpace(e), 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SqlFloat32Array: element %d %q: %w", i, e, err)
|
||||||
|
}
|
||||||
|
a.Val[i] = float32(f)
|
||||||
|
}
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat32Array) Value() (driver.Value, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := make([]string, len(a.Val))
|
||||||
|
for i, v := range a.Val {
|
||||||
|
parts[i] = strconv.FormatFloat(float64(v), 'f', -1, 32)
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ",") + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat32Array) MarshalJSON() ([]byte, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat32Array) UnmarshalJSON(b []byte) error {
|
||||||
|
if strings.TrimSpace(string(b)) == "null" {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []float32
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = vals
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlFloat32Array(v []float32) SqlFloat32Array {
|
||||||
|
return SqlFloat32Array{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlFloat64Array ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SqlFloat64Array struct {
|
||||||
|
Val []float64
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat64Array) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlFloat64Array: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
elems, err := parsePostgresArrayElements(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = make([]float64, len(elems))
|
||||||
|
for i, e := range elems {
|
||||||
|
f, err := strconv.ParseFloat(strings.TrimSpace(e), 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SqlFloat64Array: element %d %q: %w", i, e, err)
|
||||||
|
}
|
||||||
|
a.Val[i] = f
|
||||||
|
}
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat64Array) Value() (driver.Value, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := make([]string, len(a.Val))
|
||||||
|
for i, v := range a.Val {
|
||||||
|
parts[i] = strconv.FormatFloat(v, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ",") + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat64Array) MarshalJSON() ([]byte, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat64Array) UnmarshalJSON(b []byte) error {
|
||||||
|
if strings.TrimSpace(string(b)) == "null" {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []float64
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = vals
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlFloat64Array(v []float64) SqlFloat64Array {
|
||||||
|
return SqlFloat64Array{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlBoolArray ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SqlBoolArray struct {
|
||||||
|
Val []bool
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlBoolArray) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlBoolArray: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
elems, err := parsePostgresArrayElements(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = make([]bool, len(elems))
|
||||||
|
for i, e := range elems {
|
||||||
|
e = strings.ToLower(strings.TrimSpace(e))
|
||||||
|
a.Val[i] = e == "t" || e == "true" || e == "1" || e == "yes"
|
||||||
|
}
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlBoolArray) Value() (driver.Value, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := make([]string, len(a.Val))
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if v {
|
||||||
|
parts[i] = "t"
|
||||||
|
} else {
|
||||||
|
parts[i] = "f"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ",") + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlBoolArray) MarshalJSON() ([]byte, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlBoolArray) UnmarshalJSON(b []byte) error {
|
||||||
|
if strings.TrimSpace(string(b)) == "null" {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []bool
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = vals
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlBoolArray(v []bool) SqlBoolArray {
|
||||||
|
return SqlBoolArray{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlUUIDArray ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SqlUUIDArray struct {
|
||||||
|
Val []uuid.UUID
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlUUIDArray) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlUUIDArray: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
elems, err := parsePostgresArrayElements(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = make([]uuid.UUID, len(elems))
|
||||||
|
for i, e := range elems {
|
||||||
|
u, err := uuid.Parse(strings.TrimSpace(e))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SqlUUIDArray: element %d %q: %w", i, e, err)
|
||||||
|
}
|
||||||
|
a.Val[i] = u
|
||||||
|
}
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlUUIDArray) Value() (driver.Value, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := make([]string, len(a.Val))
|
||||||
|
for i, v := range a.Val {
|
||||||
|
parts[i] = v.String()
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ",") + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlUUIDArray) MarshalJSON() ([]byte, error) {
|
||||||
|
if !a.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlUUIDArray) UnmarshalJSON(b []byte) error {
|
||||||
|
if strings.TrimSpace(string(b)) == "null" {
|
||||||
|
a.Valid = false
|
||||||
|
a.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []uuid.UUID
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val = vals
|
||||||
|
a.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlUUIDArray(v []uuid.UUID) SqlUUIDArray {
|
||||||
|
return SqlUUIDArray{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlVector ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// SqlVector is a nullable pgvector `vector` type backed by []float32.
|
||||||
|
// Wire format: `[1.0,2.0,3.0]` (square brackets, comma-separated floats).
|
||||||
|
type SqlVector struct {
|
||||||
|
Val []float32
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SqlVector) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
v.Valid = false
|
||||||
|
v.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
switch val := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = val
|
||||||
|
case []byte:
|
||||||
|
s = string(val)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("SqlVector: cannot scan type %T", value)
|
||||||
|
}
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if !strings.HasPrefix(s, "[") || !strings.HasSuffix(s, "]") {
|
||||||
|
return fmt.Errorf("SqlVector: not a valid vector literal: %q", s)
|
||||||
|
}
|
||||||
|
inner := s[1 : len(s)-1]
|
||||||
|
if inner == "" {
|
||||||
|
v.Val = []float32{}
|
||||||
|
v.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(inner, ",")
|
||||||
|
v.Val = make([]float32, len(parts))
|
||||||
|
for i, p := range parts {
|
||||||
|
f, err := strconv.ParseFloat(strings.TrimSpace(p), 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SqlVector: element %d %q: %w", i, p, err)
|
||||||
|
}
|
||||||
|
v.Val[i] = float32(f)
|
||||||
|
}
|
||||||
|
v.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v SqlVector) Value() (driver.Value, error) {
|
||||||
|
if !v.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := make([]string, len(v.Val))
|
||||||
|
for i, f := range v.Val {
|
||||||
|
parts[i] = strconv.FormatFloat(float64(f), 'f', -1, 32)
|
||||||
|
}
|
||||||
|
return "[" + strings.Join(parts, ",") + "]", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v SqlVector) MarshalJSON() ([]byte, error) {
|
||||||
|
if !v.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(v.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SqlVector) UnmarshalJSON(b []byte) error {
|
||||||
|
if strings.TrimSpace(string(b)) == "null" {
|
||||||
|
v.Valid = false
|
||||||
|
v.Val = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var vals []float32
|
||||||
|
if err := json.Unmarshal(b, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Val = vals
|
||||||
|
v.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlVector(val []float32) SqlVector {
|
||||||
|
return SqlVector{Val: val, Valid: true}
|
||||||
|
}
|
||||||
1
todo.md
1
todo.md
@@ -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
|
- [ ] Long preload alias names may exceed PostgreSQL identifier limit
|
||||||
- [ ] Some edge cases in computed column handling
|
- [ ] 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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user