From 78cd5aa60a5c7d1323bb89081db2b2b811113052 Mon Sep 17 00:00:00 2001 From: Vladimir Mihailenco Date: Sun, 7 Nov 2021 12:44:53 +0200 Subject: [PATCH] feat: support multiple tag options join:left_col1=right_col1,join:left_col2=right_col2 --- dialect/pgdialect/sqltype.go | 6 ++--- example/opentelemetry/README.md | 2 +- internal/dbtest/orm_test.go | 4 +-- internal/tagparser/parser.go | 24 +++++++++++++++--- internal/tagparser/parser_test.go | 33 ++++++++++++++----------- schema/table.go | 41 ++++++++++++++++++++----------- 6 files changed, 71 insertions(+), 39 deletions(-) diff --git a/dialect/pgdialect/sqltype.go b/dialect/pgdialect/sqltype.go index dab0446ed..1fbfa7d7f 100644 --- a/dialect/pgdialect/sqltype.go +++ b/dialect/pgdialect/sqltype.go @@ -4,7 +4,6 @@ import ( "encoding/json" "net" "reflect" - "time" "github.com/uptrace/bun/dialect/sqltype" "github.com/uptrace/bun/schema" @@ -41,7 +40,6 @@ const ( ) var ( - timeType = reflect.TypeOf((*time.Time)(nil)).Elem() ipType = reflect.TypeOf((*net.IP)(nil)).Elem() ipNetType = reflect.TypeOf((*net.IPNet)(nil)).Elem() jsonRawMessageType = reflect.TypeOf((*json.RawMessage)(nil)).Elem() @@ -52,11 +50,11 @@ func fieldSQLType(field *schema.Field) string { return field.UserSQLType } - if v, ok := field.Tag.Options["composite"]; ok { + if v, ok := field.Tag.Option("composite"); ok { return v } - if _, ok := field.Tag.Options["hstore"]; ok { + if _, ok := field.Tag.Option("hstore"); ok { return "hstore" } diff --git a/example/opentelemetry/README.md b/example/opentelemetry/README.md index fa31f8433..cc219368b 100644 --- a/example/opentelemetry/README.md +++ b/example/opentelemetry/README.md @@ -19,7 +19,7 @@ OTEL_EXPORTER_JAEGER_ENDPOINT=http://localhost:14268/api/traces go run . **Uptrace** exporter: ```shell -UPTRACE_DSN="https://@api.uptrace.dev/" go run . +UPTRACE_DSN="https://@uptrace.dev/" go run . ``` ## Links diff --git a/internal/dbtest/orm_test.go b/internal/dbtest/orm_test.go index ba778665c..bccfc3c9d 100644 --- a/internal/dbtest/orm_test.go +++ b/internal/dbtest/orm_test.go @@ -485,7 +485,7 @@ type Book struct { Genres []Genre `bun:"m2m:book_genres"` // many to many relation Translations []Translation `bun:"rel:has-many"` - Comments []Comment `bun:"rel:has-many,join:\"id=trackable_id,type=trackable_type\",polymorphic"` + Comments []Comment `bun:"rel:has-many,join:id=trackable_id,join:type=trackable_type,polymorphic"` } func (b Book) String() string { @@ -509,7 +509,7 @@ type Translation struct { Book *Book `bun:"rel:belongs-to"` Lang string `bun:"unique:book_id_lang"` - Comments []Comment `bun:"rel:has-many,join:\"id=trackable_id,type=trackable_type\",polymorphic"` + Comments []Comment `bun:"rel:has-many,join:id=trackable_id,join:type=trackable_type,polymorphic"` } type Comment struct { diff --git a/internal/tagparser/parser.go b/internal/tagparser/parser.go index eb3246536..a3905853d 100644 --- a/internal/tagparser/parser.go +++ b/internal/tagparser/parser.go @@ -6,7 +6,11 @@ import ( type Tag struct { Name string - Options map[string]string + Options map[string][]string +} + +func (t Tag) IsZero() bool { + return t.Name == "" && t.Options == nil } func (t Tag) HasOption(name string) bool { @@ -14,7 +18,17 @@ func (t Tag) HasOption(name string) bool { return ok } +func (t Tag) Option(name string) (string, bool) { + if vs, ok := t.Options[name]; ok { + return vs[len(vs)-1], true + } + return "", false +} + func Parse(s string) Tag { + if s == "" { + return Tag{} + } p := parser{ s: s, } @@ -45,9 +59,13 @@ func (p *parser) addOption(key, value string) { return } if p.tag.Options == nil { - p.tag.Options = make(map[string]string) + p.tag.Options = make(map[string][]string) + } + if vs, ok := p.tag.Options[key]; ok { + p.tag.Options[key] = append(vs, value) + } else { + p.tag.Options[key] = []string{value} } - p.tag.Options[key] = value } func (p *parser) parse() { diff --git a/internal/tagparser/parser_test.go b/internal/tagparser/parser_test.go index dc7ce6300..90eb33d31 100644 --- a/internal/tagparser/parser_test.go +++ b/internal/tagparser/parser_test.go @@ -11,26 +11,29 @@ import ( var tagTests = []struct { tag string name string - options map[string]string + options map[string][]string }{ {"", "", nil}, {"hello", "hello", nil}, - {"hello,world", "hello", map[string]string{"world": ""}}, + {"hello,world", "hello", map[string][]string{"world": {""}}}, {`"hello,world'`, "", nil}, {`"hello:world"`, `hello:world`, nil}, - {",hello", "", map[string]string{"hello": ""}}, - {",hello,world", "", map[string]string{"hello": "", "world": ""}}, - {"hello:", "", map[string]string{"hello": ""}}, - {"hello:world", "", map[string]string{"hello": "world"}}, - {"hello:world,foo", "", map[string]string{"hello": "world", "foo": ""}}, - {"hello:world,foo:bar", "", map[string]string{"hello": "world", "foo": "bar"}}, - {"hello:\"world1,world2\"", "", map[string]string{"hello": "world1,world2"}}, - {`hello:"world1,world2",world3`, "", map[string]string{"hello": "world1,world2", "world3": ""}}, - {`hello:"world1:world2",world3`, "", map[string]string{"hello": "world1:world2", "world3": ""}}, - {`hello:"D'Angelo, esquire",foo:bar`, "", map[string]string{"hello": "D'Angelo, esquire", "foo": "bar"}}, - {`hello:"world('foo', 'bar')"`, "", map[string]string{"hello": "world('foo', 'bar')"}}, - {" hello,foo: bar ", " hello", map[string]string{"foo": " bar "}}, - {"type:geometry(POINT, 4326)", "", map[string]string{"type": "geometry(POINT, 4326)"}}, + {",hello", "", map[string][]string{"hello": {""}}}, + {",hello,world", "", map[string][]string{"hello": {""}, "world": {""}}}, + {"hello:", "", map[string][]string{"hello": {""}}}, + {"hello:world", "", map[string][]string{"hello": {"world"}}}, + {"hello:world,foo", "", map[string][]string{"hello": {"world"}, "foo": {""}}}, + {"hello:world,foo:bar", "", map[string][]string{"hello": {"world"}, "foo": {"bar"}}}, + {"hello:\"world1,world2\"", "", map[string][]string{"hello": {"world1,world2"}}}, + {`hello:"world1,world2",world3`, "", map[string][]string{"hello": {"world1,world2"}, "world3": {""}}}, + {`hello:"world1:world2",world3`, "", map[string][]string{"hello": {"world1:world2"}, "world3": {""}}}, + {`hello:"D'Angelo, esquire",foo:bar`, "", map[string][]string{"hello": {"D'Angelo, esquire"}, "foo": {"bar"}}}, + {`hello:"world('foo', 'bar')"`, "", map[string][]string{"hello": {"world('foo', 'bar')"}}}, + {" hello,foo: bar ", " hello", map[string][]string{"foo": {" bar "}}}, + {"foo:bar(hello, world)", "", map[string][]string{"foo": {"bar(hello, world)"}}}, + {"foo:bar(hello(), world)", "", map[string][]string{"foo": {"bar(hello(), world)"}}}, + {"type:geometry(POINT, 4326)", "", map[string][]string{"type": {"geometry(POINT, 4326)"}}}, + {"foo:bar,foo:baz", "", map[string][]string{"foo": []string{"bar", "baz"}}}, } func TestTagParser(t *testing.T) { diff --git a/schema/table.go b/schema/table.go index 3afec97f4..642881087 100644 --- a/schema/table.go +++ b/schema/table.go @@ -300,11 +300,11 @@ func (t *Table) processBaseModelField(f reflect.StructField) { t.setName(tag.Name) } - if s, ok := tag.Options["select"]; ok { + if s, ok := tag.Option("select"); ok { t.SQLNameForSelects = t.quoteTableName(s) } - if s, ok := tag.Options["alias"]; ok { + if s, ok := tag.Option("alias"); ok { t.Alias = s t.SQLAlias = t.quoteIdent(s) } @@ -359,20 +359,27 @@ func (t *Table) newField(f reflect.StructField, index []int) *Field { } if v, ok := tag.Options["unique"]; ok { - // Split the value by comma, this will allow multiple names to be specified. - // We can use this to create multiple named unique constraints where a single column - // might be included in multiple constraints. - for _, uniqueName := range strings.Split(v, ",") { + var names []string + if len(v) == 1 { + // Split the value by comma, this will allow multiple names to be specified. + // We can use this to create multiple named unique constraints where a single column + // might be included in multiple constraints. + names = strings.Split(v[0], ",") + } else { + names = v + } + + for _, uniqueName := range names { if t.Unique == nil { t.Unique = make(map[string][]*Field) } t.Unique[uniqueName] = append(t.Unique[uniqueName], field) } } - if s, ok := tag.Options["default"]; ok { + if s, ok := tag.Option("default"); ok { field.SQLDefault = s } - if s, ok := field.Tag.Options["type"]; ok { + if s, ok := field.Tag.Option("type"); ok { field.UserSQLType = s } field.DiscoveredSQLType = DiscoverSQLType(field.IndirectType) @@ -380,7 +387,7 @@ func (t *Table) newField(f reflect.StructField, index []int) *Field { field.Scan = FieldScanner(t.dialect, field) field.IsZero = zeroChecker(field.StructField.Type) - if v, ok := tag.Options["alt"]; ok { + if v, ok := tag.Option("alt"); ok { t.FieldMap[v] = field } @@ -432,7 +439,7 @@ func (t *Table) initRelations() { } func (t *Table) tryRelation(field *Field) bool { - if rel, ok := field.Tag.Options["rel"]; ok { + if rel, ok := field.Tag.Option("rel"); ok { t.initRelation(field, rel) return true } @@ -608,7 +615,7 @@ func (t *Table) hasManyRelation(field *Field) *Relation { } joinTable := t.dialect.Tables().Ref(indirectType(field.IndirectType.Elem())) - polymorphicValue, isPolymorphic := field.Tag.Options["polymorphic"] + polymorphicValue, isPolymorphic := field.Tag.Option("polymorphic") rel := &Relation{ Type: HasManyRelation, Field: field, @@ -705,7 +712,7 @@ func (t *Table) m2mRelation(field *Field) *Relation { panic(err) } - m2mTableName, ok := field.Tag.Options["m2m"] + m2mTableName, ok := field.Tag.Option("m2m") if !ok { panic(fmt.Errorf("bun: %s must have m2m tag option", field.GoName)) } @@ -890,8 +897,14 @@ func removeField(fields []*Field, field *Field) []*Field { return fields } -func parseRelationJoin(join string) ([]string, []string) { - ss := strings.Split(join, ",") +func parseRelationJoin(join []string) ([]string, []string) { + var ss []string + if len(join) == 1 { + ss = strings.Split(join[0], ",") + } else { + ss = join + } + baseColumns := make([]string, len(ss)) joinColumns := make([]string, len(ss)) for i, s := range ss {