Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/generate-database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Tag | Description
`primary=yes` | Assigns column associated with the field to be sufficient for returning a row from the table. Will default to `Name` if unspecified. Fields with this key will be included in the default 'ORDER BY' clause.
`omit=<Stmt Types>` | Omits a given field from consideration for the comma separated list of statement types (`create`, `objects-by-Name`, `update`).
`ignore=yes` | Outright ignore the struct field as though it does not exist.
`marshal=yes` | Marshal/Unmarshal data into the field. The column must be a TEXT column and the type must implement both `Marshal` and `Unmarshal`. This works for entity tables only, and not for association or mapping tables.
`marshal=<yes/json>` | Marshal/Unmarshal data into the field. The column must be a TEXT column. If `marshal=yes`, then the type must implement both `Marshal` and `Unmarshal`. If `marshal=json`, the type is marshaled to JSON using the standard library ([json.Marshal](https://pkg.go.dev/encoding/json#Marshal)). This works for entity tables only, and not for association or mapping tables.

### Go Function Generation

Expand Down
9 changes: 7 additions & 2 deletions cmd/generate-database/db/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ func destFunc(slice string, typ string, fields []*Field) string {
}

unmarshal := func(declVarName string, field *Field) {
writeLine(fmt.Sprintf("err = unmarshal(%s, &%s.%s)", declVarName, varName, field.Name))
unmarshalFunc := "unmarshal"
if field.Config.Get("marshal") == "json" {
unmarshalFunc = "unmarshalJSON"
}

writeLine(fmt.Sprintf("err = %s(%s, &%s.%s)", unmarshalFunc, declVarName, varName, field.Name))
checkErr()
}

Expand All @@ -106,7 +111,7 @@ func destFunc(slice string, typ string, fields []*Field) string {
declVarNames := make([]string, 0, len(fields))
for i, field := range fields {
var arg string
if util.IsTrue(field.Config.Get("marshal")) {
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
declVarName := fmt.Sprintf("%sStr", lex.Minuscule(field.Name))
declVarNames = append(declVarNames, declVarName)
declVars[declVarName] = field
Expand Down
2 changes: 1 addition & 1 deletion cmd/generate-database/db/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (m *Mapping) FieldParamsMarshal(fields []*Field) string {
name = lex.Minuscule(m.Name) + field.Name
}

if util.IsTrue(field.Config.Get("marshal")) {
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
name = fmt.Sprintf("marshaled%s", field.Name)
}

Expand Down
63 changes: 49 additions & 14 deletions cmd/generate-database/db/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,13 @@ func (m *Method) getMany(buf *file.Buffer) error {
var args string
for _, name := range filter {
for _, field := range mapping.Fields {
if name == field.Name && util.IsTrue(field.Config.Get("marshal")) {
buf.L("marshaledFilter%s, err := marshal(filter.%s)", name, name)
if name == field.Name && util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
marshalFunc := "marshal"
if strings.ToLower(field.Config.Get("marshal")) == "json" {
marshalFunc = "marshalJSON"
}

buf.L("marshaledFilter%s, err := %s(filter.%s)", name, marshalFunc, name)
m.ifErrNotNil(buf, true, "nil", "err")
args += fmt.Sprintf("marshaledFilter%s,", name)
} else if name == field.Name {
Expand Down Expand Up @@ -563,8 +568,13 @@ func (m *Method) id(buf *file.Buffer) error {
m.ifErrNotNil(buf, true, "-1", fmt.Sprintf(`fmt.Errorf("Failed to get \"%s\" prepared statement: %%w", err)`, stmtCodeVar(m.entity, "ID")))

for _, field := range nk {
if util.IsTrue(field.Config.Get("marshal")) {
buf.L("marshaled%s, err := marshal(%s)", field.Name, lex.Minuscule(field.Name))
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
marshalFunc := "marshal"
if strings.ToLower(field.Config.Get("marshal")) == "json" {
marshalFunc = "marshalJSON"
}

buf.L("marshaled%s, err := %s(%s)", field.Name, marshalFunc, lex.Minuscule(field.Name))
m.ifErrNotNil(buf, true, "-1", "err")
}
}
Expand Down Expand Up @@ -608,8 +618,13 @@ func (m *Method) exists(buf *file.Buffer) error {
m.ifErrNotNil(buf, true, "false", fmt.Sprintf(`fmt.Errorf("Failed to get \"%s\" prepared statement: %%w", err)`, stmtCodeVar(m.entity, "ID")))

for _, field := range nk {
if util.IsTrue(field.Config.Get("marshal")) {
buf.L("marshaled%s, err := marshal(%s)", field.Name, lex.Minuscule(field.Name))
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
marshalFunc := "marshal"
if strings.ToLower(field.Config.Get("marshal")) == "json" {
marshalFunc = "marshalJSON"
}

buf.L("marshaled%s, err := %s(%s)", field.Name, marshalFunc, lex.Minuscule(field.Name))
m.ifErrNotNil(buf, true, "false", "err")
}
}
Expand Down Expand Up @@ -726,8 +741,13 @@ func (m *Method) create(buf *file.Buffer, replace bool) error {

buf.L("// Populate the statement arguments. ")
for i, field := range fields {
if util.IsTrue(field.Config.Get("marshal")) {
buf.L("marshaled%s, err := marshal(object.%s)", field.Name, field.Name)
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
marshalFunc := "marshal"
if strings.ToLower(field.Config.Get("marshal")) == "json" {
marshalFunc = "marshalJSON"
}

buf.L("marshaled%s, err := %s(object.%s)", field.Name, marshalFunc, field.Name)
m.ifErrNotNil(buf, true, "-1", "err")
buf.L("args[%d] = marshaled%s", i, field.Name)
} else {
Expand Down Expand Up @@ -880,8 +900,13 @@ func (m *Method) rename(buf *file.Buffer) error {
m.ifErrNotNil(buf, true, fmt.Sprintf(`fmt.Errorf("Failed to get \"%s\" prepared statement: %%w", err)`, stmtCodeVar(m.entity, "rename")))

for _, field := range nk {
if util.IsTrue(field.Config.Get("marshal")) {
buf.L("marshaled%s, err := marshal(%s)", field.Name, lex.Minuscule(field.Name))
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
marshalFunc := "marshal"
if strings.ToLower(field.Config.Get("marshal")) == "json" {
marshalFunc = "marshalJSON"
}

buf.L("marshaled%s, err := %s(%s)", field.Name, marshalFunc, lex.Minuscule(field.Name))
m.ifErrNotNil(buf, true, "err")
}
}
Expand Down Expand Up @@ -999,8 +1024,13 @@ func (m *Method) update(buf *file.Buffer) error {
fields := updateMapping.ColumnFields("ID") // This exclude the ID column, which is autogenerated.
params := make([]string, len(fields))
for i, field := range fields {
if util.IsTrue(field.Config.Get("marshal")) {
buf.L("marshaled%s, err := marshal(object.%s)", field.Name, field.Name)
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
marshalFunc := "marshal"
if strings.ToLower(field.Config.Get("marshal")) == "json" {
marshalFunc = "marshalJSON"
}

buf.L("marshaled%s, err := %s(object.%s)", field.Name, marshalFunc, field.Name)
m.ifErrNotNil(buf, true, "err")
params[i] = fmt.Sprintf("marshaled%s", field.Name)
} else {
Expand Down Expand Up @@ -1111,8 +1141,13 @@ func (m *Method) delete(buf *file.Buffer, deleteOne bool) error {
buf.L("stmt, err := Stmt(db, %s)", stmtCodeVar(m.entity, "delete", FieldNames(activeFilters)...))

for _, field := range activeFilters {
if util.IsTrue(field.Config.Get("marshal")) {
buf.L("marshaled%s, err := marshal(%s)", field.Name, lex.Minuscule(field.Name))
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
marshalFunc := "marshal"
if strings.ToLower(field.Config.Get("marshal")) == "json" {
marshalFunc = "marshalJSON"
}

buf.L("marshaled%s, err := %s(%s)", field.Name, marshalFunc, lex.Minuscule(field.Name))
m.ifErrNotNil(buf, true, "err")
}
}
Expand Down
61 changes: 61 additions & 0 deletions cmd/generate-database/db/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"

"github.com/lxc/incus/v6/cmd/generate-database/lex"
"github.com/lxc/incus/v6/shared/util"
)

// FiltersFromStmt parses all filtering statement defined for the given entity. It
Expand Down Expand Up @@ -374,6 +375,11 @@ func parseField(f *types.Var, structTag string, kind string, pkgName string) (*F
if err != nil {
return nil, fmt.Errorf("Parse 'db' structure tag: %w", err)
}

err = validateFieldConfig(config)
if err != nil {
return nil, fmt.Errorf("Invalid struct tag for field %q: %v", name, err)
}
}

// Ignore fields that are marked with `db:"omit"`.
Expand Down Expand Up @@ -440,3 +446,58 @@ func parseType(x types.Type, pkgName string) string {
return ""
}
}

func validateFieldConfig(config url.Values) error {
for tag, values := range config {
switch tag {
case
"sql",
"coalesce",
"join",
"leftjoin",
"joinon",
"omit":

_, err := exactlyOneValue(tag, values)
return err

case
"order",
"primary",
"ignore":

value, err := exactlyOneValue(tag, values)
if err != nil {
return err
}

if !util.IsTrue(value) && !util.IsFalse(value) {
return fmt.Errorf("Unexpected value %q for %q tag", value, tag)
}

case "marshal":
value, err := exactlyOneValue(tag, values)
if err != nil {
return err
}

if !util.IsTrue(value) && !util.IsFalse(value) && strings.ToLower(value) != "json" {
return fmt.Errorf("Unexpected value %q for %q tag", value, tag)
}
}
}

return nil
}

func exactlyOneValue(tag string, values []string) (string, error) {
if len(values) == 0 {
return "", fmt.Errorf("Missing value for %q tag", tag)
}

if len(values) > 1 {
return "", fmt.Errorf("More than one value for %q tag", tag)
}

return values[0], nil
}
14 changes: 14 additions & 0 deletions cmd/generate-database/file/boilerplate/boilerplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package boilerplate
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
)
Expand Down Expand Up @@ -127,6 +128,19 @@ func unmarshal(data string, v any) error {
return unmarshaler.UnmarshalDB(data)
}

func marshalJSON(v any) (string, error) {
marshalled, err := json.Marshal(v)
if err != nil {
return "", err
}

return string(marshalled), nil
}

func unmarshalJSON(data string, v any) error {
return json.Unmarshal([]byte(data), v)
}

// dest is a function that is expected to return the objects to pass to the
// 'dest' argument of sql.Rows.Scan(). It is invoked by SelectObjects once per
// yielded row, and it will be passed the index of the row being scanned.
Expand Down
2 changes: 2 additions & 0 deletions cmd/generate-database/file/boilerplate/boilerplate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ func Test(t *testing.T) {
_ = defaultMapErr
_ = marshal
_ = unmarshal
_ = marshalJSON
_ = unmarshalJSON
_ = selectObjects
_ = scan
}
14 changes: 14 additions & 0 deletions internal/server/db/cluster/mapper_boilerplate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions shared/util/boolean.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ func IsTrue(value string) bool {
return slices.Contains([]string{"true", "1", "yes", "on"}, strings.ToLower(value))
}

// IsNeitherFalseNorEmpty is true, if value is neither false nor empty,
// which is the contrary to IsFalseOrEmpty.
func IsNeitherFalseNorEmpty(value string) bool {
isFalseOrEmtpy := IsFalseOrEmpty(value)
return !isFalseOrEmtpy
}

// IsTrueOrEmpty returns true if value is empty or if IsTrue() returns true.
func IsTrueOrEmpty(value string) bool {
return value == "" || IsTrue(value)
Expand Down