Skip to content

Commit

Permalink
improve and fix struct field tag 'ref' line parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
kataras committed Sep 27, 2023
1 parent 83a1f6b commit 6f9c0d5
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 12 deletions.
2 changes: 1 addition & 1 deletion desc/column.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type (
ReferenceTableName string // an optional reference table name for a foreign key constraint, e.g. user_profiles(id) -> user_profiles
ReferenceColumnName string // an optional reference column name for a foreign key constraint, e.g. user_profiles(id) -> id
DeferrableReference bool // a flag that indicates if the foreign key constraint is deferrable (omits foreign key checks on transactions)
ReferenceOnDelete string // an optional action for deleting referenced rows when referencing rows are deleted, e.g. CASCADE
ReferenceOnDelete string // an optional action for deleting referenced rows when referencing rows are deleted, e.g. NO ACTION, RESTRICT, CASCADE, SET NULL and SET DEFAULT. Defaults to CASCADE.

Index IndexType // an optional index type for the column

Expand Down
3 changes: 3 additions & 0 deletions desc/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ func ConvertRowsToStruct(td *Table, rows pgx.Rows, valuePtr interface{}) error {
if errors.As(err, &scanArgErr) {
if len(td.Columns) > scanArgErr.ColumnIndex {
col := td.Columns[scanArgErr.ColumnIndex]
// NOTE: this index may be invalid if the struct contains different order of the column in database,
// the only one option is to use the col's OrdinalPosition (starting from 1, where scanArgErr.ColumnIndex starts from 0)
// but OrdinalPosition is set only when CheckSchema method was called previously.
destColumnName := col.Name
err = fmt.Errorf("%w: field: %s.%s (%s): column: %s.%s",
err,
Expand Down
90 changes: 79 additions & 11 deletions desc/struct_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package desc
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)
Expand Down Expand Up @@ -50,8 +51,64 @@ const (

genRandomUUIDPGCryptoFunction1 = "gen_random_uuid()"
genRandomUUIDPGCryptoFunction2 = "uuid_generate_v4()"

// foreign key on delete actions:
refNoAction = "NO ACTION"
refCascade = "CASCADE"
refRestrict = "RESTRICT"
refSetNull = "SET NULL"
refSetDef = "SET DEFAULT"
)

func isForeignKeyOnDeleteActionValid(s string) bool {
switch strings.ToUpper(s) {
case refNoAction, refCascade, refRestrict, refSetNull, refSetDef:
return true
default:
return false
}
}

var (
refLineRegex = regexp.MustCompile(`(?i)(\w+)\((\w+)\s*(no action|cascade|restrict|set null|set default)?\s*(\w*)?\)$`)
errInvalidReferenceTag = fmt.Errorf("invalid reference tag")
)

func parseReferenceTagValue(value string) (refTableName, refColumnName, onDeleteAction string, isDeferrable bool, err error) {
matches := refLineRegex.FindStringSubmatch(value)
if len(matches) < 5 {
return "", "", "", false, fmt.Errorf("%w: %s", errInvalidReferenceTag, value)
}

refTableName = matches[1]
refColumnName = matches[2]
// If the string does not have those parts,
// the regexp will still match the table name and the column name,
// but the submatches for the optional parts will be empty strings.
// The matches[3 and 4] will not crash the program because it will still return a valid string, even if it’s empty.
onDeleteAction = strings.ToUpper(matches[3])
if onDeleteAction == "" {
onDeleteAction = refCascade // defaults to cascade.
}

deferrableValue := strings.ToUpper(matches[4])
if deferrableValue != "" && deferrableValue != "DEFERRABLE" {
return "", "", "", false, fmt.Errorf("%w: %s: invalid deferrable value: %s", errInvalidReferenceTag, value, deferrableValue)
}

isDeferrable = deferrableValue == "DEFERRABLE"

if !isForeignKeyOnDeleteActionValid(onDeleteAction) { // this check is not needed, but we do it anyway.
return "", "", "", false, fmt.Errorf("%w: %s: invalid on delete action: %s", errInvalidReferenceTag, value, onDeleteAction)
}

if isDeferrable && onDeleteAction == refRestrict {
return "", "", "", false, fmt.Errorf("%w: %s: deferrable reference cannot have RESTRICT on delete", errInvalidReferenceTag, value)
}

return
}

// convertStructFieldToColumnDefinion takes a table name and a reflect.StructField that represents a struct field
// and returns a pointer to a Column that represents a column definition for the database
// or an error if the conversion fails.
Expand Down Expand Up @@ -192,22 +249,33 @@ func convertStructFieldToColumnDefinion(tableName string, field reflect.StructFi
return c, fmt.Errorf("struct field: %s: invalid reference tag: %s", field.Name, fieldTag)
}

refTableName := value[0:idx]
refColumnNameLine := strings.Split(value[idx+1:len(value)-1], " ") // e.g. "ref=blogs(id cascade deferrable)"
/*
refTableName := value[0:idx]
refColumnNameLine := strings.Split(value[idx+1:len(value)-1], " ") // e.g. "ref=blogs(id cascade deferrable)"
c.ReferenceTableName = refTableName
c.ReferenceColumnName = refColumnNameLine[0]
c.ReferenceTableName = refTableName
c.ReferenceColumnName = refColumnNameLine[0]
if len(refColumnNameLine) > 1 {
c.ReferenceOnDelete = strings.ToUpper(refColumnNameLine[1])
} else {
c.ReferenceOnDelete = "CASCADE"
}
if len(refColumnNameLine) > 1 {
c.ReferenceOnDelete = strings.ToUpper(refColumnNameLine[1])
} else {
c.ReferenceOnDelete = "CASCADE"
}
if len(refColumnNameLine) > 2 {
c.DeferrableReference = strings.ToUpper(refColumnNameLine[2]) == "DEFERRABLE"
}
*/

if len(refColumnNameLine) > 2 {
c.DeferrableReference = strings.ToUpper(refColumnNameLine[2]) == "DEFERRABLE"
refTableName, refColumnName, onDeleteAction, isDeferrable, err := parseReferenceTagValue(value)
if err != nil {
return c, fmt.Errorf("struct field: %s: %w", field.Name, err)
}

c.ReferenceTableName = refTableName
c.ReferenceColumnName = refColumnName
c.ReferenceOnDelete = onDeleteAction
c.DeferrableReference = isDeferrable
case "index":
idx := parseIndexType(value)
if idx == InvalidIndex {
Expand Down
59 changes: 59 additions & 0 deletions desc/struct_table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package desc

import (
"errors"
"testing"
)

func TestParseReferenceTagValue(t *testing.T) {
// Define some test cases with different input values and expected outputs.
testCases := []struct {
input string
refTableName string
refColumnName string
onDeleteAction string
isDeferrable bool
err error
}{
// Valid cases.
{"blogs(id no action deferrable)", "blogs", "id", "NO ACTION", true, nil},
{"blogs(id no action)", "blogs", "id", "NO ACTION", false, nil},
{"blogs(id)", "blogs", "id", "CASCADE", false, nil},
{"blogs(id cascade)", "blogs", "id", "CASCADE", false, nil},
{"blogs(id set null deferrable)", "blogs", "id", "SET NULL", true, nil},
{"blogs(id set default)", "blogs", "id", "SET DEFAULT", false, nil},
// Invalid cases.
{"blogs(id foo)", "", "", "", false, errInvalidReferenceTag},
{"blogs(id restrict deferrable)", "", "", "", false, errInvalidReferenceTag},
}

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
// Call the function with the input value and get the output values.
refTableName, refColumnName, onDeleteAction, isDeferrable, err := parseReferenceTagValue(tc.input)

// Check if the output values match the expected values.
if refTableName != tc.refTableName {
t.Errorf("%s: expected refTableName to be %s, got %s", tc.input, tc.refTableName, refTableName)
}

if refColumnName != tc.refColumnName {
t.Errorf("%s: expected refColumnName to be %s, got %s", tc.input, tc.refColumnName, refColumnName)
}

if onDeleteAction != tc.onDeleteAction {
t.Errorf("%s: expected onDeleteAction to be %s, got %s", tc.input, tc.onDeleteAction, onDeleteAction)
}

if isDeferrable != tc.isDeferrable {
t.Errorf("%s: expected isDeferrable to be %t, got %t", tc.input, tc.isDeferrable, isDeferrable)
}

if err != tc.err {
if !errors.Is(err, tc.err) {
t.Errorf("%s: expected err to be %v, got %v", tc.input, tc.err, err)
}
}
})
}
}

0 comments on commit 6f9c0d5

Please sign in to comment.