From f9e2609e957f914a25714917c9142866b33cde7e Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Mon, 2 Aug 2021 17:33:13 +0200 Subject: [PATCH] dialect/sql: add pattern matching predicates for json values --- dialect/sql/sqljson/sqljson.go | 33 +++++++++++++++++ dialect/sql/sqljson/sqljson_test.go | 56 +++++++++++++++++++++++++++++ entc/integration/json/json_test.go | 32 +++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/dialect/sql/sqljson/sqljson.go b/dialect/sql/sqljson/sqljson.go index 0a302ab5c..e24155f86 100644 --- a/dialect/sql/sqljson/sqljson.go +++ b/dialect/sql/sqljson/sqljson.go @@ -142,6 +142,39 @@ func ValueContains(column string, arg interface{}, opts ...Option) *sql.Predicat }) } +// StrValueContains return a predicate for checking that a JSON string value +// (returned by the path) contains the given substring +func StrValueContains(column string, arg string, opts ...Option) *sql.Predicate { + return sql.P(func(b *sql.Builder) { + opts = append(opts, Unquote(true)) + ValuePath(b, column, opts...) + argWithWildCard := "%" + arg + "%" + b.WriteOp(sql.OpLike).Arg(argWithWildCard) + }) +} + +// StrValueHasPrefix return a predicate for checking that a JSON string value +// (returned by the path) has the given substring as prefix +func StrValueHasPrefix(column string, arg string, opts ...Option) *sql.Predicate { + return sql.P(func(b *sql.Builder) { + opts = append(opts, Unquote(true)) + ValuePath(b, column, opts...) + argWithWildCard := arg + "%" + b.WriteOp(sql.OpLike).Arg(argWithWildCard) + }) +} + +// StrValueHasSuffix return a predicate for checking that a JSON string value +// (returned by the path) has the given substring as suffix +func StrValueHasSuffix(column string, arg string, opts ...Option) *sql.Predicate { + return sql.P(func(b *sql.Builder) { + opts = append(opts, Unquote(true)) + ValuePath(b, column, opts...) + argWithWildCard := "%" + arg + b.WriteOp(sql.OpLike).Arg(argWithWildCard) + }) +} + // LenEQ return a predicate for checking that an array length // of a JSON (returned by the path) is equal to the given argument. // diff --git a/dialect/sql/sqljson/sqljson_test.go b/dialect/sql/sqljson/sqljson_test.go index 34e202731..641e61810 100644 --- a/dialect/sql/sqljson/sqljson_test.go +++ b/dialect/sql/sqljson/sqljson_test.go @@ -195,6 +195,62 @@ func TestWritePath(t *testing.T) { wantQuery: "SELECT * FROM \"users\" WHERE (\"tags\"->'a')::jsonb @> $1", wantArgs: []interface{}{"1"}, }, + { + input: sql.Dialect(dialect.Postgres). + Select("*"). + From(sql.Table("users")). + Where(sqljson.StrValueContains("a", "substr", sqljson.Path("b", "c", "[1]", "d"))), + wantQuery: `SELECT * FROM "users" WHERE "a"->'b'->'c'->1->>'d' LIKE $1`, + wantArgs: []interface{}{"%substr%"}, + }, + { + input: sql.Dialect(dialect.MySQL). + Select("*"). + From(sql.Table("users")). + Where(sqljson.StrValueContains("a", "substr", sqljson.Path("b", "c", "[1]", "d"))), + wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, \"$.b.c[1].d\")) LIKE ?", + wantArgs: []interface{}{"%substr%"}, + }, + { + input: sql.Dialect(dialect.SQLite). + Select("*"). + From(sql.Table("users")). + Where(sqljson.StrValueContains("a", "substr", sqljson.Path("b", "c", "[1]", "d"))), + wantQuery: "SELECT * FROM `users` WHERE JSON_EXTRACT(`a`, \"$.b.c[1].d\") LIKE ?", + wantArgs: []interface{}{"%substr%"}, + }, + { + input: sql.Dialect(dialect.Postgres). + Select("*"). + From(sql.Table("users")). + Where(sqljson.StrValueHasPrefix("a", "substr", sqljson.Path("b", "c", "[1]", "d"))), + wantQuery: `SELECT * FROM "users" WHERE "a"->'b'->'c'->1->>'d' LIKE $1`, + wantArgs: []interface{}{"substr%"}, + }, + { + input: sql.Dialect(dialect.MySQL). + Select("*"). + From(sql.Table("users")). + Where(sqljson.StrValueHasPrefix("a", "substr", sqljson.Path("b", "c", "[1]", "d"))), + wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, \"$.b.c[1].d\")) LIKE ?", + wantArgs: []interface{}{"substr%"}, + }, + { + input: sql.Dialect(dialect.Postgres). + Select("*"). + From(sql.Table("users")). + Where(sqljson.StrValueHasSuffix("a", "substr", sqljson.Path("b", "c", "[1]", "d"))), + wantQuery: `SELECT * FROM "users" WHERE "a"->'b'->'c'->1->>'d' LIKE $1`, + wantArgs: []interface{}{"%substr"}, + }, + { + input: sql.Dialect(dialect.MySQL). + Select("*"). + From(sql.Table("users")). + Where(sqljson.StrValueHasSuffix("a", "substr", sqljson.Path("b", "c", "[1]", "d"))), + wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, \"$.b.c[1].d\")) LIKE ?", + wantArgs: []interface{}{"%substr"}, + }, } for i, tt := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { diff --git a/entc/integration/json/json_test.go b/entc/integration/json/json_test.go index 194f0ec19..854fe067f 100644 --- a/entc/integration/json/json_test.go +++ b/entc/integration/json/json_test.go @@ -291,6 +291,7 @@ func Predicates(t *testing.T, client *ent.Client) { s.Where(sqljson.ValueContains(user.FieldInts, 3)) }).OnlyX(ctx) require.Contains(t, r.Ints, 3) + r = client.User.Query().Where(func(s *sql.Selector) { s.Where(sqljson.ValueContains(user.FieldT, 3, sqljson.Path("li"))) }).OnlyX(ctx) @@ -300,4 +301,35 @@ func Predicates(t *testing.T, client *ent.Client) { s.Where(sqljson.ValueContains(user.FieldT, "a", sqljson.Path("ls"))) }).OnlyX(ctx) require.Contains(t, r.T.Ls, "a") + + client.User.Delete().ExecX(ctx) + u, err := url.Parse("https://github.com/a8m") + require.NoError(t, err) + dirs := []http.Dir{"/dev/null"} + _, err = client.User.CreateBulk( + client.User.Create().SetURL(u), + client.User.Create().SetDirs(dirs), + client.User.Create().SetT(&schema.T{S: "foobar", Ls: []string{"foo", "bar"}}), + ).Save(ctx) + require.NoError(t, err) + + r = client.User.Query().Where(func(s *sql.Selector) { + s.Where(sqljson.StrValueContains(user.FieldDirs, "dev", sqljson.Path("[0]"))) + }).OnlyX(ctx) + require.Equal(t, r.Dirs, dirs) + + r = client.User.Query().Where(func(s *sql.Selector) { + s.Where(sqljson.StrValueContains(user.FieldURL, "github", sqljson.Path("Host"))) + }).OnlyX(ctx) + require.Equal(t, r.URL, u) + + r = client.User.Query().Where(func(s *sql.Selector) { + s.Where(sqljson.StrValueHasPrefix(user.FieldT, "foo", sqljson.Path("s"))) + }).OnlyX(ctx) + require.Equal(t, r.T.S, "foobar") + + r = client.User.Query().Where(func(s *sql.Selector) { + s.Where(sqljson.StrValueHasSuffix(user.FieldT, "bar", sqljson.Path("s"))) + }).OnlyX(ctx) + require.Equal(t, r.T.S, "foobar") }