mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-05 04:51:58 +00:00
fix(crud): skip generated and read-only columns on insert and update
Tests / Integration Tests (push) Skipped
Build , Vet Test, and Lint / Build (push) Successful in 1m34s
Tests / Unit Tests (push) Successful in 1m42s
Build , Vet Test, and Lint / Lint Code (push) Successful in 2m11s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 2m25s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 2m28s
Tests / Race Detector (push) Successful in 4m6s
Tests / Integration Tests (push) Skipped
Build , Vet Test, and Lint / Build (push) Successful in 1m34s
Tests / Unit Tests (push) Successful in 1m42s
Build , Vet Test, and Lint / Lint Code (push) Successful in 2m11s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 2m25s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 2m28s
Tests / Race Detector (push) Successful in 4m6s
Read-merge-update wrote every model column back, so GENERATED ALWAYS columns failed with SQLSTATE 428C9. Add a bun 'generated' tag option, reflection.NonWritableColumns/RemoveNonWritableColumns, and apply them in resolvespec, restheadspec, websocketspec, mqttspec, resolvemcp and the nested CUD processor. Add ExcludeColumn to InsertQuery/UpdateQuery for model-based writes.
This commit is contained in:
@@ -656,7 +656,7 @@ func isColumnWritableInType(typ reflect.Type, columnName string) (found bool, wr
|
||||
// Check bun tag for scanonly
|
||||
bunTag := field.Tag.Get("bun")
|
||||
if bunTag != "" {
|
||||
if isBunFieldScanOnly(bunTag) {
|
||||
if isBunFieldScanOnly(bunTag) || isBunFieldGenerated(bunTag) {
|
||||
return true, false
|
||||
}
|
||||
}
|
||||
@@ -689,6 +689,70 @@ func isBunFieldScanOnly(tag string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isBunFieldGenerated checks if a bun tag marks the column as database-generated
|
||||
// (GENERATED ALWAYS AS ... STORED), which can be read but never written.
|
||||
// Example: "email_normalized,generated" -> true
|
||||
func isBunFieldGenerated(tag string) bool {
|
||||
for _, part := range strings.Split(tag, ",") {
|
||||
if strings.TrimSpace(part) == "generated" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveNonWritableColumns deletes from values every key that maps to a
|
||||
// non-writable model column (bun scanonly/generated, gorm read-only). Used
|
||||
// before writing a read-merged record back with UPDATE ... SET.
|
||||
func RemoveNonWritableColumns(model any, values map[string]interface{}) {
|
||||
for key := range values {
|
||||
if !IsColumnWritable(model, key) {
|
||||
delete(values, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NonWritableColumns returns the column names of the model that cannot be
|
||||
// written (bun scanonly/generated, gorm read-only), including embedded structs.
|
||||
func NonWritableColumns(model any) []string {
|
||||
t := reflect.TypeOf(model)
|
||||
for t != nil && (t.Kind() == reflect.Pointer || t.Kind() == reflect.Slice || t.Kind() == reflect.Array) {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t == nil || t.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
var cols []string
|
||||
collectNonWritable(t, &cols)
|
||||
return cols
|
||||
}
|
||||
|
||||
func collectNonWritable(typ reflect.Type, cols *[]string) {
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if field.Anonymous {
|
||||
ft := field.Type
|
||||
if ft.Kind() == reflect.Pointer {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct {
|
||||
collectNonWritable(ft, cols)
|
||||
continue
|
||||
}
|
||||
}
|
||||
bunTag, gormTag := field.Tag.Get("bun"), field.Tag.Get("gorm")
|
||||
if bunTag == "-" || gormTag == "-" {
|
||||
continue
|
||||
}
|
||||
if (bunTag != "" && (isBunFieldScanOnly(bunTag) || isBunFieldGenerated(bunTag))) ||
|
||||
(gormTag != "" && isGormFieldReadOnly(gormTag)) {
|
||||
if name := getColumnNameFromField(field); name != "" {
|
||||
*cols = append(*cols, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isGormFieldReadOnly checks if a gorm tag indicates the field is read-only
|
||||
// Examples:
|
||||
// - "<-:false" -> true (no writes allowed)
|
||||
|
||||
Reference in New Issue
Block a user