snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
155
dialect/sql/sqlgraph/blob.go
Normal file
155
dialect/sql/sqlgraph/blob.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package sqlgraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
)
|
||||
|
||||
// BlobSpec configures SQL-level blob key queries and implements [ent.BlobQuerier].
|
||||
type BlobSpec struct {
|
||||
Driver dialect.Driver
|
||||
Table string
|
||||
Columns map[string]string // field name -> key column name
|
||||
// CheckRefs holds the fields whose keys more than one row can hold, the ones
|
||||
// declaring CheckRefs on the schema. Only these are looked up before a delete;
|
||||
// a key generated fresh per write has no other holder to find. An empty
|
||||
// CheckRefs makes [BlobSpec.ReferencedBlobKeys] a no-op.
|
||||
CheckRefs map[string]bool
|
||||
Predicate func(*sql.Selector)
|
||||
}
|
||||
|
||||
// maxBlobKeysPerQuery bounds the keys per reference lookup, keeping the
|
||||
// statement clear of driver placeholder limits on large deletes.
|
||||
const maxBlobKeysPerQuery = 500
|
||||
|
||||
// ReferencedBlobKeys implements [ent.BlobRefChecker]. It returns the subset of keys
|
||||
// some row holds in its field's key column. [BlobSpec.Predicate] is deliberately not
|
||||
// applied: the question is which keys are in use anywhere, and callers time the lookup
|
||||
// so the mutation's own rows already read the way they will settle.
|
||||
//
|
||||
// Only fields listed in [BlobSpec.CheckRefs] are looked up. Keys of any other field are
|
||||
// reported as held by no row, which is what an unshared key strategy guarantees anyway.
|
||||
//
|
||||
// The lookup only asks whether a key is used, never how often, so an index on each
|
||||
// blob key column lets it stop at the first match.
|
||||
func (s *BlobSpec) ReferencedBlobKeys(ctx context.Context, keys []ent.BlobKey) ([]ent.BlobKey, error) {
|
||||
if len(keys) == 0 || len(s.CheckRefs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// Group the distinct keys by the column they would be stored in.
|
||||
lookup := make(map[string][]any)
|
||||
seen := make(map[ent.BlobKey]bool, len(keys))
|
||||
for _, k := range keys {
|
||||
if k.Key == "" || seen[k] || !s.CheckRefs[k.Field] {
|
||||
continue
|
||||
}
|
||||
seen[k] = true
|
||||
if col, ok := s.Columns[k.Field]; ok {
|
||||
lookup[col] = append(lookup[col], k.Key)
|
||||
}
|
||||
}
|
||||
var referenced []ent.BlobKey
|
||||
for field := range s.CheckRefs {
|
||||
col, ok := s.Columns[field]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
vals := lookup[col]
|
||||
for len(vals) > 0 {
|
||||
n := min(len(vals), maxBlobKeysPerQuery)
|
||||
found, err := s.selectReferenced(ctx, col, vals[:n])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, key := range found {
|
||||
referenced = append(referenced, ent.BlobKey{Field: field, Key: key})
|
||||
}
|
||||
vals = vals[n:]
|
||||
}
|
||||
}
|
||||
return referenced, nil
|
||||
}
|
||||
|
||||
// selectReferenced returns which of vals appear in col.
|
||||
func (s *BlobSpec) selectReferenced(ctx context.Context, col string, vals []any) ([]string, error) {
|
||||
query, args := sql.Dialect(s.Driver.Dialect()).
|
||||
Select(col).
|
||||
Distinct().
|
||||
From(sql.Table(s.Table)).
|
||||
Where(sql.In(col, vals...)).
|
||||
Query()
|
||||
rows := &sql.Rows{}
|
||||
if err := s.Driver.Query(ctx, query, args, rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var found []string
|
||||
for rows.Next() {
|
||||
var key string
|
||||
if err := rows.Scan(&key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
found = append(found, key)
|
||||
}
|
||||
return found, rows.Err()
|
||||
}
|
||||
|
||||
// QueryBlobKeys implements [ent.BlobQuerier].
|
||||
// If fields is nil, all columns are queried (for deletes);
|
||||
// otherwise only the named fields are queried.
|
||||
func (s *BlobSpec) QueryBlobKeys(ctx context.Context, fields []string) ([]ent.BlobKey, error) {
|
||||
cols := s.Columns
|
||||
if len(fields) > 0 {
|
||||
cols = make(map[string]string, len(fields))
|
||||
for _, f := range fields {
|
||||
if c, ok := s.Columns[f]; ok {
|
||||
cols[f] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(cols) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
names := make([]string, 0, len(cols))
|
||||
colNames := make([]string, 0, len(cols))
|
||||
for field, col := range cols {
|
||||
names = append(names, field)
|
||||
colNames = append(colNames, col)
|
||||
}
|
||||
selector := sql.Dialect(s.Driver.Dialect()).
|
||||
Select(colNames...).
|
||||
From(sql.Table(s.Table))
|
||||
if s.Predicate != nil {
|
||||
s.Predicate(selector)
|
||||
}
|
||||
query, args := selector.Query()
|
||||
rows := &sql.Rows{}
|
||||
if err := s.Driver.Query(ctx, query, args, rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var keys []ent.BlobKey
|
||||
for rows.Next() {
|
||||
vals := make([]*string, len(colNames))
|
||||
ptrs := make([]any, len(colNames))
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
if err := rows.Scan(ptrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, v := range vals {
|
||||
if v != nil && *v != "" {
|
||||
keys = append(keys, ent.BlobKey{Field: names[i], Key: *v})
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys, rows.Err()
|
||||
}
|
||||
182
dialect/sql/sqlgraph/blob_test.go
Normal file
182
dialect/sql/sqlgraph/blob_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package sqlgraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBlobSpecReferencedBlobKeys(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dialect string
|
||||
keys []ent.BlobKey
|
||||
expect func(sqlmock.Sqlmock)
|
||||
want []ent.BlobKey
|
||||
}{
|
||||
{
|
||||
name: "keys held by a row",
|
||||
dialect: dialect.MySQL,
|
||||
keys: []ent.BlobKey{
|
||||
{Field: "content", Key: "k1"},
|
||||
{Field: "content", Key: "k2"},
|
||||
},
|
||||
expect: func(m sqlmock.Sqlmock) {
|
||||
m.ExpectQuery(escape("SELECT DISTINCT `content_key` FROM `documents` WHERE `content_key` IN (?, ?)")).
|
||||
WithArgs("k1", "k2").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"content_key"}).AddRow("k1"))
|
||||
},
|
||||
want: []ent.BlobKey{{Field: "content", Key: "k1"}},
|
||||
},
|
||||
{
|
||||
name: "keys held by no row",
|
||||
dialect: dialect.SQLite,
|
||||
keys: []ent.BlobKey{{Field: "content", Key: "gone"}},
|
||||
expect: func(m sqlmock.Sqlmock) {
|
||||
m.ExpectQuery(escape("SELECT DISTINCT `content_key` FROM `documents` WHERE `content_key` IN (?)")).
|
||||
WithArgs("gone").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"content_key"}))
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
// The mutation's own rows are not filtered out here -- callers time the
|
||||
// lookup so those rows already read the way they will settle.
|
||||
name: "the mutation's predicate is not applied",
|
||||
dialect: dialect.Postgres,
|
||||
keys: []ent.BlobKey{{Field: "content", Key: "k1"}},
|
||||
expect: func(m sqlmock.Sqlmock) {
|
||||
m.ExpectQuery(escape(`SELECT DISTINCT "content_key" FROM "documents" WHERE "content_key" IN ($1)`)).
|
||||
WithArgs("k1").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"content_key"}).AddRow("k1"))
|
||||
},
|
||||
want: []ent.BlobKey{{Field: "content", Key: "k1"}},
|
||||
},
|
||||
{
|
||||
name: "duplicate keys are looked up once",
|
||||
dialect: dialect.MySQL,
|
||||
keys: []ent.BlobKey{
|
||||
{Field: "content", Key: "k1"},
|
||||
{Field: "content", Key: "k1"},
|
||||
},
|
||||
expect: func(m sqlmock.Sqlmock) {
|
||||
m.ExpectQuery(escape("SELECT DISTINCT `content_key` FROM `documents` WHERE `content_key` IN (?)")).
|
||||
WithArgs("k1").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"content_key"}).AddRow("k1"))
|
||||
},
|
||||
want: []ent.BlobKey{{Field: "content", Key: "k1"}},
|
||||
},
|
||||
{
|
||||
name: "unknown fields and empty keys are skipped",
|
||||
dialect: dialect.MySQL,
|
||||
keys: []ent.BlobKey{
|
||||
{Field: "nosuchfield", Key: "k1"},
|
||||
{Field: "content", Key: ""},
|
||||
},
|
||||
expect: func(sqlmock.Sqlmock) {},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
tt.expect(mock)
|
||||
spec := &BlobSpec{
|
||||
Driver: sql.OpenDB(tt.dialect, db),
|
||||
Table: "documents",
|
||||
// Set to prove ReferencedBlobKeys ignores it.
|
||||
Predicate: func(s *sql.Selector) { s.Where(sql.EQ("id", 7)) },
|
||||
Columns: map[string]string{"content": "content_key", "meta": "meta_key"},
|
||||
CheckRefs: map[string]bool{"content": true},
|
||||
}
|
||||
referenced, err := spec.ReferencedBlobKeys(context.Background(), tt.keys)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, referenced)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A key set larger than maxBlobKeysPerQuery is split across statements so the
|
||||
// query stays clear of driver placeholder limits.
|
||||
func TestBlobSpecReferencedBlobKeysChunks(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
var (
|
||||
keys = make([]ent.BlobKey, 0, maxBlobKeysPerQuery+1)
|
||||
first = make([]driver.Value, 0, maxBlobKeysPerQuery)
|
||||
)
|
||||
for i := range maxBlobKeysPerQuery + 1 {
|
||||
key := fmt.Sprintf("k%d", i)
|
||||
keys = append(keys, ent.BlobKey{Field: "content", Key: key})
|
||||
if i < maxBlobKeysPerQuery {
|
||||
first = append(first, key)
|
||||
}
|
||||
}
|
||||
// The first statement carries a full chunk, the leftover key its own. Matched
|
||||
// loosely — escape anchors the pattern, and the argument list asserts the size.
|
||||
mock.ExpectQuery("SELECT DISTINCT").
|
||||
WithArgs(first...).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"content_key"}).AddRow(keys[0].Key))
|
||||
mock.ExpectQuery(escape("SELECT DISTINCT `content_key` FROM `documents` WHERE `content_key` IN (?)")).
|
||||
WithArgs(keys[maxBlobKeysPerQuery].Key).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"content_key"}).AddRow(keys[maxBlobKeysPerQuery].Key))
|
||||
spec := &BlobSpec{
|
||||
Driver: sql.OpenDB(dialect.MySQL, db),
|
||||
Table: "documents",
|
||||
Columns: map[string]string{"content": "content_key"},
|
||||
CheckRefs: map[string]bool{"content": true},
|
||||
}
|
||||
referenced, err := spec.ReferencedBlobKeys(context.Background(), keys)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []ent.BlobKey{keys[0], keys[maxBlobKeysPerQuery]}, referenced)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// Empty input, and fields that did not opt in, must not reach the database.
|
||||
func TestBlobSpecReferencedBlobKeysNoop(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
spec := &BlobSpec{
|
||||
Driver: sql.OpenDB(dialect.MySQL, db),
|
||||
Table: "documents",
|
||||
Columns: map[string]string{"content": "content_key"},
|
||||
CheckRefs: map[string]bool{"content": true},
|
||||
}
|
||||
keys := []ent.BlobKey{{Field: "content", Key: "k"}}
|
||||
referenced, err := spec.ReferencedBlobKeys(context.Background(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, referenced)
|
||||
|
||||
// No field opted in: the whole lookup is skipped.
|
||||
spec.CheckRefs = nil
|
||||
referenced, err = spec.ReferencedBlobKeys(context.Background(), keys)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, referenced)
|
||||
|
||||
// Some other field opted in, but not this one.
|
||||
spec.CheckRefs = map[string]bool{"thumbnail": true}
|
||||
referenced, err = spec.ReferencedBlobKeys(context.Background(), keys)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, referenced)
|
||||
|
||||
spec.CheckRefs = map[string]bool{"content": true}
|
||||
spec.Columns = nil
|
||||
referenced, err = spec.ReferencedBlobKeys(context.Background(), keys)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, referenced)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
334
dialect/sql/sqlgraph/entql.go
Normal file
334
dialect/sql/sqlgraph/entql.go
Normal file
@@ -0,0 +1,334 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package sqlgraph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/entql"
|
||||
)
|
||||
|
||||
type (
|
||||
// A Schema holds a representation of ent/schema at runtime. Each Node
|
||||
// represents a single schema-type and its relations in the graph (storage).
|
||||
//
|
||||
// It is used for translating common graph traversal operations to the
|
||||
// underlying SQL storage. For example, an operation like `has_edge(E)`,
|
||||
// will be translated to an SQL lookup based on the relation type and the
|
||||
// FK configuration.
|
||||
Schema struct {
|
||||
Nodes []*Node
|
||||
}
|
||||
|
||||
// A Node in the graph holds the SQL information for an ent/schema.
|
||||
Node struct {
|
||||
NodeSpec
|
||||
|
||||
// Type holds the node type (schema name).
|
||||
Type string
|
||||
|
||||
// Fields maps from field names to their spec.
|
||||
Fields map[string]*FieldSpec
|
||||
|
||||
// Edges maps from edge names to their spec.
|
||||
Edges map[string]struct {
|
||||
To *Node
|
||||
Spec *EdgeSpec
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// AddE adds an edge to the graph. It fails, if one of the node
|
||||
// types is missing.
|
||||
//
|
||||
// g.AddE("pets", spec, "user", "pet")
|
||||
// g.AddE("friends", spec, "user", "user")
|
||||
func (g *Schema) AddE(name string, spec *EdgeSpec, from, to string) error {
|
||||
var fromT, toT *Node
|
||||
for i := range g.Nodes {
|
||||
t := g.Nodes[i].Type
|
||||
if t == from {
|
||||
fromT = g.Nodes[i]
|
||||
}
|
||||
if t == to {
|
||||
toT = g.Nodes[i]
|
||||
}
|
||||
}
|
||||
if fromT == nil || toT == nil {
|
||||
return fmt.Errorf("from/to type was not found")
|
||||
}
|
||||
if fromT.Edges == nil {
|
||||
fromT.Edges = make(map[string]struct {
|
||||
To *Node
|
||||
Spec *EdgeSpec
|
||||
})
|
||||
}
|
||||
fromT.Edges[name] = struct {
|
||||
To *Node
|
||||
Spec *EdgeSpec
|
||||
}{
|
||||
To: toT,
|
||||
Spec: spec,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustAddE is like AddE but panics if the edge can be added to the graph.
|
||||
func (g *Schema) MustAddE(name string, spec *EdgeSpec, from, to string) {
|
||||
if err := g.AddE(name, spec, from, to); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// EvalP evaluates the entql predicate on the given selector (query builder).
|
||||
func (g *Schema) EvalP(nodeType string, p entql.P, selector *sql.Selector) error {
|
||||
var node *Node
|
||||
for i := range g.Nodes {
|
||||
if g.Nodes[i].Type == nodeType {
|
||||
node = g.Nodes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if node == nil {
|
||||
return fmt.Errorf("node %s was not found in the graph schema", nodeType)
|
||||
}
|
||||
pr, err := evalExpr(node, selector, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selector.Where(pr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FuncSelector represents a selector function to be used as an entql foreign-function.
|
||||
const FuncSelector entql.Func = "func_selector"
|
||||
|
||||
// wrappedFunc wraps the selector-function to an ent-expression.
|
||||
type wrappedFunc struct {
|
||||
entql.Expr
|
||||
Func func(*sql.Selector)
|
||||
}
|
||||
|
||||
// WrapFunc wraps a selector-func with an entql call expression.
|
||||
func WrapFunc(s func(*sql.Selector)) *entql.CallExpr {
|
||||
return &entql.CallExpr{
|
||||
Func: FuncSelector,
|
||||
Args: []entql.Expr{wrappedFunc{Func: s}},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
binary = [...]sql.Op{
|
||||
entql.OpEQ: sql.OpEQ,
|
||||
entql.OpNEQ: sql.OpNEQ,
|
||||
entql.OpGT: sql.OpGT,
|
||||
entql.OpGTE: sql.OpGTE,
|
||||
entql.OpLT: sql.OpLT,
|
||||
entql.OpLTE: sql.OpLTE,
|
||||
entql.OpIn: sql.OpIn,
|
||||
entql.OpNotIn: sql.OpNotIn,
|
||||
}
|
||||
nary = [...]func(...*sql.Predicate) *sql.Predicate{
|
||||
entql.OpAnd: sql.And,
|
||||
entql.OpOr: sql.Or,
|
||||
}
|
||||
strFunc = map[entql.Func]func(string, string) *sql.Predicate{
|
||||
entql.FuncContains: sql.Contains,
|
||||
entql.FuncContainsFold: sql.ContainsFold,
|
||||
entql.FuncEqualFold: sql.EqualFold,
|
||||
entql.FuncHasPrefix: sql.HasPrefix,
|
||||
entql.FuncHasSuffix: sql.HasSuffix,
|
||||
}
|
||||
nullFunc = [...]func(string) *sql.Predicate{
|
||||
entql.OpEQ: sql.IsNull,
|
||||
entql.OpNEQ: sql.NotNull,
|
||||
}
|
||||
)
|
||||
|
||||
// state represents the state of a predicate evaluation.
|
||||
// Note that, the evaluation output is a predicate to be
|
||||
// applied on the database.
|
||||
type state struct {
|
||||
sql.Builder
|
||||
context *Node
|
||||
selector *sql.Selector
|
||||
}
|
||||
|
||||
// evalExpr evaluates the entql expression and returns a new SQL predicate to be applied on the database.
|
||||
func evalExpr(context *Node, selector *sql.Selector, expr entql.Expr) (p *sql.Predicate, err error) {
|
||||
ex := &state{
|
||||
context: context,
|
||||
selector: selector,
|
||||
}
|
||||
defer catch(&err)
|
||||
p = ex.evalExpr(expr)
|
||||
return
|
||||
}
|
||||
|
||||
// evalExpr evaluates any expression.
|
||||
func (e *state) evalExpr(expr entql.Expr) *sql.Predicate {
|
||||
switch expr := expr.(type) {
|
||||
case *entql.BinaryExpr:
|
||||
return e.evalBinary(expr)
|
||||
case *entql.UnaryExpr:
|
||||
return sql.Not(e.evalExpr(expr.X))
|
||||
case *entql.NaryExpr:
|
||||
ps := make([]*sql.Predicate, len(expr.Xs))
|
||||
for i, x := range expr.Xs {
|
||||
ps[i] = e.evalExpr(x)
|
||||
}
|
||||
return nary[expr.Op](ps...)
|
||||
case *entql.CallExpr:
|
||||
switch expr.Func {
|
||||
case entql.FuncHasPrefix, entql.FuncHasSuffix, entql.FuncContains, entql.FuncEqualFold, entql.FuncContainsFold:
|
||||
expect(len(expr.Args) == 2, "invalid number of arguments for %s", expr.Func)
|
||||
f, ok := expr.Args[0].(*entql.Field)
|
||||
expect(ok, "*entql.Field, got %T", expr.Args[0])
|
||||
v, ok := expr.Args[1].(*entql.Value)
|
||||
expect(ok, "*entql.Value, got %T", expr.Args[1])
|
||||
s, ok := v.V.(string)
|
||||
expect(ok, "string value, got %T", v.V)
|
||||
return strFunc[expr.Func](e.field(f), s)
|
||||
case entql.FuncHasEdge:
|
||||
expect(len(expr.Args) > 0, "invalid number of arguments for %s", expr.Func)
|
||||
edge, ok := expr.Args[0].(*entql.Edge)
|
||||
expect(ok, "*entql.Edge, got %T", expr.Args[0])
|
||||
return e.evalEdge(edge.Name, expr.Args[1:]...)
|
||||
}
|
||||
}
|
||||
panic("invalid")
|
||||
}
|
||||
|
||||
// evalBinary evaluates binary expressions.
|
||||
func (e *state) evalBinary(expr *entql.BinaryExpr) *sql.Predicate {
|
||||
switch expr.Op {
|
||||
case entql.OpOr:
|
||||
return sql.Or(e.evalExpr(expr.X), e.evalExpr(expr.Y))
|
||||
case entql.OpAnd:
|
||||
return sql.And(e.evalExpr(expr.X), e.evalExpr(expr.Y))
|
||||
case entql.OpEQ, entql.OpNEQ:
|
||||
if expr.Y == (*entql.Value)(nil) {
|
||||
f, ok := expr.X.(*entql.Field)
|
||||
expect(ok, "*entql.Field, got %T", expr.Y)
|
||||
return nullFunc[expr.Op](e.field(f))
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
field, ok := expr.X.(*entql.Field)
|
||||
expect(ok, "expr.X to be *entql.Field (got %T)", expr.X)
|
||||
_, ok = expr.Y.(*entql.Field)
|
||||
if !ok {
|
||||
_, ok = expr.Y.(*entql.Value)
|
||||
}
|
||||
expect(ok, "expr.Y to be *entql.Field or *entql.Value (got %T)", expr.X)
|
||||
switch x := expr.Y.(type) {
|
||||
case *entql.Field:
|
||||
return sql.ColumnsOp(e.field(field), e.field(x), binary[expr.Op])
|
||||
case *entql.Value:
|
||||
c := e.field(field)
|
||||
return sql.P(func(b *sql.Builder) {
|
||||
b.Ident(c).WriteOp(binary[expr.Op])
|
||||
args(b, x)
|
||||
})
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// evalEdge evaluates has-edge and has-edge-with calls.
|
||||
func (e *state) evalEdge(name string, exprs ...entql.Expr) *sql.Predicate {
|
||||
edge, ok := e.context.Edges[name]
|
||||
expect(ok, "edge %q was not found for node %q", name, e.context.Type)
|
||||
var fromC, toC string
|
||||
switch {
|
||||
case edge.To.ID != nil:
|
||||
toC = edge.To.ID.Column
|
||||
// Edge-owner points to its edge schema.
|
||||
case edge.To.CompositeID != nil && !edge.Spec.Inverse:
|
||||
toC = edge.To.CompositeID[0].Column
|
||||
// Edge-backref points to its edge schema.
|
||||
case edge.To.CompositeID != nil && edge.Spec.Inverse:
|
||||
toC = edge.To.CompositeID[1].Column
|
||||
default:
|
||||
panic(evalError{fmt.Sprintf("expect id definition for edge %q", name)})
|
||||
}
|
||||
switch {
|
||||
case e.context.ID != nil:
|
||||
fromC = e.context.ID.Column
|
||||
case e.context.CompositeID != nil && (edge.Spec.Rel == M2O || (edge.Spec.Rel == O2O && edge.Spec.Inverse)):
|
||||
// An edge-schema with a composite id can query
|
||||
// only edges that it owns (holds the foreign-key).
|
||||
default:
|
||||
panic(evalError{fmt.Sprintf("unexpected edge-query from an edge-schema %q", e.context.Type)})
|
||||
}
|
||||
step := NewStep(
|
||||
From(e.context.Table, fromC),
|
||||
To(edge.To.Table, toC),
|
||||
Edge(edge.Spec.Rel, edge.Spec.Inverse, edge.Spec.Table, edge.Spec.Columns...),
|
||||
)
|
||||
selector := e.selector.Clone().SetP(nil)
|
||||
selector.SetTotal(e.Total())
|
||||
if len(exprs) == 0 {
|
||||
HasNeighbors(selector, step)
|
||||
return selector.P()
|
||||
}
|
||||
HasNeighborsWith(selector, step, func(s *sql.Selector) {
|
||||
for _, expr := range exprs {
|
||||
if cx, ok := expr.(*entql.CallExpr); ok && cx.Func == FuncSelector {
|
||||
expect(len(cx.Args) == 1, "invalid number of arguments for %s", FuncSelector)
|
||||
wrapped, ok := cx.Args[0].(wrappedFunc)
|
||||
expect(ok, "invalid argument for %s: %T", FuncSelector, cx.Args[0])
|
||||
wrapped.Func(s)
|
||||
} else {
|
||||
p, err := evalExpr(edge.To, s, expr)
|
||||
expect(err == nil, "edge evaluation failed for %s->%s: %s", e.context.Type, name, err)
|
||||
s.Where(p)
|
||||
}
|
||||
}
|
||||
})
|
||||
return selector.P()
|
||||
}
|
||||
|
||||
func (e *state) field(f *entql.Field) string {
|
||||
_, ok := e.context.Fields[f.Name]
|
||||
expect(ok || e.context.ID.Column == f.Name, "field %q was not found for node %q", f.Name, e.context.Type)
|
||||
return e.selector.C(f.Name)
|
||||
}
|
||||
|
||||
func args(b *sql.Builder, v *entql.Value) {
|
||||
vs, ok := v.V.([]any)
|
||||
if !ok {
|
||||
b.Arg(v.V)
|
||||
return
|
||||
}
|
||||
b.WriteByte('(').Args(vs...).WriteByte(')')
|
||||
}
|
||||
|
||||
// expect panics if the condition is false.
|
||||
func expect(cond bool, msg string, args ...any) {
|
||||
if !cond {
|
||||
panic(evalError{fmt.Sprintf("expect "+msg, args...)})
|
||||
}
|
||||
}
|
||||
|
||||
type evalError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (p evalError) Error() string {
|
||||
return fmt.Sprintf("sqlgraph: %s", p.msg)
|
||||
}
|
||||
|
||||
func catch(err *error) {
|
||||
if e := recover(); e != nil {
|
||||
xerr, ok := e.(evalError)
|
||||
if !ok {
|
||||
panic(e)
|
||||
}
|
||||
*err = xerr
|
||||
}
|
||||
}
|
||||
171
dialect/sql/sqlgraph/entql_test.go
Normal file
171
dialect/sql/sqlgraph/entql_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package sqlgraph
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/entql"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGraph_AddE(t *testing.T) {
|
||||
g := &Schema{
|
||||
Nodes: []*Node{{Type: "user"}, {Type: "pet"}},
|
||||
}
|
||||
err := g.AddE("pets", &EdgeSpec{Rel: O2M}, "user", "pet")
|
||||
assert.NoError(t, err)
|
||||
err = g.AddE("owner", &EdgeSpec{Rel: O2M}, "pet", "user")
|
||||
assert.NoError(t, err)
|
||||
err = g.AddE("groups", &EdgeSpec{Rel: M2M}, "pet", "groups")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGraph_EvalP(t *testing.T) {
|
||||
g := &Schema{
|
||||
Nodes: []*Node{
|
||||
{
|
||||
Type: "user",
|
||||
NodeSpec: NodeSpec{
|
||||
Table: "users",
|
||||
ID: &FieldSpec{Column: "uid"},
|
||||
},
|
||||
Fields: map[string]*FieldSpec{
|
||||
"name": {Column: "name", Type: field.TypeString},
|
||||
"last": {Column: "last", Type: field.TypeString},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "pet",
|
||||
NodeSpec: NodeSpec{
|
||||
Table: "pets",
|
||||
ID: &FieldSpec{Column: "pid"},
|
||||
},
|
||||
Fields: map[string]*FieldSpec{
|
||||
"name": {Column: "name", Type: field.TypeString},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "group",
|
||||
NodeSpec: NodeSpec{
|
||||
Table: "groups",
|
||||
ID: &FieldSpec{Column: "gid"},
|
||||
},
|
||||
Fields: map[string]*FieldSpec{
|
||||
"name": {Column: "name", Type: field.TypeString},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := g.AddE("pets", &EdgeSpec{Rel: O2M, Table: "pets", Columns: []string{"owner_id"}}, "user", "pet")
|
||||
require.NoError(t, err)
|
||||
err = g.AddE("owner", &EdgeSpec{Rel: M2O, Inverse: true, Table: "pets", Columns: []string{"owner_id"}}, "pet", "user")
|
||||
require.NoError(t, err)
|
||||
err = g.AddE("groups", &EdgeSpec{Rel: M2M, Table: "user_groups", Columns: []string{"user_id", "group_id"}}, "user", "group")
|
||||
require.NoError(t, err)
|
||||
err = g.AddE("users", &EdgeSpec{Rel: M2M, Inverse: true, Table: "user_groups", Columns: []string{"user_id", "group_id"}}, "group", "user")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
s *sql.Selector
|
||||
p entql.P
|
||||
wantQuery string
|
||||
wantArgs []any
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")),
|
||||
p: entql.FieldHasPrefix("name", "a"),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "users"."name" LIKE $1`,
|
||||
wantArgs: []any{"a%"},
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")).
|
||||
Where(sql.EQ("age", 1)),
|
||||
p: entql.FieldHasPrefix("name", "a"),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "age" = $1 AND "users"."name" LIKE $2`,
|
||||
wantArgs: []any{1, "a%"},
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")).
|
||||
Where(sql.EQ("age", 1)),
|
||||
p: entql.FieldHasPrefix("name", "a"),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "age" = $1 AND "users"."name" LIKE $2`,
|
||||
wantArgs: []any{1, "a%"},
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")),
|
||||
p: entql.EQ(entql.F("name"), entql.F("last")),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "users"."name" = "users"."last"`,
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")),
|
||||
p: entql.EQ(entql.F("name"), entql.F("last")),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "users"."name" = "users"."last"`,
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")),
|
||||
p: entql.And(entql.FieldNil("name"), entql.FieldNotNil("last")),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "users"."name" IS NULL AND "users"."last" IS NOT NULL`,
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")).
|
||||
Where(sql.EQ("foo", "bar")),
|
||||
p: entql.Or(entql.FieldEQ("name", "foo"), entql.FieldEQ("name", "baz")),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "foo" = $1 AND ("users"."name" = $2 OR "users"."name" = $3)`,
|
||||
wantArgs: []any{"bar", "foo", "baz"},
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")),
|
||||
p: entql.HasEdge("pets"),
|
||||
wantQuery: `SELECT * FROM "users" WHERE EXISTS (SELECT "pets"."owner_id" FROM "pets" WHERE "users"."uid" = "pets"."owner_id")`,
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")),
|
||||
p: entql.HasEdge("groups"),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "users"."uid" IN (SELECT "user_groups"."user_id" FROM "user_groups")`,
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")),
|
||||
p: entql.HasEdgeWith("pets", entql.Or(entql.FieldEQ("name", "pedro"), entql.FieldEQ("name", "xabi"))),
|
||||
wantQuery: `SELECT * FROM "users" WHERE EXISTS (SELECT "pets"."owner_id" FROM "pets" WHERE "users"."uid" = "pets"."owner_id" AND ("pets"."name" = $1 OR "pets"."name" = $2))`,
|
||||
wantArgs: []any{"pedro", "xabi"},
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")).Where(sql.EQ("active", true)),
|
||||
p: entql.HasEdgeWith("groups", entql.Or(entql.FieldEQ("name", "GitHub"), entql.FieldEQ("name", "GitLab"))),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "active" AND "users"."uid" IN (SELECT "user_groups"."user_id" FROM "user_groups" JOIN "groups" AS "t1" ON "user_groups"."group_id" = "t1"."gid" WHERE "t1"."name" = $1 OR "t1"."name" = $2)`,
|
||||
wantArgs: []any{"GitHub", "GitLab"},
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")).Where(sql.EQ("active", true)),
|
||||
p: entql.And(entql.HasEdge("pets"), entql.HasEdge("groups"), entql.EQ(entql.F("name"), entql.F("uid"))),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "active" AND (EXISTS (SELECT "pets"."owner_id" FROM "pets" WHERE "users"."uid" = "pets"."owner_id") AND "users"."uid" IN (SELECT "user_groups"."user_id" FROM "user_groups") AND "users"."name" = "users"."uid")`,
|
||||
},
|
||||
{
|
||||
s: sql.Dialect(dialect.Postgres).Select().From(sql.Table("users")).Where(sql.EQ("active", true)),
|
||||
p: entql.HasEdgeWith("pets", entql.FieldEQ("name", "pedro"), WrapFunc(func(s *sql.Selector) {
|
||||
s.Where(sql.EQ("owner_id", 10))
|
||||
})),
|
||||
wantQuery: `SELECT * FROM "users" WHERE "active" AND EXISTS (SELECT "pets"."owner_id" FROM "pets" WHERE ("users"."uid" = "pets"."owner_id" AND "pets"."name" = $1) AND "owner_id" = $2)`,
|
||||
wantArgs: []any{"pedro", 10},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
err = g.EvalP("user", tt.p, tt.s)
|
||||
require.Equal(t, tt.wantErr, err != nil, err)
|
||||
query, args := tt.s.Query()
|
||||
require.Equal(t, tt.wantQuery, query)
|
||||
require.Equal(t, tt.wantArgs, args)
|
||||
})
|
||||
}
|
||||
}
|
||||
74
dialect/sql/sqlgraph/errors.go
Normal file
74
dialect/sql/sqlgraph/errors.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package sqlgraph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsConstraintError returns true if the error resulted from a database constraint violation.
|
||||
func IsConstraintError(err error) bool {
|
||||
var e *ConstraintError
|
||||
return errors.As(err, &e) ||
|
||||
IsUniqueConstraintError(err) ||
|
||||
IsForeignKeyConstraintError(err) ||
|
||||
IsCheckConstraintError(err)
|
||||
}
|
||||
|
||||
// IsUniqueConstraintError reports if the error resulted from a DB uniqueness constraint violation.
|
||||
// e.g. duplicate value in unique index.
|
||||
func IsUniqueConstraintError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
for _, s := range []string{
|
||||
"Error 1062", // MySQL
|
||||
"violates unique constraint", // Postgres
|
||||
"UNIQUE constraint failed", // SQLite
|
||||
} {
|
||||
if strings.Contains(err.Error(), s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsForeignKeyConstraintError reports if the error resulted from a database foreign-key constraint violation.
|
||||
// e.g. parent row does not exist.
|
||||
func IsForeignKeyConstraintError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
for _, s := range []string{
|
||||
"Error 1451", // MySQL (Cannot delete or update a parent row).
|
||||
"Error 1452", // MySQL (Cannot add or update a child row).
|
||||
"violates foreign key constraint", // Postgres
|
||||
"FOREIGN KEY constraint failed", // SQLite
|
||||
} {
|
||||
if strings.Contains(err.Error(), s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCheckConstraintError reports if the error resulted from a database check constraint violation.
|
||||
// e.g. a value does not satisfy a check condition.
|
||||
func IsCheckConstraintError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
for _, s := range []string{
|
||||
"Error 3819", // MySQL
|
||||
"violates check constraint", // Postgres
|
||||
"CHECK constraint failed", // SQLite
|
||||
} {
|
||||
if strings.Contains(err.Error(), s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
2076
dialect/sql/sqlgraph/graph.go
Normal file
2076
dialect/sql/sqlgraph/graph.go
Normal file
File diff suppressed because it is too large
Load Diff
2755
dialect/sql/sqlgraph/graph_test.go
Normal file
2755
dialect/sql/sqlgraph/graph_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user