snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Atlas CI Public / lint (push) Failing after 13s
Atlas CI Public / sync (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:12:36 +05:00
commit 4b3ad0b611
2680 changed files with 719472 additions and 0 deletions

37
dialect/sql/bench_test.go Normal file
View File

@@ -0,0 +1,37 @@
// 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 sql
import (
"testing"
"entgo.io/ent/dialect"
)
func BenchmarkInsertBuilder_Default(b *testing.B) {
for _, d := range []string{dialect.SQLite, dialect.MySQL, dialect.Postgres} {
b.Run(d, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
Dialect(d).Insert("users").Default().Returning("id").Query()
}
})
}
}
func BenchmarkInsertBuilder_Small(b *testing.B) {
for _, d := range []string{dialect.SQLite, dialect.MySQL, dialect.Postgres} {
b.Run(d, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
Dialect(d).Insert("users").
Columns("id", "age", "first_name", "last_name", "nickname", "spouse_id", "created_at", "updated_at").
Values(1, 30, "Ariel", "Mashraki", "a8m", 2, "2009-11-10 23:00:00", "2009-11-10 23:00:00").
Returning("id").
Query()
}
})
}
}

3603
dialect/sql/builder.go Normal file

File diff suppressed because it is too large Load Diff

2407
dialect/sql/builder_test.go Normal file

File diff suppressed because it is too large Load Diff

305
dialect/sql/driver.go Normal file
View File

@@ -0,0 +1,305 @@
// 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 sql
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"strconv"
"strings"
"entgo.io/ent/dialect"
)
// Driver is a dialect.Driver implementation for SQL based databases.
type Driver struct {
Conn
dialect string
}
// NewDriver creates a new Driver with the given Conn and dialect.
func NewDriver(dialect string, c Conn) *Driver {
return &Driver{dialect: dialect, Conn: c}
}
// Open wraps the database/sql.Open method and returns a dialect.Driver that implements the an ent/dialect.Driver interface.
func Open(dialect, source string) (*Driver, error) {
db, err := sql.Open(dialect, source)
if err != nil {
return nil, err
}
return NewDriver(dialect, Conn{db, dialect}), nil
}
// OpenDB wraps the given database/sql.DB method with a Driver.
func OpenDB(dialect string, db *sql.DB) *Driver {
return NewDriver(dialect, Conn{db, dialect})
}
// DB returns the underlying *sql.DB instance.
func (d Driver) DB() *sql.DB {
return d.ExecQuerier.(*sql.DB)
}
// Dialect implements the dialect.Dialect method.
func (d Driver) Dialect() string {
// If the underlying driver is wrapped with a telemetry driver.
for _, name := range []string{dialect.MySQL, dialect.SQLite, dialect.Postgres} {
if strings.HasPrefix(d.dialect, name) {
return name
}
}
return d.dialect
}
// Tx starts and returns a transaction.
func (d *Driver) Tx(ctx context.Context) (dialect.Tx, error) {
return d.BeginTx(ctx, nil)
}
// BeginTx starts a transaction with options.
func (d *Driver) BeginTx(ctx context.Context, opts *TxOptions) (dialect.Tx, error) {
tx, err := d.DB().BeginTx(ctx, opts)
if err != nil {
return nil, err
}
return &Tx{
Conn: Conn{tx, d.dialect},
Tx: tx,
}, nil
}
// Close closes the underlying connection.
func (d *Driver) Close() error { return d.DB().Close() }
// Tx implements dialect.Tx interface.
type Tx struct {
Conn
driver.Tx
}
// ctyVarsKey is the key used for attaching and reading the context variables.
type ctxVarsKey struct{}
// sessionVars holds sessions/transactions variables to set before every statement.
type sessionVars struct {
vars []struct{ k, v string }
}
// WithVar returns a new context that holds the session variable to be executed before every query.
func WithVar(ctx context.Context, name, value string) context.Context {
sv, _ := ctx.Value(ctxVarsKey{}).(sessionVars)
sv.vars = append(sv.vars, struct {
k, v string
}{
k: name,
v: value,
})
return context.WithValue(ctx, ctxVarsKey{}, sv)
}
// VarFromContext returns the session variable value from the context.
func VarFromContext(ctx context.Context, name string) (string, bool) {
sv, _ := ctx.Value(ctxVarsKey{}).(sessionVars)
for _, s := range sv.vars {
if s.k == name {
return s.v, true
}
}
return "", false
}
// WithIntVar calls WithVar with the string representation of the value.
func WithIntVar(ctx context.Context, name string, value int) context.Context {
return WithVar(ctx, name, strconv.Itoa(value))
}
// ExecQuerier wraps the standard Exec and Query methods.
type ExecQuerier interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}
// Conn implements dialect.ExecQuerier given ExecQuerier.
type Conn struct {
ExecQuerier
dialect string
}
// Exec implements the dialect.Exec method.
func (c Conn) Exec(ctx context.Context, query string, args, v any) (rerr error) {
argv, ok := args.([]any)
if !ok {
return fmt.Errorf("dialect/sql: invalid type %T. expect []any for args", v)
}
ex, cf, err := c.maySetVars(ctx)
if err != nil {
return err
}
if cf != nil {
defer func() { rerr = errors.Join(rerr, cf()) }()
}
switch v := v.(type) {
case nil:
if _, err := ex.ExecContext(ctx, query, argv...); err != nil {
return err
}
case *sql.Result:
res, err := ex.ExecContext(ctx, query, argv...)
if err != nil {
return err
}
*v = res
default:
return fmt.Errorf("dialect/sql: invalid type %T. expect *sql.Result", v)
}
return nil
}
// Query implements the dialect.Query method.
func (c Conn) Query(ctx context.Context, query string, args, v any) error {
vr, ok := v.(*Rows)
if !ok {
return fmt.Errorf("dialect/sql: invalid type %T. expect *sql.Rows", v)
}
argv, ok := args.([]any)
if !ok {
return fmt.Errorf("dialect/sql: invalid type %T. expect []any for args", args)
}
ex, cf, err := c.maySetVars(ctx)
if err != nil {
return err
}
rows, err := ex.QueryContext(ctx, query, argv...)
if err != nil {
if cf != nil {
err = errors.Join(err, cf())
}
return err
}
*vr = Rows{rows}
if cf != nil {
vr.ColumnScanner = rowsWithCloser{rows, cf}
}
return nil
}
// maySetVars sets the session variables before executing a query.
func (c Conn) maySetVars(ctx context.Context) (ExecQuerier, func() error, error) {
sv, _ := ctx.Value(ctxVarsKey{}).(sessionVars)
if len(sv.vars) == 0 {
return c, nil, nil
}
var (
ex ExecQuerier // Underlying ExecQuerier.
cf func() error // Close function.
reset []string // Reset variables.
seen = make(map[string]struct{}, len(sv.vars))
)
switch e := c.ExecQuerier.(type) {
case *sql.Tx:
ex = e
case *sql.DB:
conn, err := e.Conn(ctx)
if err != nil {
return nil, nil, err
}
ex, cf = conn, conn.Close
}
for _, s := range sv.vars {
if _, ok := seen[s.k]; !ok {
switch c.dialect {
case dialect.Postgres:
reset = append(reset, fmt.Sprintf("RESET %s", s.k))
case dialect.MySQL:
reset = append(reset, fmt.Sprintf("SET %s = NULL", s.k))
}
seen[s.k] = struct{}{}
}
if _, err := ex.ExecContext(ctx, fmt.Sprintf("SET %s = '%s'", s.k, s.v)); err != nil {
if cf != nil {
err = errors.Join(err, cf())
}
return nil, nil, err
}
}
// If there are variables to reset, and we need to return the
// connection to the pool, we need to clean up the variables.
if cls := cf; cf != nil && len(reset) > 0 {
cf = func() error {
for _, q := range reset {
if _, err := ex.ExecContext(ctx, q); err != nil {
return errors.Join(err, cls())
}
}
return cls()
}
}
return ex, cf, nil
}
var _ dialect.Driver = (*Driver)(nil)
type (
// Rows wraps the sql.Rows to avoid locks copy.
Rows struct{ ColumnScanner }
// Result is an alias to sql.Result.
Result = sql.Result
// NullBool is an alias to sql.NullBool.
NullBool = sql.NullBool
// NullInt64 is an alias to sql.NullInt64.
NullInt64 = sql.NullInt64
// NullString is an alias to sql.NullString.
NullString = sql.NullString
// NullFloat64 is an alias to sql.NullFloat64.
NullFloat64 = sql.NullFloat64
// NullTime represents a time.Time that may be null.
NullTime = sql.NullTime
// TxOptions holds the transaction options to be used in DB.BeginTx.
TxOptions = sql.TxOptions
)
// NullScanner implements the sql.Scanner interface such that it
// can be used as a scan destination, similar to the types above.
type NullScanner struct {
S sql.Scanner
Valid bool // Valid is true if the Scan value is not NULL.
}
// Scan implements the Scanner interface.
func (n *NullScanner) Scan(value any) error {
n.Valid = value != nil
if n.Valid {
return n.S.Scan(value)
}
return nil
}
// ColumnScanner is the interface that wraps the standard
// sql.Rows methods used for scanning database rows.
type ColumnScanner interface {
Close() error
ColumnTypes() ([]*sql.ColumnType, error)
Columns() ([]string, error)
Err() error
Next() bool
NextResultSet() bool
Scan(dest ...any) error
}
// rowsWithCloser wraps the ColumnScanner interface with a custom Close hook.
type rowsWithCloser struct {
ColumnScanner
closer func() error
}
// Close closes the underlying ColumnScanner and calls the custom closer.
func (r rowsWithCloser) Close() error {
err := r.ColumnScanner.Close()
return errors.Join(err, r.closer())
}

View File

@@ -0,0 +1,93 @@
// 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 sql
import (
"context"
"testing"
"entgo.io/ent/dialect"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/require"
)
func TestWithVars(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
db.SetMaxOpenConns(1)
drv := OpenDB(dialect.Postgres, db)
mock.ExpectExec("SET foo = 'bar'").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectQuery("SELECT 1").WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
mock.ExpectExec("RESET foo").WillReturnResult(sqlmock.NewResult(0, 0))
rows := &Rows{}
err = drv.Query(
WithVar(context.Background(), "foo", "bar"),
"SELECT 1",
[]any{},
rows,
)
require.NoError(t, err)
require.NoError(t, rows.Close(), "rows should be closed to release the connection")
require.NoError(t, mock.ExpectationsWereMet())
mock.ExpectExec("SET foo = 'bar'").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("SET foo = 'baz'").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectQuery("SELECT 1").WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
mock.ExpectExec("RESET foo").WillReturnResult(sqlmock.NewResult(0, 0))
err = drv.Query(
WithVar(WithVar(context.Background(), "foo", "bar"), "foo", "baz"),
"SELECT 1",
[]any{},
rows,
)
require.NoError(t, err)
require.NoError(t, rows.Close(), "rows should be closed to release the connection")
require.NoError(t, mock.ExpectationsWereMet())
mock.ExpectBegin()
mock.ExpectExec("SET foo = 'bar'").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectQuery("SELECT 1").WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
mock.ExpectCommit()
tx, err := drv.Tx(context.Background())
require.NoError(t, err)
err = tx.Query(
WithVar(context.Background(), "foo", "bar"),
"SELECT 1",
[]any{},
rows,
)
require.NoError(t, err)
require.NoError(t, tx.Commit())
require.NoError(t, mock.ExpectationsWereMet())
// Rows should not be closed to release the session,
// as a transaction is always scoped to a single connection.
mock.ExpectExec("SET foo = 'qux'").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("INSERT INTO users DEFAULT VALUES").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("RESET foo").WillReturnResult(sqlmock.NewResult(0, 0))
err = drv.Exec(
WithVar(context.Background(), "foo", "qux"),
"INSERT INTO users DEFAULT VALUES",
[]any{},
nil,
)
require.NoError(t, err)
require.NoError(t, mock.ExpectationsWereMet())
// No rows are returned, so no need to close them.
mock.ExpectExec("SET foo = 'foo'").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("INSERT INTO users DEFAULT VALUES").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("RESET foo").WillReturnResult(sqlmock.NewResult(0, 0))
err = drv.Exec(
WithVar(context.Background(), "foo", "foo"),
"INSERT INTO users DEFAULT VALUES",
[]any{},
nil,
)
require.NoError(t, err)
require.NoError(t, mock.ExpectationsWereMet())
// No rows are returned, so no need to close them.
}

425
dialect/sql/scan.go Normal file
View File

@@ -0,0 +1,425 @@
// 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 sql
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
)
// ScanOne scans one row to the given value. It fails if the rows holds more than 1 row.
func ScanOne(rows ColumnScanner, v any) error {
columns, err := rows.Columns()
if err != nil {
return fmt.Errorf("sql/scan: failed getting column names: %w", err)
}
if n := len(columns); n != 1 {
return fmt.Errorf("sql/scan: unexpected number of columns: %d", n)
}
if !rows.Next() {
if err := rows.Err(); err != nil {
return err
}
return sql.ErrNoRows
}
if err := rows.Scan(v); err != nil {
return err
}
if rows.Next() {
return fmt.Errorf("sql/scan: expect exactly one row in result set")
}
return rows.Err()
}
// ScanInt64 scans and returns an int64 from the rows.
func ScanInt64(rows ColumnScanner) (int64, error) {
var n int64
if err := ScanOne(rows, &n); err != nil {
return 0, err
}
return n, nil
}
// ScanInt scans and returns an int from the rows.
func ScanInt(rows ColumnScanner) (int, error) {
n, err := ScanInt64(rows)
if err != nil {
return 0, err
}
return int(n), nil
}
// ScanBool scans and returns a boolean from the rows.
func ScanBool(rows ColumnScanner) (bool, error) {
var b bool
if err := ScanOne(rows, &b); err != nil {
return false, err
}
return b, nil
}
// ScanString scans and returns a string from the rows.
func ScanString(rows ColumnScanner) (string, error) {
var s string
if err := ScanOne(rows, &s); err != nil {
return "", err
}
return s, nil
}
// ScanValue scans and returns a driver.Value from the rows.
func ScanValue(rows ColumnScanner) (driver.Value, error) {
var v driver.Value
if err := ScanOne(rows, &v); err != nil {
return "", err
}
return v, nil
}
// ScanSlice scans the given ColumnScanner (basically, sql.Row or sql.Rows) into the given slice.
func ScanSlice(rows ColumnScanner, v any) error {
columns, err := rows.Columns()
if err != nil {
return fmt.Errorf("sql/scan: failed getting column names: %w", err)
}
rv := reflect.ValueOf(v)
switch {
case rv.Kind() != reflect.Ptr:
if t := reflect.TypeOf(v); t != nil {
return fmt.Errorf("sql/scan: ScanSlice(non-pointer %s)", t)
}
fallthrough
case rv.IsNil():
return fmt.Errorf("sql/scan: ScanSlice(nil)")
}
rv = reflect.Indirect(rv)
if k := rv.Kind(); k != reflect.Slice {
return fmt.Errorf("sql/scan: invalid type %s. expected slice as an argument", k)
}
scan, err := scanType(rv.Type().Elem(), columns)
if err != nil {
return err
}
if n, m := len(columns), len(scan.columns); n > m {
return fmt.Errorf("sql/scan: columns do not match (%d > %d)", n, m)
}
for rows.Next() {
values := scan.values()
if err := rows.Scan(values...); err != nil {
return fmt.Errorf("sql/scan: failed scanning rows: %w", err)
}
vv, err := scan.value(values...)
if err != nil {
return err
}
rv.Set(reflect.Append(rv, vv))
}
return rows.Err()
}
// rowScan is the configuration for scanning one sql.Row.
type rowScan struct {
// column types of a row.
columns []reflect.Type
// value functions that converts the row columns (result) to a reflect.Value.
value func(v ...any) (reflect.Value, error)
}
// values returns a []any from the configured column types.
func (r *rowScan) values() []any {
values := make([]any, len(r.columns))
for i := range r.columns {
values[i] = reflect.New(r.columns[i]).Interface()
}
return values
}
// scanType returns rowScan for the given reflect.Type.
func scanType(typ reflect.Type, columns []string) (*rowScan, error) {
switch k := typ.Kind(); {
case assignable(typ):
return &rowScan{
columns: []reflect.Type{typ},
value: func(v ...any) (reflect.Value, error) {
return reflect.Indirect(reflect.ValueOf(v[0])), nil
},
}, nil
case k == reflect.Ptr:
return scanPtr(typ, columns)
case k == reflect.Struct:
return scanStruct(typ, columns)
default:
return nil, fmt.Errorf("sql/scan: unsupported type ([]%s)", k)
}
}
var (
timeType = reflect.TypeOf(time.Time{})
scannerType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
nullJSONType = reflect.TypeOf((*nullJSON)(nil)).Elem()
)
// nullJSON represents a json.RawMessage that may be NULL.
type nullJSON json.RawMessage
// Scan implements the sql.Scanner interface.
func (j *nullJSON) Scan(v interface{}) error {
if v == nil {
return nil
}
*j = v.([]byte)
return nil
}
// assignable reports if the given type can be assigned directly by `Rows.Scan`.
func assignable(typ reflect.Type) bool {
switch k := typ.Kind(); {
case typ.Implements(scannerType):
case k == reflect.Interface && typ.NumMethod() == 0:
case k == reflect.String || k >= reflect.Bool && k <= reflect.Float64:
case (k == reflect.Slice || k == reflect.Array) && typ.Elem().Kind() == reflect.Uint8:
default:
return false
}
return true
}
// scanStruct returns the configuration for scanning a sql.Row into a struct.
func scanStruct(typ reflect.Type, columns []string) (*rowScan, error) {
var (
scan = &rowScan{}
idxs = make([][]int, 0, typ.NumField())
names = make(map[string][]int, typ.NumField())
)
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
// Skip unexported fields.
if f.PkgPath != "" {
continue
}
// Support 1-level embedding to accept types as `type T struct {ent.T; V int}`.
if typ := f.Type; f.Anonymous && typ.Kind() == reflect.Struct {
for j := 0; j < typ.NumField(); j++ {
names[columnName(typ.Field(j))] = []int{i, j}
}
continue
}
names[columnName(f)] = []int{i}
}
for _, c := range columns {
var idx []int
// Normalize columns if necessary,
// for example: COUNT(*) => count.
switch name := strings.Split(c, "(")[0]; {
case names[name] != nil:
idx = names[name]
case names[strings.ToLower(name)] != nil:
idx = names[strings.ToLower(name)]
default:
return nil, fmt.Errorf("sql/scan: missing struct field for column: %s (%s)", c, name)
}
idxs = append(idxs, idx)
rtype := typ.Field(idx[0]).Type
if len(idx) > 1 {
rtype = rtype.Field(idx[1]).Type
}
switch {
// If the field is not support by the standard
// convertAssign, assume it is a JSON field.
case !supportsScan(rtype):
rtype = nullJSONType
// Create a pointer to the actual reflect
// types to accept optional struct fields.
case !nillable(rtype):
rtype = reflect.PtrTo(rtype)
}
scan.columns = append(scan.columns, rtype)
}
scan.value = func(vs ...any) (reflect.Value, error) {
st := reflect.New(typ).Elem()
for i, v := range vs {
rv := reflect.Indirect(reflect.ValueOf(v))
if rv.IsNil() {
continue
}
idx := idxs[i]
rvalue, ft := st.Field(idx[0]), st.Type().Field(idx[0])
if len(idx) > 1 {
// Embedded field.
rvalue, ft = rvalue.Field(idx[1]), ft.Type.Field(idx[1])
}
switch {
case rv.Type() == nullJSONType:
if rv = reflect.Indirect(rv); rv.IsNil() {
continue
}
if err := json.Unmarshal(rv.Bytes(), rvalue.Addr().Interface()); err != nil {
return reflect.Value{}, fmt.Errorf("unmarshal field %q: %w", ft.Name, err)
}
case !nillable(rvalue.Type()):
rv = reflect.Indirect(rv)
fallthrough
default:
rvalue.Set(rv)
}
}
return st, nil
}
return scan, nil
}
// columnName returns the column name of a struct-field.
func columnName(f reflect.StructField) string {
name := strings.ToLower(f.Name)
if tag, ok := f.Tag.Lookup("sql"); ok {
name = tag
} else if tag, ok := f.Tag.Lookup("json"); ok {
name = strings.Split(tag, ",")[0]
}
return name
}
// nillable reports if the reflect-type can have nil value.
func nillable(t reflect.Type) bool {
switch t.Kind() {
case reflect.Interface, reflect.Slice, reflect.Map, reflect.Ptr, reflect.UnsafePointer:
return true
}
return false
}
// scanPtr wraps the underlying type with rowScan.
func scanPtr(typ reflect.Type, columns []string) (*rowScan, error) {
typ = typ.Elem()
scan, err := scanType(typ, columns)
if err != nil {
return nil, err
}
wrap := scan.value
scan.value = func(vs ...any) (reflect.Value, error) {
v, err := wrap(vs...)
if err != nil {
return reflect.Value{}, err
}
pt := reflect.PtrTo(v.Type())
pv := reflect.New(pt.Elem())
pv.Elem().Set(v)
return pv, nil
}
return scan, nil
}
func supportsScan(t reflect.Type) bool {
if t.Implements(scannerType) || reflect.PtrTo(t).Implements(scannerType) {
return true
}
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
switch t.Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.Pointer, reflect.String:
return true
case reflect.Slice:
return t == reflect.TypeOf(sql.RawBytes(nil)) || t == reflect.TypeOf([]byte(nil))
case reflect.Interface:
return t == reflect.TypeOf((*any)(nil)).Elem()
default:
return t == reflect.TypeOf(time.Time{}) || t.Implements(scannerType)
}
}
// UnknownType is a named type to any indicates the info
// needs to be extracted from the underlying rows.
type UnknownType any
// ScanTypeOf returns the type used for scanning column i from the database.
func ScanTypeOf(rows *Rows, i int) any {
unknown := new(any)
ct, err := rows.ColumnTypes()
if err != nil || len(ct) <= i {
return unknown
}
rt := ct[i].ScanType()
if rt.Kind() == reflect.Pointer {
rt = rt.Elem()
}
// Handle NULL values.
switch k := rt.Kind(); k {
case reflect.Bool:
rt = reflect.TypeOf(sql.NullBool{})
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
rt = reflect.TypeOf(sql.NullInt64{})
case reflect.Float32, reflect.Float64:
rt = reflect.TypeOf(sql.NullFloat64{})
case reflect.String:
rt = reflect.TypeOf(sql.NullString{})
default:
if k == reflect.Struct && rt == timeType {
rt = reflect.TypeOf(sql.NullTime{})
}
}
return reflect.New(rt).Interface()
}
// SelectValues maps a selected column to its value.
// Used by the generated code for storing runtime selected columns/expressions.
type SelectValues map[string]any
// Set sets the value of the given column.
func (s *SelectValues) Set(name string, v any) {
if *s == nil {
*s = make(SelectValues)
}
if pv, ok := v.(*any); ok && pv != nil {
v = *pv
}
(*s)[name] = v
}
// Get returns the value of the given column.
func (s SelectValues) Get(name string) (any, error) {
v, ok := s[name]
if !ok {
return nil, fmt.Errorf("%s value was not selected", name)
}
if v == nil {
return nil, nil
}
switch rv := reflect.Indirect(reflect.ValueOf(v)).Interface().(type) {
case NullString:
if rv.Valid {
return rv.String, nil
}
case NullInt64:
if rv.Valid {
return rv.Int64, nil
}
case NullFloat64:
if rv.Valid {
return rv.Float64, nil
}
case NullBool:
if rv.Valid {
return rv.Bool, nil
}
case NullTime:
if rv.Valid {
return rv.Time, nil
}
case sql.RawBytes:
return []byte(rv), nil
default:
return rv, nil
}
return nil, nil
}

367
dialect/sql/scan_test.go Normal file
View File

@@ -0,0 +1,367 @@
// 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 sql
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestScanSlice(t *testing.T) {
mock := sqlmock.NewRows([]string{"name"}).
AddRow("foo").
AddRow("bar")
var v0 []string
require.NoError(t, ScanSlice(toRows(mock), &v0))
require.Equal(t, []string{"foo", "bar"}, v0)
mock = sqlmock.NewRows([]string{"age"}).
AddRow(1).
AddRow(2)
var v1 []int
require.NoError(t, ScanSlice(toRows(mock), &v1))
require.Equal(t, []int{1, 2}, v1)
mock = sqlmock.NewRows([]string{"name", "COUNT(*)"}).
AddRow("foo", 1).
AddRow("bar", 2)
var v2 []struct {
Name string
Count int
}
require.NoError(t, ScanSlice(toRows(mock), &v2))
require.Equal(t, "foo", v2[0].Name)
require.Equal(t, "bar", v2[1].Name)
require.Equal(t, 1, v2[0].Count)
require.Equal(t, 2, v2[1].Count)
mock = sqlmock.NewRows([]string{"nick_name", "COUNT(*)"}).
AddRow("foo", 1).
AddRow("bar", 2)
var v3 []struct {
Count int
Name string `json:"nick_name"`
}
require.NoError(t, ScanSlice(toRows(mock), &v3))
require.Equal(t, "foo", v3[0].Name)
require.Equal(t, "bar", v3[1].Name)
require.Equal(t, 1, v3[0].Count)
require.Equal(t, 2, v3[1].Count)
mock = sqlmock.NewRows([]string{"nick_name", "COUNT(*)"}).
AddRow("foo", 1).
AddRow("bar", 2)
var v4 []*struct {
Count int
Name string `json:"nick_name"`
Ignored string `json:"string"`
}
require.NoError(t, ScanSlice(toRows(mock), &v4))
require.Equal(t, "foo", v4[0].Name)
require.Equal(t, "bar", v4[1].Name)
require.Equal(t, 1, v4[0].Count)
require.Equal(t, 2, v4[1].Count)
mock = sqlmock.NewRows([]string{"nick_name", "COUNT(*)"}).
AddRow("foo", 1).
AddRow("bar", 2)
var v5 []*struct {
Count int
Name string `json:"name" sql:"nick_name"`
}
require.NoError(t, ScanSlice(toRows(mock), &v5))
require.Equal(t, "foo", v5[0].Name)
require.Equal(t, "bar", v5[1].Name)
require.Equal(t, 1, v5[0].Count)
require.Equal(t, 2, v5[1].Count)
mock = sqlmock.NewRows([]string{"age", "name"}).
AddRow(1, nil).
AddRow(nil, "a8m")
var v6 []struct {
Age NullInt64
Name NullString
}
require.NoError(t, ScanSlice(toRows(mock), &v6))
require.EqualValues(t, 1, v6[0].Age.Int64)
require.False(t, v6[0].Name.Valid)
require.False(t, v6[1].Age.Valid)
require.Equal(t, "a8m", v6[1].Name.String)
u1, u2 := uuid.New().String(), uuid.New().String()
mock = sqlmock.NewRows([]string{"ids"}).
AddRow([]byte(u1)).
AddRow([]byte(u2))
var ids []uuid.UUID
require.NoError(t, ScanSlice(toRows(mock), &ids))
require.Equal(t, u1, ids[0].String())
require.Equal(t, u2, ids[1].String())
mock = sqlmock.NewRows([]string{"pids"}).
AddRow([]byte(u1)).
AddRow([]byte(u2))
var pids []*uuid.UUID
require.NoError(t, ScanSlice(toRows(mock), &pids))
require.Equal(t, u1, pids[0].String())
require.Equal(t, u2, pids[1].String())
mock = sqlmock.NewRows([]string{"id", "first", "last"}).
AddRow(10, "Ariel", "Mashraki")
err := ScanSlice(toRows(mock), nil)
require.EqualError(t, err, "sql/scan: ScanSlice(nil)")
type P struct {
_ int
ID int
First string
Last string
}
var p []P
err = ScanSlice(toRows(mock), p)
require.EqualError(t, err, "sql/scan: ScanSlice(non-pointer []sql.P)")
require.NoError(t, ScanSlice(toRows(mock), &p))
require.Equal(t, 10, p[0].ID)
require.Equal(t, "Ariel", p[0].First)
require.Equal(t, "Mashraki", p[0].Last)
var pp []struct{ _, id int }
mock = sqlmock.NewRows([]string{"id"}).
AddRow(10)
err = ScanSlice(toRows(mock), &pp)
require.EqualError(t, err, "sql/scan: missing struct field for column: id (id)")
require.Empty(t, pp)
}
func TestScanSlice_CamelTags(t *testing.T) {
mock := sqlmock.NewRows([]string{"nickName"}).
AddRow("foo").
AddRow("bar")
var v []*struct {
NickName string `json:"nickName"`
}
require.NoError(t, ScanSlice(toRows(mock), &v))
require.Equal(t, "foo", v[0].NickName)
}
func TestScanJSON(t *testing.T) {
mock := sqlmock.NewRows([]string{"v", "p"}).
AddRow([]byte(`{"i": 1, "s":"a8m"}`), []byte(`{"i": 1, "s":"a8m"}`)).
AddRow([]byte(`{"i": 2, "s":"tmr"}`), []byte(`{"i": 2, "s":"tmr"}`)).
AddRow([]byte(nil), []byte(`null`)).
AddRow(nil, nil)
var v1 []*struct {
V struct {
I int `json:"i"`
S string `json:"s"`
} `json:"v"`
P *struct {
I int `json:"i"`
S string `json:"s"`
} `json:"p"`
}
require.NoError(t, ScanSlice(toRows(mock), &v1))
require.Equal(t, 1, v1[0].V.I)
require.Equal(t, "a8m", v1[0].V.S)
require.Equal(t, v1[0].V, *v1[0].P)
require.Equal(t, 2, v1[1].V.I)
require.Equal(t, "tmr", v1[1].V.S)
require.Equal(t, v1[1].V, *v1[1].P)
require.Equal(t, 0, v1[2].V.I)
require.Equal(t, "", v1[2].V.S)
require.Nil(t, v1[2].P)
require.Equal(t, 0, v1[3].V.I)
require.Equal(t, "", v1[3].V.S)
require.Nil(t, v1[3].P)
mock = sqlmock.NewRows([]string{"v", "p"}).
AddRow([]byte(`[1]`), []byte(`[1]`)).
AddRow([]byte(`[2]`), []byte(`[2]`))
var v2 []*struct {
V []int `json:"v"`
P *[]int `json:"p"`
}
require.NoError(t, ScanSlice(toRows(mock), &v2))
require.Equal(t, []int{1}, v2[0].V)
require.Equal(t, v2[0].V, *v2[0].P)
require.Equal(t, []int{2}, v2[1].V)
require.Equal(t, v2[1].V, *v2[1].P)
mock = sqlmock.NewRows([]string{"v", "p"}).
AddRow([]byte(`null`), []byte(`{}`)).
AddRow(nil, nil)
var v3 []*struct {
V json.RawMessage `json:"v"`
P *json.RawMessage `json:"p"`
}
require.NoError(t, ScanSlice(toRows(mock), &v3))
require.Equal(t, json.RawMessage("null"), v3[0].V)
require.Equal(t, json.RawMessage("{}"), *v3[0].P)
require.Equal(t, json.RawMessage(nil), v3[1].V)
require.Nil(t, v3[1].P)
// Unmarshal errors.
mock = sqlmock.NewRows([]string{"v", "p"}).
AddRow([]byte(`{invalid}`), []byte(`{}`))
require.EqualError(t, ScanSlice(toRows(mock), &v1), `unmarshal field "V": invalid character 'i' looking for beginning of object key string`)
mock = sqlmock.NewRows([]string{"v", "p"}).
AddRow([]byte(``), []byte(``))
require.EqualError(t, ScanSlice(toRows(mock), &v1), `unmarshal field "V": unexpected end of JSON input`)
}
func TestScanNestedStruct(t *testing.T) {
mock := sqlmock.NewRows([]string{"name", "age"}).
AddRow("foo", 1).
AddRow("bar", 2).
AddRow("baz", nil)
type T struct{ Name string }
var v []struct {
T
Age int
}
require.NoError(t, ScanSlice(toRows(mock), &v))
require.Equal(t, "foo", v[0].Name)
require.Equal(t, 1, v[0].Age)
require.Equal(t, "bar", v[1].Name)
require.Equal(t, 2, v[1].Age)
require.Equal(t, "baz", v[2].Name)
require.Equal(t, 0, v[2].Age)
mock = sqlmock.NewRows([]string{"name", "age"}).
AddRow("foo", 1).
AddRow("bar", nil)
type T1 struct{ Name **string }
var v1 []struct {
T1
Age *int
}
require.NoError(t, ScanSlice(toRows(mock), &v1))
require.Equal(t, "foo", **v1[0].Name)
require.Equal(t, "bar", **v1[1].Name)
require.Equal(t, 1, *v1[0].Age)
require.Nil(t, v1[1].Age)
}
func TestScanSlicePtr(t *testing.T) {
mock := sqlmock.NewRows([]string{"name"}).
AddRow("foo").
AddRow("bar")
var v0 []*string
require.NoError(t, ScanSlice(toRows(mock), &v0))
require.Equal(t, "foo", *v0[0])
require.Equal(t, "bar", *v0[1])
mock = sqlmock.NewRows([]string{"age"}).
AddRow(1).
AddRow(2)
var v1 []**int
require.NoError(t, ScanSlice(toRows(mock), &v1))
require.Equal(t, 1, **v1[0])
require.Equal(t, 2, **v1[1])
mock = sqlmock.NewRows([]string{"age", "name"}).
AddRow(1, "a8m").
AddRow(2, "nati")
var v2 []*struct {
Age *int
Name **string
}
require.NoError(t, ScanSlice(toRows(mock), &v2))
require.Equal(t, 1, *v2[0].Age)
require.Equal(t, "a8m", **v2[0].Name)
require.Equal(t, 2, *v2[1].Age)
require.Equal(t, "nati", **v2[1].Name)
}
func TestScanInt64(t *testing.T) {
mock := sqlmock.NewRows([]string{"age"}).
AddRow("10").
AddRow("20")
n, err := ScanInt64(toRows(mock))
require.Error(t, err)
require.Zero(t, n)
mock = sqlmock.NewRows([]string{"age", "count"}).
AddRow("10", "1")
n, err = ScanInt64(toRows(mock))
require.Error(t, err)
require.Zero(t, n)
mock = sqlmock.NewRows([]string{"count"}).
AddRow(10)
n, err = ScanInt64(toRows(mock))
require.NoError(t, err)
require.EqualValues(t, 10, n)
mock = sqlmock.NewRows([]string{"count"}).
AddRow("10")
n, err = ScanInt64(toRows(mock))
require.NoError(t, err)
require.EqualValues(t, 10, n)
}
func TestScanValue(t *testing.T) {
mock := sqlmock.NewRows([]string{"count"}).
AddRow(10)
n, err := ScanValue(toRows(mock))
require.NoError(t, err)
require.EqualValues(t, 10, n)
}
func TestScanOne(t *testing.T) {
mock := sqlmock.NewRows([]string{"name"}).
AddRow("10").
AddRow("20")
err := ScanOne(toRows(mock), new(string))
require.Error(t, err, "multiple lines")
mock = sqlmock.NewRows([]string{"name"}).
AddRow("10")
err = ScanOne(toRows(mock), "")
require.Error(t, err, "not a pointer")
mock = sqlmock.NewRows([]string{"name"}).
AddRow("10")
var s string
err = ScanOne(toRows(mock), &s)
require.NoError(t, err)
require.Equal(t, "10", s)
}
func TestInterface(t *testing.T) {
mock := sqlmock.NewRows([]string{"age"}).
AddRow("10").
AddRow("20")
var values []driver.Value
err := ScanSlice(toRows(mock), &values)
require.NoError(t, err)
require.Equal(t, []driver.Value{"10", "20"}, values)
mock = sqlmock.NewRows([]string{"age"}).
AddRow(10).
AddRow(20)
values = values[:0:0]
err = ScanSlice(toRows(mock), &values)
require.NoError(t, err)
require.Equal(t, []driver.Value{int64(10), int64(20)}, values)
}
func TestScanTypeOf(t *testing.T) {
mock := &Rows{ColumnScanner: toRows(sqlmock.NewRows([]string{"age"}).AddRow(10))}
tv := ScanTypeOf(mock, 0)
require.IsType(t, (*any)(nil), tv)
}
func toRows(mrows *sqlmock.Rows) *sql.Rows {
db, mock, _ := sqlmock.New()
mock.ExpectQuery("").WillReturnRows(mrows)
rows, _ := db.Query("")
return rows
}

1226
dialect/sql/schema/atlas.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,160 @@
// 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 schema
import (
"context"
"fmt"
"math"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/schema/field"
)
const (
// TypeTable defines the table name holding the type information.
TypeTable = "ent_types"
// MaxTypes defines the max number of types can be created when
// defining universal ids. The left 16-bits are reserved.
MaxTypes = math.MaxUint16
)
// NewTypesTable returns a new table for holding the global-id information.
func NewTypesTable() *Table {
return NewTable(TypeTable).
AddPrimary(&Column{Name: "id", Type: field.TypeUint, Increment: true}).
AddColumn(&Column{Name: "type", Type: field.TypeString, Unique: true})
}
// MigrateOption allows configuring Atlas using functional arguments.
type MigrateOption func(*Atlas)
// WithGlobalUniqueID sets the universal ids options to the migration.
// Defaults to false.
func WithGlobalUniqueID(b bool) MigrateOption {
return func(a *Atlas) {
a.universalID = b
}
}
// WithIndent sets Atlas to generate SQL statements with indentation.
// An empty string indicates no indentation.
func WithIndent(indent string) MigrateOption {
return func(a *Atlas) {
a.indent = indent
}
}
// WithErrNoPlan sets Atlas to returns a migrate.ErrNoPlan in case
// the migration plan is empty. Defaults to false.
func WithErrNoPlan(b bool) MigrateOption {
return func(a *Atlas) {
a.errNoPlan = b
}
}
// WithSchemaName sets the database schema for the migration.
// If not set, the CURRENT_SCHEMA() is used.
func WithSchemaName(ns string) MigrateOption {
return func(a *Atlas) {
a.schema = ns
}
}
// WithDropColumn sets the columns dropping option to the migration.
// Defaults to false.
func WithDropColumn(b bool) MigrateOption {
return func(a *Atlas) {
a.dropColumns = b
}
}
// WithDropIndex sets the indexes dropping option to the migration.
// Defaults to false.
func WithDropIndex(b bool) MigrateOption {
return func(a *Atlas) {
a.dropIndexes = b
}
}
// WithForeignKeys enables creating foreign-key in ddl. Defaults to true.
func WithForeignKeys(b bool) MigrateOption {
return func(a *Atlas) {
a.withForeignKeys = b
}
}
// WithHooks adds a list of hooks to the schema migration.
func WithHooks(hooks ...Hook) MigrateOption {
return func(a *Atlas) {
a.hooks = append(a.hooks, hooks...)
}
}
type (
// Creator is the interface that wraps the Create method.
Creator interface {
// Create creates the given tables in the database. See Migrate.Create for more details.
Create(context.Context, ...*Table) error
}
// The CreateFunc type is an adapter to allow the use of ordinary function as Creator.
// If f is a function with the appropriate signature, CreateFunc(f) is a Creator that calls f.
CreateFunc func(context.Context, ...*Table) error
// Hook defines the "create middleware". A function that gets a Creator and returns a Creator.
// For example:
//
// hook := func(next schema.Creator) schema.Creator {
// return schema.CreateFunc(func(ctx context.Context, tables ...*schema.Table) error {
// fmt.Println("Tables:", tables)
// return next.Create(ctx, tables...)
// })
// }
//
Hook func(Creator) Creator
)
// Create calls f(ctx, tables...).
func (f CreateFunc) Create(ctx context.Context, tables ...*Table) error {
return f(ctx, tables...)
}
// exist checks if the given COUNT query returns a value >= 1.
func exist(ctx context.Context, conn dialect.ExecQuerier, query string, args ...any) (bool, error) {
rows := &sql.Rows{}
if err := conn.Query(ctx, query, args, rows); err != nil {
return false, fmt.Errorf("reading schema information %w", err)
}
defer rows.Close()
n, err := sql.ScanInt(rows)
if err != nil {
return false, err
}
return n > 0, nil
}
func indexOf(a []string, s string) int {
for i := range a {
if a[i] == s {
return i
}
}
return -1
}
type sqlDialect interface {
atBuilder
dialect.Driver
init(context.Context) error
tableExist(context.Context, dialect.ExecQuerier, string) (bool, error)
}
// verifyRanger wraps the method for verifying global-id range correctness.
type verifyRanger interface {
verifyRange(context.Context, dialect.ExecQuerier, *Table, int64) error
}

View File

@@ -0,0 +1,464 @@
// 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 schema
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
"text/template"
"time"
"ariga.io/atlas/sql/migrate"
"ariga.io/atlas/sql/schema"
"ariga.io/atlas/sql/sqlite"
"ariga.io/atlas/sql/sqltool"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/schema/field"
"github.com/DATA-DOG/go-sqlmock"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/require"
)
func TestMigrate_SchemaName(t *testing.T) {
db, mk, err := sqlmock.New()
require.NoError(t, err)
mk.ExpectQuery(escape("SHOW server_version_num")).
WillReturnRows(sqlmock.NewRows([]string{"server_version_num"}).AddRow("130000"))
mk.ExpectQuery(escape("SELECT current_setting('server_version_num'), current_setting('default_table_access_method', true), current_setting('crdb_version', true)")).
WillReturnRows(sqlmock.NewRows([]string{"current_setting", "current_setting", "current_setting"}).AddRow("130000", "heap", ""))
mk.ExpectQuery("SELECT nspname AS schema_name,.+").
WithArgs("public"). // Schema "public" param is used.
WillReturnRows(sqlmock.NewRows([]string{"schema_name", "comment"}).AddRow("public", "default schema"))
mk.ExpectQuery("SELECT t3.oid, t1.table_schema,.+").
WillReturnRows(sqlmock.NewRows([]string{}))
m, err := NewMigrate(sql.OpenDB("postgres", db), WithSchemaName("public"), WithDiffHook(func(next Differ) Differ {
return DiffFunc(func(current, desired *schema.Schema) ([]schema.Change, error) {
return nil, nil // Noop.
})
}))
require.NoError(t, err)
require.NoError(t, m.Create(context.Background()))
require.NoError(t, mk.ExpectationsWereMet())
// Without schema name the CURRENT_SCHEMA is used.
mk.ExpectQuery(escape("SHOW server_version_num")).
WillReturnRows(sqlmock.NewRows([]string{"server_version_num"}).AddRow("130000"))
mk.ExpectQuery(escape("SELECT current_setting('server_version_num'), current_setting('default_table_access_method', true), current_setting('crdb_version', true)")).
WillReturnRows(sqlmock.NewRows([]string{"current_setting", "current_setting", "current_setting"}).AddRow("130000", "heap", ""))
mk.ExpectQuery("SELECT nspname AS schema_name,.+CURRENT_SCHEMA().+").
WillReturnRows(sqlmock.NewRows([]string{"schema_name", "comment"}).AddRow("public", "default schema"))
mk.ExpectQuery("SELECT t3.oid, t1.table_schema,.+").
WillReturnRows(sqlmock.NewRows([]string{}))
m, err = NewMigrate(sql.OpenDB("postgres", db), WithDiffHook(func(next Differ) Differ {
return DiffFunc(func(current, desired *schema.Schema) ([]schema.Change, error) {
return nil, nil // Noop.
})
}))
require.NoError(t, err)
require.NoError(t, m.Create(context.Background()))
}
func escape(query string) string {
rows := strings.Split(query, "\n")
for i := range rows {
rows[i] = strings.TrimPrefix(rows[i], " ")
}
query = strings.Join(rows, " ")
return strings.TrimSpace(regexp.QuoteMeta(query)) + "$"
}
func TestMigrate_Formatter(t *testing.T) {
db, _, err := sqlmock.New()
require.NoError(t, err)
// If no formatter is given it will be set according to the given migration directory implementation.
for _, tt := range []struct {
dir migrate.Dir
fmt migrate.Formatter
}{
{&migrate.LocalDir{}, sqltool.GolangMigrateFormatter},
{&sqltool.GolangMigrateDir{}, sqltool.GolangMigrateFormatter},
{&sqltool.GooseDir{}, sqltool.GooseFormatter},
{&sqltool.DBMateDir{}, sqltool.DBMateFormatter},
{&sqltool.FlywayDir{}, sqltool.FlywayFormatter},
{&sqltool.LiquibaseDir{}, sqltool.LiquibaseFormatter},
{struct{ migrate.Dir }{}, sqltool.GolangMigrateFormatter}, // default one if migration dir is unknown
} {
m, err := NewMigrate(sql.OpenDB("", db), WithDir(tt.dir))
require.NoError(t, err)
require.Equal(t, tt.fmt, m.fmt)
}
// If a formatter is given, it is not overridden.
m, err := NewMigrate(sql.OpenDB("", db), WithDir(&migrate.LocalDir{}), WithFormatter(migrate.DefaultFormatter))
require.NoError(t, err)
require.Equal(t, migrate.DefaultFormatter, m.fmt)
}
func TestMigrate_DiffJoinTableAllocationBC(t *testing.T) {
// Due to a bug in previous versions, if the universal ID option was enabled and the schema did contain an M2M
// relation, the join table would have had an entry for the join table in the types table. This test ensures,
// that the PK range allocated for the join table stays in place, since it's removal would break existing projects
// due to shifted ranges.
db, err := sql.Open(dialect.SQLite, "file:test?mode=memory&_fk=1")
require.NoError(t, err)
// Mock an existing database with an allocation for a join table.
for _, stmt := range []string{
"CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);",
"CREATE INDEX `short` ON `groups` (`id`);",
"CREATE INDEX `long____________________________1cb2e7e47a309191385af4ad320875b1` ON `groups` (`id`);",
"CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);",
"INSERT INTO sqlite_sequence (name, seq) VALUES (\"users\", 4294967296);",
"CREATE TABLE `user_groups` (`user_id` integer NOT NULL, `group_id` integer NOT NULL, PRIMARY KEY (`user_id`, `group_id`), CONSTRAINT `user_groups_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `user_groups_group_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE);",
"INSERT INTO sqlite_sequence (name, seq) VALUES (\"user_groups\", 8589934592);",
"CREATE TABLE `ent_types` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `type` text NOT NULL);",
"CREATE UNIQUE INDEX `ent_types_type_key` ON `ent_types` (`type`);",
"INSERT INTO `ent_types` (`type`) VALUES ('groups'), ('users'), ('user_groups');",
"INSERT INTO `groups` (`name`) VALUES ('seniors'), ('juniors')",
"INSERT INTO `users` (`name`) VALUES ('masseelch'), ('a8m'), ('rotemtam')",
"INSERT INTO `user_groups` (`user_id`, `group_id`) VALUES (4294967297, 1), (4294967298, 1), (4294967299, 2)",
} {
_, err := db.ExecContext(context.Background(), stmt)
require.NoError(t, err)
}
// Expect to have no changes when migration runs with fix.
m, err := NewMigrate(db, WithGlobalUniqueID(true), WithDiffHook(func(next Differ) Differ {
return DiffFunc(func(current, desired *schema.Schema) ([]schema.Change, error) {
changes, err := next.Diff(current, desired)
if err != nil {
return nil, err
}
require.Len(t, changes, 0)
return changes, nil
})
}))
require.NoError(t, err)
require.NoError(t, m.Create(context.Background(), tables...))
// Expect to have no changes to the allocation when the join table is dropped.
m, err = NewMigrate(db, WithGlobalUniqueID(true))
require.NoError(t, err)
require.NoError(t, m.Create(context.Background(), groupsTable, usersTable))
rows, err := db.QueryContext(context.Background(), "SELECT `type` from `ent_types` ORDER BY `id` ASC")
require.NoError(t, err)
var types []string
for rows.Next() {
var typ string
require.NoError(t, rows.Scan(&typ))
types = append(types, typ)
}
require.NoError(t, rows.Err())
require.Equal(t, []string{"groups", "users", "user_groups"}, types)
}
var (
groupsColumns = []*Column{
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "name", Type: field.TypeString},
}
groupsTable = &Table{
Name: "groups",
Columns: groupsColumns,
PrimaryKey: []*Column{groupsColumns[0]},
Indexes: []*Index{
{
Name: "short",
Columns: []*Column{groupsColumns[0]}},
{
Name: "long_" + strings.Repeat("_", 60),
Columns: []*Column{groupsColumns[0]},
},
},
}
usersColumns = []*Column{
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "name", Type: field.TypeString},
}
usersTable = &Table{
Name: "users",
Columns: usersColumns,
PrimaryKey: []*Column{usersColumns[0]},
}
userGroupsColumns = []*Column{
{Name: "user_id", Type: field.TypeInt},
{Name: "group_id", Type: field.TypeInt},
}
userGroupsTable = &Table{
Name: "user_groups",
Columns: userGroupsColumns,
PrimaryKey: []*Column{userGroupsColumns[0], userGroupsColumns[1]},
ForeignKeys: []*ForeignKey{
{
Symbol: "user_groups_user_id",
Columns: []*Column{userGroupsColumns[0]},
RefColumns: []*Column{usersColumns[0]},
OnDelete: Cascade,
},
{
Symbol: "user_groups_group_id",
Columns: []*Column{userGroupsColumns[1]},
RefColumns: []*Column{groupsColumns[0]},
OnDelete: Cascade,
},
},
}
tables = []*Table{
groupsTable,
usersTable,
userGroupsTable,
}
petColumns = []*Column{
{Name: "id", Type: field.TypeInt, Increment: true},
}
petsTable = &Table{
Name: "pets",
Columns: petColumns,
PrimaryKey: petColumns,
}
)
func init() {
userGroupsTable.ForeignKeys[0].RefTable = usersTable
userGroupsTable.ForeignKeys[1].RefTable = groupsTable
}
func TestMigrate_Diff(t *testing.T) {
ctx := context.Background()
db, err := sql.Open(dialect.SQLite, "file:test?mode=memory&_fk=1")
require.NoError(t, err)
p := t.TempDir()
d, err := migrate.NewLocalDir(p)
require.NoError(t, err)
m, err := NewMigrate(db, WithDir(d))
require.NoError(t, err)
require.NoError(t, m.Diff(ctx, &Table{Name: "users"}))
v := time.Now().UTC().Format("20060102150405")
requireFileEqual(t, filepath.Join(p, v+"_changes.up.sql"), "-- create \"users\" table\nCREATE TABLE `users` ();\n")
requireFileEqual(t, filepath.Join(p, v+"_changes.down.sql"), "-- reverse: create \"users\" table\nDROP TABLE `users`;\n")
require.FileExists(t, filepath.Join(p, migrate.HashFileName))
// Test integrity file.
p = t.TempDir()
d, err = migrate.NewLocalDir(p)
require.NoError(t, err)
m, err = NewMigrate(db, WithDir(d))
require.NoError(t, err)
require.NoError(t, m.Diff(ctx, &Table{Name: "users"}))
requireFileEqual(t, filepath.Join(p, v+"_changes.up.sql"), "-- create \"users\" table\nCREATE TABLE `users` ();\n")
requireFileEqual(t, filepath.Join(p, v+"_changes.down.sql"), "-- reverse: create \"users\" table\nDROP TABLE `users`;\n")
require.FileExists(t, filepath.Join(p, migrate.HashFileName))
require.NoError(t, d.WriteFile("tmp.sql", nil))
require.ErrorIs(t, m.Diff(ctx, &Table{Name: "users"}), migrate.ErrChecksumMismatch)
p = t.TempDir()
d, err = migrate.NewLocalDir(p)
require.NoError(t, err)
f, err := migrate.NewTemplateFormatter(
template.Must(template.New("").Parse("{{ .Name }}.sql")),
template.Must(template.New("").Parse(
`{{ range .Changes }}{{ printf "%s;\n" .Cmd }}{{ end }}`,
)),
)
require.NoError(t, err)
// Join tables (mapping between user and group) will not result in an entry to the types table.
m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithGlobalUniqueID(true))
require.NoError(t, err)
require.NoError(t, m.Diff(ctx, tables...))
changesSQL := strings.Join([]string{
"CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);",
"CREATE INDEX `short` ON `groups` (`id`);",
"CREATE INDEX `long____________________________1cb2e7e47a309191385af4ad320875b1` ON `groups` (`id`);",
"CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);",
fmt.Sprintf("INSERT INTO sqlite_sequence (name, seq) VALUES (\"users\", %d);", 1<<32),
"CREATE TABLE `user_groups` (`user_id` integer NOT NULL, `group_id` integer NOT NULL, PRIMARY KEY (`user_id`, `group_id`), CONSTRAINT `user_groups_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `user_groups_group_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE);",
"CREATE TABLE `ent_types` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `type` text NOT NULL);",
"CREATE UNIQUE INDEX `ent_types_type_key` ON `ent_types` (`type`);",
"INSERT INTO `ent_types` (`type`) VALUES ('groups'), ('users');",
"",
}, "\n")
requireFileEqual(t, filepath.Join(p, "changes.sql"), changesSQL)
// Skipping table creation should write only the ent_type insertion.
m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithGlobalUniqueID(true), WithDiffOptions(schema.DiffSkipChanges(&schema.AddTable{})))
require.NoError(t, err)
require.NoError(t, m.Diff(ctx, tables...))
requireFileEqual(t, filepath.Join(p, "changes.sql"), "INSERT INTO `ent_types` (`type`) VALUES ('groups'), ('users');\n")
// Enable indentations.
m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithGlobalUniqueID(true), WithIndent(" "))
require.NoError(t, err)
// Adding another node will result in a new entry to the TypeTable (without actually creating it).
_, err = db.ExecContext(ctx, changesSQL, nil, nil)
require.NoError(t, err)
require.NoError(t, m.NamedDiff(ctx, "changes_2", petsTable))
requireFileEqual(t,
filepath.Join(p, "changes_2.sql"), strings.Join([]string{
"CREATE TABLE `pets` (\n `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT\n);",
fmt.Sprintf("INSERT INTO sqlite_sequence (name, seq) VALUES (\"pets\", %d);", 2<<32),
"INSERT INTO `ent_types` (`type`) VALUES ('pets');", "",
}, "\n"))
// Checksum will be updated as well.
require.NoError(t, migrate.Validate(d))
require.NoError(t, m.NamedDiff(ctx, "no_changes"), "should not error if WithErrNoPlan is not set")
// Enable WithErrNoPlan.
m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithGlobalUniqueID(true), WithErrNoPlan(true))
require.NoError(t, err)
err = m.NamedDiff(ctx, "no_changes")
require.ErrorIs(t, err, migrate.ErrNoPlan)
}
func requireFileEqual(t *testing.T, name, contents string) {
c, err := os.ReadFile(name)
require.NoError(t, err)
require.Equal(t, contents, string(c))
}
func TestMigrateWithoutForeignKeys(t *testing.T) {
tbl := &schema.Table{
Name: "tbl",
Columns: []*schema.Column{
{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
},
}
fk := &schema.ForeignKey{
Symbol: "fk",
Table: tbl,
Columns: tbl.Columns[1:],
RefTable: tbl,
RefColumns: tbl.Columns[:1],
OnUpdate: schema.NoAction,
OnDelete: schema.Cascade,
}
tbl.ForeignKeys = append(tbl.ForeignKeys, fk)
t.Run("AddTable", func(t *testing.T) {
mdiff := DiffFunc(func(_, _ *schema.Schema) ([]schema.Change, error) {
return []schema.Change{
&schema.AddTable{
T: tbl,
},
}, nil
})
df, err := withoutForeignKeys(mdiff).Diff(nil, nil)
require.NoError(t, err)
require.Len(t, df, 1)
actual, ok := df[0].(*schema.AddTable)
require.True(t, ok)
require.Nil(t, actual.T.ForeignKeys)
})
t.Run("ModifyTable", func(t *testing.T) {
mdiff := DiffFunc(func(_, _ *schema.Schema) ([]schema.Change, error) {
return []schema.Change{
&schema.ModifyTable{
T: tbl,
Changes: []schema.Change{
&schema.AddIndex{
I: &schema.Index{
Name: "id_key",
Parts: []*schema.IndexPart{
{C: tbl.Columns[0]},
},
},
},
&schema.DropForeignKey{
F: fk,
},
&schema.AddForeignKey{
F: fk,
},
&schema.ModifyForeignKey{
From: fk,
To: fk,
Change: schema.ChangeRefColumn,
},
&schema.AddColumn{
C: &schema.Column{Name: "name", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
},
},
},
}, nil
})
df, err := withoutForeignKeys(mdiff).Diff(nil, nil)
require.NoError(t, err)
require.Len(t, df, 1)
actual, ok := df[0].(*schema.ModifyTable)
require.True(t, ok)
require.Len(t, actual.Changes, 2)
addIndex, ok := actual.Changes[0].(*schema.AddIndex)
require.True(t, ok)
require.EqualValues(t, "id_key", addIndex.I.Name)
addColumn, ok := actual.Changes[1].(*schema.AddColumn)
require.True(t, ok)
require.EqualValues(t, "name", addColumn.C.Name)
})
}
func TestAtlas_StateReader(t *testing.T) {
db, err := sql.Open(dialect.SQLite, "file:test?mode=memory&_fk=1")
require.NoError(t, err)
m, err := NewMigrate(db)
require.NoError(t, err)
realm, err := m.StateReader(&Table{
Name: "users",
Columns: []*Column{
{Name: "id", Type: field.TypeInt64, Increment: true},
{Name: "name", Type: field.TypeString},
{Name: "active", Type: field.TypeBool},
},
Annotation: &entsql.Annotation{
IncrementStart: func(i int) *int { return &i }(100),
},
}).ReadState(context.Background())
require.NoError(t, err)
require.NotNil(t, realm)
require.Len(t, realm.Schemas, 1)
require.Len(t, realm.Schemas[0].Tables, 1)
require.Equal(t, "users", realm.Schemas[0].Tables[0].Name)
require.Equal(t, []schema.Attr{&sqlite.AutoIncrement{Seq: 100}}, realm.Schemas[0].Tables[0].Attrs)
require.Equal(t,
realm.Schemas[0].Tables[0].Columns,
[]*schema.Column{
schema.NewIntColumn("id", "integer").
AddAttrs(&sqlite.AutoIncrement{}),
schema.NewStringColumn("name", "text"),
schema.NewBoolColumn("active", "bool"),
},
)
}
func TestAtlas_ParallelCreate(t *testing.T) {
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
db, err := sql.Open(dialect.SQLite, fmt.Sprintf("file:test-%d?mode=memory&_fk=1", i))
require.NoError(t, err)
m, err := NewMigrate(db)
require.NoError(t, err)
go func() {
defer wg.Done()
require.NoError(t, m.Create(context.Background(), petsTable))
require.NoError(t, db.Close())
}()
}
wg.Wait()
}

333
dialect/sql/schema/mysql.go Normal file
View File

@@ -0,0 +1,333 @@
// 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 schema
import (
"context"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/schema/field"
"ariga.io/atlas/sql/migrate"
"ariga.io/atlas/sql/mysql"
"ariga.io/atlas/sql/schema"
)
// MySQL adapter for Atlas migration engine.
type MySQL struct {
dialect.Driver
schema string
version string
}
// init loads the MySQL version from the database for later use in the migration process.
func (d *MySQL) init(ctx context.Context) error {
if d.version != "" {
return nil // already initialized.
}
rows := &sql.Rows{}
if err := d.Query(ctx, "SHOW VARIABLES LIKE 'version'", []any{}, rows); err != nil {
return fmt.Errorf("mysql: querying mysql version %w", err)
}
defer rows.Close()
if !rows.Next() {
if err := rows.Err(); err != nil {
return err
}
return fmt.Errorf("mysql: version variable was not found")
}
version := make([]string, 2)
if err := rows.Scan(&version[0], &version[1]); err != nil {
return fmt.Errorf("mysql: scanning mysql version: %w", err)
}
d.version = version[1]
return nil
}
func (d *MySQL) tableExist(ctx context.Context, conn dialect.ExecQuerier, name string) (bool, error) {
query, args := sql.Select(sql.Count("*")).From(sql.Table("TABLES").Schema("INFORMATION_SCHEMA")).
Where(sql.And(
d.matchSchema(),
sql.EQ("TABLE_NAME", name),
)).Query()
return exist(ctx, conn, query, args...)
}
// matchSchema returns the predicate for matching table schema.
func (d *MySQL) matchSchema(columns ...string) *sql.Predicate {
column := "TABLE_SCHEMA"
if len(columns) > 0 {
column = columns[0]
}
if d.schema != "" {
return sql.EQ(column, d.schema)
}
return sql.EQ(column, sql.Raw("(SELECT DATABASE())"))
}
func (d *MySQL) atOpen(conn dialect.ExecQuerier) (migrate.Driver, error) {
return mysql.Open(&db{ExecQuerier: conn})
}
func (d *MySQL) atTable(t1 *Table, t2 *schema.Table) {
t2.SetCharset("utf8mb4").SetCollation("utf8mb4_bin")
if t1.Annotation == nil {
return
}
if charset := t1.Annotation.Charset; charset != "" {
t2.SetCharset(charset)
}
if collate := t1.Annotation.Collation; collate != "" {
t2.SetCollation(collate)
}
if opts := t1.Annotation.Options; opts != "" {
t2.AddAttrs(&mysql.CreateOptions{
V: opts,
})
}
// Check if the connected database supports the CHECK clause.
// For MySQL, is >= "8.0.16" and for MariaDB it is "10.2.1".
v1, v2 := d.version, "8.0.16"
if v, ok := d.mariadb(); ok {
v1, v2 = v, "10.2.1"
}
if compareVersions(v1, v2) >= 0 {
setAtChecks(t1, t2)
}
}
func (d *MySQL) supportsDefault(c *Column) bool {
_, maria := d.mariadb()
switch c.Default.(type) {
case Expr, map[string]Expr:
if maria {
return compareVersions(d.version, "10.2.0") >= 0
}
return c.supportDefault() && compareVersions(d.version, "8.0.0") >= 0
default:
return c.supportDefault() || maria
}
}
func (d *MySQL) supportsUUID() bool {
_, maria := d.mariadb()
return maria && compareVersions(d.version, "10.7.0") >= 0
}
func (d *MySQL) atTypeC(c1 *Column, c2 *schema.Column) error {
if c1.SchemaType != nil && c1.SchemaType[dialect.MySQL] != "" {
t, err := mysql.ParseType(strings.ToLower(c1.SchemaType[dialect.MySQL]))
if err != nil {
return err
}
c2.Type.Type = t
return nil
}
var t schema.Type
switch c1.Type {
case field.TypeBool:
t = &schema.BoolType{T: "boolean"}
case field.TypeBlob:
return fmt.Errorf("blob fields are not stored in the database")
case field.TypeInt8:
t = &schema.IntegerType{T: mysql.TypeTinyInt}
case field.TypeUint8:
t = &schema.IntegerType{T: mysql.TypeTinyInt, Unsigned: true}
case field.TypeInt16:
t = &schema.IntegerType{T: mysql.TypeSmallInt}
case field.TypeUint16:
t = &schema.IntegerType{T: mysql.TypeSmallInt, Unsigned: true}
case field.TypeInt32:
t = &schema.IntegerType{T: mysql.TypeInt}
case field.TypeUint32:
t = &schema.IntegerType{T: mysql.TypeInt, Unsigned: true}
case field.TypeInt, field.TypeInt64:
t = &schema.IntegerType{T: mysql.TypeBigInt}
case field.TypeUint, field.TypeUint64:
t = &schema.IntegerType{T: mysql.TypeBigInt, Unsigned: true}
case field.TypeBytes:
size := int64(math.MaxUint16)
if c1.Size > 0 {
size = c1.Size
}
switch {
case size <= math.MaxUint8:
t = &schema.BinaryType{T: mysql.TypeTinyBlob}
case size <= math.MaxUint16:
t = &schema.BinaryType{T: mysql.TypeBlob}
case size < 1<<24:
t = &schema.BinaryType{T: mysql.TypeMediumBlob}
case size <= math.MaxUint32:
t = &schema.BinaryType{T: mysql.TypeLongBlob}
}
case field.TypeJSON:
t = &schema.JSONType{T: mysql.TypeJSON}
if compareVersions(d.version, "5.7.8") == -1 {
t = &schema.BinaryType{T: mysql.TypeLongBlob}
}
case field.TypeString:
size := c1.Size
if size == 0 {
size = d.defaultSize(c1)
}
switch {
case c1.typ == "tinytext", c1.typ == "text":
t = &schema.StringType{T: c1.typ}
case size <= math.MaxUint16:
t = &schema.StringType{T: mysql.TypeVarchar, Size: int(size)}
case size == 1<<24-1:
t = &schema.StringType{T: mysql.TypeMediumText}
default:
t = &schema.StringType{T: mysql.TypeLongText}
}
case field.TypeFloat32, field.TypeFloat64:
t = &schema.FloatType{T: c1.scanTypeOr(mysql.TypeDouble)}
case field.TypeTime:
t = &schema.TimeType{T: c1.scanTypeOr(mysql.TypeTimestamp)}
// In MariaDB or in MySQL < v8.0.2, the TIMESTAMP column has both `DEFAULT CURRENT_TIMESTAMP`
// and `ON UPDATE CURRENT_TIMESTAMP` if neither is specified explicitly. this behavior is
// suppressed if the column is defined with a `DEFAULT` clause or with the `NULL` attribute.
if _, maria := d.mariadb(); maria || compareVersions(d.version, "8.0.2") == -1 && c1.Default == nil {
c2.SetNull(c1.Attr == "")
}
case field.TypeEnum:
t = &schema.EnumType{T: mysql.TypeEnum, Values: c1.Enums}
case field.TypeUUID:
if d.supportsUUID() {
// Native support for the uuid type
t = &schema.UUIDType{T: mysql.TypeUUID}
} else {
// "CHAR(X) BINARY" is treated as "CHAR(X) COLLATE latin1_bin", and in MySQL < 8,
// and "COLLATE utf8mb4_bin" in MySQL >= 8. However we already set the table to
t = &schema.StringType{T: mysql.TypeChar, Size: 36}
c2.SetCollation("utf8mb4_bin")
}
default:
t, err := mysql.ParseType(strings.ToLower(c1.typ))
if err != nil {
return err
}
c2.Type.Type = t
}
c2.Type.Type = t
return nil
}
func (d *MySQL) atUniqueC(t1 *Table, c1 *Column, t2 *schema.Table, c2 *schema.Column) {
// For UNIQUE columns, MySQL create an implicit index
// named as the column with an extra index in case the
// name is already taken (<e.g. c>, <c_2>, <c_3>, ...).
for _, idx := range t1.Indexes {
// Index also defined explicitly, and will be add in atIndexes.
if idx.Unique && d.atImplicitIndexName(idx, c1) {
return
}
}
t2.AddIndexes(schema.NewUniqueIndex(c1.Name).AddColumns(c2))
}
func (d *MySQL) atIncrementC(t *schema.Table, c *schema.Column) {
if c.Default != nil {
t.Attrs = removeAttr(t.Attrs, reflect.TypeOf(&mysql.AutoIncrement{}))
} else {
c.AddAttrs(&mysql.AutoIncrement{})
}
}
func (d *MySQL) atIncrementT(t *schema.Table, v int64) {
if v >= 0 {
t.AddAttrs(mysql.NewAutoIncrement(uint64(v)))
}
}
func (d *MySQL) atImplicitIndexName(idx *Index, c1 *Column) bool {
if idx.Name == c1.Name {
return true
}
if !strings.HasPrefix(idx.Name, c1.Name+"_") {
return false
}
i, err := strconv.ParseInt(strings.TrimLeft(idx.Name, c1.Name+"_"), 10, 64)
return err == nil && i > 1
}
func (d *MySQL) atIndex(idx1 *Index, t2 *schema.Table, idx2 *schema.Index) error {
prefix := indexParts(idx1)
for _, c1 := range idx1.Columns {
c2, ok := t2.Column(c1.Name)
if !ok {
return fmt.Errorf("unexpected index %q column: %q", idx1.Name, c1.Name)
}
part := &schema.IndexPart{C: c2}
if v, ok := prefix[c1.Name]; ok {
part.AddAttrs(&mysql.SubPart{Len: int(v)})
}
idx2.AddParts(part)
}
if t, ok := indexType(idx1, dialect.MySQL); ok {
idx2.AddAttrs(&mysql.IndexType{T: t})
}
return nil
}
func (*MySQL) atTypeRangeSQL(ts ...string) string {
for i := range ts {
ts[i] = fmt.Sprintf("('%s')", ts[i])
}
return fmt.Sprintf("INSERT INTO `%s` (`type`) VALUES %s", TypeTable, strings.Join(ts, ", "))
}
// mariadb reports if the migration runs on MariaDB and returns the semver string.
func (d *MySQL) mariadb() (string, bool) {
idx := strings.Index(d.version, "MariaDB")
if idx == -1 {
return "", false
}
return d.version[:idx-1], true
}
// defaultSize returns the default size for MySQL/MariaDB varchar type
// based on column size, charset and table indexes, in order to avoid
// index prefix key limit (767) for older versions of MySQL/MariaDB.
func (d *MySQL) defaultSize(c *Column) int64 {
size := DefaultStringLen
version, checked := d.version, "5.7.0"
if v, ok := d.mariadb(); ok {
version, checked = v, "10.2.2"
}
switch {
// Version is >= 5.7 for MySQL, or >= 10.2.2 for MariaDB.
case compareVersions(version, checked) != -1:
// Column is non-unique, or not part of any index (reaching
// the error 1071).
case !c.Unique && len(c.indexes) == 0 && !c.PrimaryKey():
default:
size = 191
}
return size
}
// indexParts returns a map holding the sub_part mapping if exists.
func indexParts(idx *Index) map[string]uint {
parts := make(map[string]uint)
if idx.Annotation == nil {
return parts
}
// If prefix (without a name) was defined on the
// annotation, map it to the single column index.
if idx.Annotation.Prefix > 0 && len(idx.Columns) == 1 {
parts[idx.Columns[0].Name] = idx.Annotation.Prefix
}
for column, part := range idx.Annotation.PrefixColumns {
parts[column] = part
}
return parts
}

View File

@@ -0,0 +1,260 @@
// 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 schema
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/schema/field"
"ariga.io/atlas/sql/migrate"
"ariga.io/atlas/sql/postgres"
"ariga.io/atlas/sql/schema"
)
// Postgres adapter for Atlas migration engine.
type Postgres struct {
dialect.Driver
schema string
version string
}
// init loads the Postgres version from the database for later use in the migration process.
// It returns an error if the server version is lower than v10.
func (d *Postgres) init(ctx context.Context) error {
if d.version != "" {
return nil // already initialized.
}
rows := &sql.Rows{}
if err := d.Query(ctx, "SHOW server_version_num", []any{}, rows); err != nil {
return fmt.Errorf("querying server version %w", err)
}
defer rows.Close()
if !rows.Next() {
if err := rows.Err(); err != nil {
return err
}
return fmt.Errorf("server_version_num variable was not found")
}
var version string
if err := rows.Scan(&version); err != nil {
return fmt.Errorf("scanning version: %w", err)
}
if len(version) < 6 {
return fmt.Errorf("malformed version: %s", version)
}
d.version = fmt.Sprintf("%s.%s.%s", version[:2], version[2:4], version[4:])
if compareVersions(d.version, "10.0.0") == -1 {
return fmt.Errorf("unsupported postgres version: %s", d.version)
}
return nil
}
// tableExist checks if a table exists in the database and current schema.
func (d *Postgres) tableExist(ctx context.Context, conn dialect.ExecQuerier, name string) (bool, error) {
query, args := sql.Dialect(dialect.Postgres).
Select(sql.Count("*")).From(sql.Table("tables").Schema("information_schema")).
Where(sql.And(
d.matchSchema(),
sql.EQ("table_name", name),
)).Query()
return exist(ctx, conn, query, args...)
}
// matchSchema returns the predicate for matching table schema.
func (d *Postgres) matchSchema(columns ...string) *sql.Predicate {
column := "table_schema"
if len(columns) > 0 {
column = columns[0]
}
if d.schema != "" {
return sql.EQ(column, d.schema)
}
return sql.EQ(column, sql.Raw("CURRENT_SCHEMA()"))
}
// maxCharSize defines the maximum size of limited character types in Postgres (10 MB).
const maxCharSize = 10 << 20
func (d *Postgres) atOpen(conn dialect.ExecQuerier) (migrate.Driver, error) {
return postgres.Open(&db{ExecQuerier: conn})
}
func (d *Postgres) atTable(t1 *Table, t2 *schema.Table) {
if t1.Annotation != nil {
setAtChecks(t1, t2)
}
}
func (d *Postgres) supportsDefault(*Column) bool {
// PostgreSQL supports default values for all standard types.
return true
}
func (d *Postgres) atTypeC(c1 *Column, c2 *schema.Column) error {
if c1.SchemaType != nil && c1.SchemaType[dialect.Postgres] != "" {
t, err := postgres.ParseType(strings.ToLower(c1.SchemaType[dialect.Postgres]))
if err != nil {
return err
}
c2.Type.Type = t
if s, ok := t.(*postgres.SerialType); c1.foreign != nil && ok {
c2.Type.Type = s.IntegerType()
}
return nil
}
var t schema.Type
switch c1.Type {
case field.TypeBlob:
return fmt.Errorf("blob fields are not stored in the database")
case field.TypeBool:
t = &schema.BoolType{T: postgres.TypeBoolean}
case field.TypeUint8, field.TypeInt8, field.TypeInt16:
t = &schema.IntegerType{T: postgres.TypeSmallInt}
case field.TypeUint16, field.TypeInt32:
t = &schema.IntegerType{T: postgres.TypeInt}
case field.TypeUint32, field.TypeInt, field.TypeUint, field.TypeInt64, field.TypeUint64:
t = &schema.IntegerType{T: postgres.TypeBigInt}
case field.TypeFloat32:
t = &schema.FloatType{T: c1.scanTypeOr(postgres.TypeReal)}
case field.TypeFloat64:
t = &schema.FloatType{T: c1.scanTypeOr(postgres.TypeDouble)}
case field.TypeBytes:
t = &schema.BinaryType{T: postgres.TypeBytea}
case field.TypeUUID:
t = &postgres.UUIDType{T: postgres.TypeUUID}
case field.TypeJSON:
t = &schema.JSONType{T: postgres.TypeJSONB}
case field.TypeString:
t = &schema.StringType{T: postgres.TypeVarChar}
if c1.Size > maxCharSize {
t = &schema.StringType{T: postgres.TypeText}
}
case field.TypeTime:
t = &schema.TimeType{T: c1.scanTypeOr(postgres.TypeTimestampWTZ)}
case field.TypeEnum:
// Although atlas supports enum types, we keep backwards compatibility
// with previous versions of ent and use varchar (see cType).
t = &schema.StringType{T: postgres.TypeVarChar}
case field.TypeOther:
t = &schema.UnsupportedType{T: c1.typ}
default:
t, err := postgres.ParseType(strings.ToLower(c1.typ))
if err != nil {
return err
}
c2.Type.Type = t
}
c2.Type.Type = t
return nil
}
func (d *Postgres) atUniqueC(t1 *Table, c1 *Column, t2 *schema.Table, c2 *schema.Column) {
// For UNIQUE columns, PostgreSQL creates an implicit index named
// "<table>_<column>_key<i>".
for _, idx := range t1.Indexes {
// Index also defined explicitly, and will be added in atIndexes.
if idx.Unique && d.atImplicitIndexName(idx, t1, c1) {
return
}
}
t2.AddIndexes(schema.NewUniqueIndex(fmt.Sprintf("%s_%s_key", t1.Name, c1.Name)).AddColumns(c2))
}
func (d *Postgres) atImplicitIndexName(idx *Index, t1 *Table, c1 *Column) bool {
p := fmt.Sprintf("%s_%s_key", t1.Name, c1.Name)
if idx.Name == p {
return true
}
i, err := strconv.ParseInt(strings.TrimPrefix(idx.Name, p), 10, 64)
return err == nil && i > 0
}
func (d *Postgres) atIncrementC(t *schema.Table, c *schema.Column) {
// Skip marking this column as an identity in case it is
// serial type or a default was already defined for it.
if _, ok := c.Type.Type.(*postgres.SerialType); ok || c.Default != nil {
t.Attrs = removeAttr(t.Attrs, reflect.TypeOf(&postgres.Identity{}))
return
}
id := &postgres.Identity{}
for _, a := range t.Attrs {
if a, ok := a.(*postgres.Identity); ok {
id = a
}
}
c.AddAttrs(id)
}
func (d *Postgres) atIncrementT(t *schema.Table, v int64) {
t.AddAttrs(&postgres.Identity{Sequence: &postgres.Sequence{Start: v}})
}
// indexOpClass returns a map holding the operator-class mapping if exists.
func indexOpClass(idx *Index) map[string]string {
opc := make(map[string]string)
if idx.Annotation == nil {
return opc
}
// If operator-class (without a name) was defined on
// the annotation, map it to the single column index.
if idx.Annotation.OpClass != "" && len(idx.Columns) == 1 {
opc[idx.Columns[0].Name] = idx.Annotation.OpClass
}
for column, op := range idx.Annotation.OpClassColumns {
opc[column] = op
}
return opc
}
func (d *Postgres) atIndex(idx1 *Index, t2 *schema.Table, idx2 *schema.Index) error {
opc := indexOpClass(idx1)
for _, c1 := range idx1.Columns {
c2, ok := t2.Column(c1.Name)
if !ok {
return fmt.Errorf("unexpected index %q column: %q", idx1.Name, c1.Name)
}
part := &schema.IndexPart{C: c2}
if v, ok := opc[c1.Name]; ok {
var op postgres.IndexOpClass
if err := op.UnmarshalText([]byte(v)); err != nil {
return fmt.Errorf("unmarshalling operator-class %q for column %q: %v", v, c1.Name, err)
}
part.Attrs = append(part.Attrs, &op)
}
idx2.AddParts(part)
}
if t, ok := indexType(idx1, dialect.Postgres); ok {
idx2.AddAttrs(&postgres.IndexType{T: t})
}
if ant, supportsInclude := idx1.Annotation, compareVersions(d.version, "11.0.0") >= 0; ant != nil && len(ant.IncludeColumns) > 0 && supportsInclude {
columns := make([]*schema.Column, len(ant.IncludeColumns))
for i, ic := range ant.IncludeColumns {
c, ok := t2.Column(ic)
if !ok {
return fmt.Errorf("include column %q was not found for index %q", ic, idx1.Name)
}
columns[i] = c
}
idx2.AddAttrs(&postgres.IndexInclude{Columns: columns})
}
if idx1.Annotation != nil && idx1.Annotation.Where != "" {
idx2.AddAttrs(&postgres.IndexPredicate{P: idx1.Annotation.Where})
}
return nil
}
func (*Postgres) atTypeRangeSQL(ts ...string) string {
for i := range ts {
ts[i] = fmt.Sprintf("('%s')", ts[i])
}
return fmt.Sprintf(`INSERT INTO "%s" ("type") VALUES %s`, TypeTable, strings.Join(ts, ", "))
}

View File

@@ -0,0 +1,695 @@
// 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 schema contains all schema migration logic for SQL dialects.
package schema
import (
"context"
"fmt"
"slices"
"strconv"
"strings"
"sync"
"ariga.io/atlas/sql/migrate"
"ariga.io/atlas/sql/mysql"
"ariga.io/atlas/sql/postgres"
"ariga.io/atlas/sql/schema"
"ariga.io/atlas/sql/sqlite"
entdialect "entgo.io/ent/dialect"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/schema/field"
)
const (
// DefaultStringLen describes the default length for string/varchar types.
DefaultStringLen int64 = 255
// Null is the string representation of NULL in SQL.
Null = "NULL"
// PrimaryKey is the string representation of PKs in SQL.
PrimaryKey = "PRI"
// UniqueKey is the string representation of PKs in SQL.
UniqueKey = "UNI"
)
// Table schema definition for SQL dialects.
type Table struct {
mu sync.Mutex
Name string
Schema string
Columns []*Column
columns map[string]*Column
Indexes []*Index
PrimaryKey []*Column
ForeignKeys []*ForeignKey
Annotation *entsql.Annotation
Comment string
View bool // Indicate the table is a view.
Pos string // filename:line of the ent schema definition.
}
// NewTable returns a new table with the given name.
func NewTable(name string) *Table {
return &Table{
Name: name,
columns: make(map[string]*Column),
}
}
// NewView returns a new view with the given name.
func NewView(name string) *Table {
t := NewTable(name)
t.View = true
return t
}
// SetComment sets the table comment.
func (t *Table) SetComment(c string) *Table {
t.Comment = c
return t
}
// SetSchema sets the table schema.
func (t *Table) SetSchema(s string) *Table {
t.Schema = s
return t
}
// SetPos sets the table position.
func (t *Table) SetPos(p string) *Table {
t.Pos = p
return t
}
// AddPrimary adds a new primary key to the table.
func (t *Table) AddPrimary(c *Column) *Table {
c.Key = PrimaryKey
t.AddColumn(c)
t.PrimaryKey = append(t.PrimaryKey, c)
return t
}
// AddForeignKey adds a foreign key to the table.
func (t *Table) AddForeignKey(fk *ForeignKey) *Table {
t.ForeignKeys = append(t.ForeignKeys, fk)
return t
}
// AddColumn adds a new column to the table.
func (t *Table) AddColumn(c *Column) *Table {
t.columns[c.Name] = c
t.Columns = append(t.Columns, c)
return t
}
// HasColumn reports if the table contains a column with the given name.
func (t *Table) HasColumn(name string) bool {
_, ok := t.Column(name)
return ok
}
// Column returns the column with the given name. If exists.
func (t *Table) Column(name string) (*Column, bool) {
if c, ok := t.columns[name]; ok {
return c, true
}
// In case the column was added
// directly to the Columns field.
for _, c := range t.Columns {
if c.Name == name {
return c, true
}
}
return nil, false
}
// SetAnnotation the entsql.Annotation on the table.
func (t *Table) SetAnnotation(ant *entsql.Annotation) *Table {
t.Annotation = ant
return t
}
// AddIndex creates and adds a new index to the table from the given options.
func (t *Table) AddIndex(name string, unique bool, columns []string) *Table {
return t.addIndex(&Index{
Name: name,
Unique: unique,
columns: columns,
Columns: make([]*Column, 0, len(columns)),
})
}
// AddIndex creates and adds a new index to the table from the given options.
func (t *Table) addIndex(idx *Index) *Table {
for _, name := range idx.columns {
c, ok := t.columns[name]
if ok {
c.indexes.append(idx)
idx.Columns = append(idx.Columns, c)
}
}
t.Indexes = append(t.Indexes, idx)
return t
}
// column returns a table column by its name.
// faster than map lookup for most cases.
func (t *Table) column(name string) (*Column, bool) {
for _, c := range t.Columns {
if c.Name == name {
return c, true
}
}
return nil, false
}
// Index returns a table index by its exact name.
func (t *Table) Index(name string) (*Index, bool) {
idx, ok := t.index(name)
if ok && idx.Name == name {
return idx, ok
}
return nil, false
}
// index returns a table index by its name.
func (t *Table) index(name string) (*Index, bool) {
for _, idx := range t.Indexes {
if name == idx.Name || name == idx.realname {
return idx, true
}
// Same as below, there are cases where the index name
// is unknown (created automatically on column constraint).
if len(idx.Columns) == 1 && idx.Columns[0].Name == name {
return idx, true
}
}
// If it is an "implicit index" (unique constraint on
// table creation) and it wasn't loaded in table scanning.
c, ok := t.column(name)
if !ok {
// Postgres naming convention for unique constraint (<table>_<column>_key).
name = strings.TrimPrefix(name, t.Name+"_")
name = strings.TrimSuffix(name, "_key")
c, ok = t.column(name)
}
if ok && c.Unique {
return &Index{Name: name, Unique: c.Unique, Columns: []*Column{c}, columns: []string{c.Name}}, true
}
return nil, false
}
// CopyTables returns a deep-copy of the given tables. This utility function is
// useful for copying the generated schema tables (i.e. migrate.Tables) before
// running schema migration when there is a need for execute multiple migrations
// concurrently. e.g. running parallel unit-tests using the generated enttest package.
func CopyTables(tables []*Table) ([]*Table, error) {
var (
copyT = make([]*Table, len(tables))
byName = make(map[string]*Table)
)
for i, t := range tables {
copyT[i] = &Table{
Name: t.Name,
Columns: make([]*Column, len(t.Columns)),
Indexes: make([]*Index, len(t.Indexes)),
ForeignKeys: make([]*ForeignKey, len(t.ForeignKeys)),
}
for j, c := range t.Columns {
cc := *c
// SchemaType and Enums are read-only fields.
cc.indexes = nil
cc.foreign = nil
copyT[i].Columns[j] = &cc
}
if at := t.Annotation; at != nil {
cat := *at
copyT[i].Annotation = &cat
}
byName[t.Name] = copyT[i]
}
for i, t := range tables {
ct := copyT[i]
for _, c := range t.PrimaryKey {
cc, ok := ct.column(c.Name)
if !ok {
return nil, fmt.Errorf("sql/schema: missing primary key column %q", c.Name)
}
ct.PrimaryKey = append(ct.PrimaryKey, cc)
}
for j, idx := range t.Indexes {
cidx := &Index{
Name: idx.Name,
Unique: idx.Unique,
Columns: make([]*Column, len(idx.Columns)),
}
if at := idx.Annotation; at != nil {
cat := *at
cidx.Annotation = &cat
}
for k, c := range idx.Columns {
cc, ok := ct.column(c.Name)
if !ok {
return nil, fmt.Errorf("sql/schema: missing index column %q", c.Name)
}
cidx.Columns[k] = cc
}
ct.Indexes[j] = cidx
}
for j, fk := range t.ForeignKeys {
cfk := &ForeignKey{
Symbol: fk.Symbol,
OnUpdate: fk.OnUpdate,
OnDelete: fk.OnDelete,
Columns: make([]*Column, len(fk.Columns)),
RefColumns: make([]*Column, len(fk.RefColumns)),
}
for k, c := range fk.Columns {
cc, ok := ct.column(c.Name)
if !ok {
return nil, fmt.Errorf("sql/schema: missing foreign-key column %q", c.Name)
}
cfk.Columns[k] = cc
}
cref, ok := byName[fk.RefTable.Name]
if !ok {
return nil, fmt.Errorf("sql/schema: missing foreign-key ref-table %q", fk.RefTable.Name)
}
cfk.RefTable = cref
for k, c := range fk.RefColumns {
cc, ok := cref.column(c.Name)
if !ok {
return nil, fmt.Errorf("sql/schema: missing foreign-key ref-column %q", c.Name)
}
cfk.RefColumns[k] = cc
}
ct.ForeignKeys[j] = cfk
}
}
return copyT, nil
}
// Column schema definition for SQL dialects.
type Column struct {
Name string // column name.
Type field.Type // column type.
SchemaType map[string]string // optional schema type per dialect.
Attr string // extra attributes.
Size int64 // max size parameter for string, blob, etc.
Key string // key definition (PRI, UNI or MUL).
Unique bool // column with unique constraint.
Increment bool // auto increment attribute.
Nullable bool // null or not null attribute.
Default any // default value.
Enums []string // enum values.
Collation string // collation type (utf8mb4_unicode_ci, utf8mb4_general_ci)
typ string // row column type (used for Rows.Scan).
indexes Indexes // linked indexes.
foreign *ForeignKey // linked foreign-key.
Comment string // optional column comment.
}
// Expr represents a raw expression. It is used to distinguish between
// literal values and raw expressions when defining default values.
type Expr string
// UniqueKey returns boolean indicates if this column is a unique key.
// Used by the migration tool when parsing the `DESCRIBE TABLE` output Go objects.
func (c *Column) UniqueKey() bool { return c.Key == UniqueKey }
// PrimaryKey returns boolean indicates if this column is on of the primary key columns.
// Used by the migration tool when parsing the `DESCRIBE TABLE` output Go objects.
func (c *Column) PrimaryKey() bool { return c.Key == PrimaryKey }
// ConvertibleTo reports whether a column can be converted to the new column without altering its data.
func (c *Column) ConvertibleTo(d *Column) bool {
switch {
case c.Type == d.Type:
if c.Size != 0 && d.Size != 0 {
// Types match and have a size constraint.
return c.Size <= d.Size
}
return true
case c.IntType() && d.IntType() || c.UintType() && d.UintType():
return c.Type <= d.Type
case c.UintType() && d.IntType():
// uintX can not be converted to intY, when X > Y.
return c.Type-field.TypeUint8 <= d.Type-field.TypeInt8
case c.Type == field.TypeString && d.Type == field.TypeEnum ||
c.Type == field.TypeEnum && d.Type == field.TypeString:
return true
case c.Type.Integer() && d.Type == field.TypeString:
return true
}
return c.FloatType() && d.FloatType()
}
// IntType reports whether the column is an int type (int8 ... int64).
func (c Column) IntType() bool { return c.Type >= field.TypeInt8 && c.Type <= field.TypeInt64 }
// UintType reports of the given type is a uint type (int8 ... int64).
func (c Column) UintType() bool { return c.Type >= field.TypeUint8 && c.Type <= field.TypeUint64 }
// FloatType reports of the given type is a float type (float32, float64).
func (c Column) FloatType() bool { return c.Type == field.TypeFloat32 || c.Type == field.TypeFloat64 }
// ScanDefault scans the default value string to its interface type.
func (c *Column) ScanDefault(value string) error {
switch {
case strings.ToUpper(value) == Null: // ignore.
case c.IntType():
v := &sql.NullInt64{}
if err := v.Scan(value); err != nil {
return fmt.Errorf("scanning int value for column %q: %w", c.Name, err)
}
c.Default = v.Int64
case c.UintType():
v := &sql.NullInt64{}
if err := v.Scan(value); err != nil {
return fmt.Errorf("scanning uint value for column %q: %w", c.Name, err)
}
c.Default = uint64(v.Int64)
case c.FloatType():
v := &sql.NullFloat64{}
if err := v.Scan(value); err != nil {
return fmt.Errorf("scanning float value for column %q: %w", c.Name, err)
}
c.Default = v.Float64
case c.Type == field.TypeBool:
v := &sql.NullBool{}
if err := v.Scan(value); err != nil {
return fmt.Errorf("scanning bool value for column %q: %w", c.Name, err)
}
c.Default = v.Bool
case c.Type == field.TypeString || c.Type == field.TypeEnum:
v := &sql.NullString{}
if err := v.Scan(value); err != nil {
return fmt.Errorf("scanning string value for column %q: %w", c.Name, err)
}
c.Default = v.String
case c.Type == field.TypeJSON:
v := &sql.NullString{}
if err := v.Scan(value); err != nil {
return fmt.Errorf("scanning json value for column %q: %w", c.Name, err)
}
c.Default = v.String
case c.Type == field.TypeBytes:
c.Default = []byte(value)
case c.Type == field.TypeUUID:
// skip function
if !strings.Contains(value, "()") {
c.Default = value
}
default:
return fmt.Errorf("unsupported default type: %v default to %q", c.Type, value)
}
return nil
}
// supportDefault reports if the column type supports default value.
func (c Column) supportDefault() bool {
switch t := c.Type; t {
case field.TypeString, field.TypeEnum:
return c.Size < 1<<16 // not a text.
case field.TypeBool, field.TypeTime, field.TypeUUID:
return true
default:
return t.Numeric()
}
}
// scanTypeOr returns the scanning type or the given value.
func (c *Column) scanTypeOr(t string) string {
if c.typ != "" {
return strings.ToLower(c.typ)
}
return t
}
// ForeignKey definition for creation.
type ForeignKey struct {
Symbol string // foreign-key name. Generated if empty.
Columns []*Column // table column
RefTable *Table // referenced table.
RefColumns []*Column // referenced columns.
OnUpdate ReferenceOption // action on update.
OnDelete ReferenceOption // action on delete.
}
// ReferenceOption for constraint actions.
type ReferenceOption string
// Reference options.
const (
NoAction ReferenceOption = "NO ACTION"
Restrict ReferenceOption = "RESTRICT"
Cascade ReferenceOption = "CASCADE"
SetNull ReferenceOption = "SET NULL"
SetDefault ReferenceOption = "SET DEFAULT"
)
// ConstName returns the constant name of a reference option. It's used by entc for printing the constant name in templates.
func (r ReferenceOption) ConstName() string {
return strings.ReplaceAll(strings.Title(strings.ToLower(string(r))), " ", "")
}
// Index definition for table index.
type Index struct {
Name string // index name.
Unique bool // uniqueness.
Columns []*Column // actual table columns.
Annotation *entsql.IndexAnnotation // index annotation.
columns []string // columns loaded from query scan.
realname string // real name in the database (Postgres only).
}
// Indexes used for scanning all sql.Rows into a list of indexes, because
// multiple sql rows can represent the same index (multi-columns indexes).
type Indexes []*Index
// append wraps the basic `append` function by filtering duplicates indexes.
func (i *Indexes) append(idx1 *Index) {
for _, idx2 := range *i {
if idx2.Name == idx1.Name {
return
}
}
*i = append(*i, idx1)
}
// compareVersions returns an integer comparing the 2 versions.
func compareVersions(v1, v2 string) int {
pv1, ok1 := parseVersion(v1)
pv2, ok2 := parseVersion(v2)
if !ok1 && !ok2 {
return 0
}
if !ok1 {
return -1
}
if !ok2 {
return 1
}
if v := compare(pv1.major, pv2.major); v != 0 {
return v
}
if v := compare(pv1.minor, pv2.minor); v != 0 {
return v
}
return compare(pv1.patch, pv2.patch)
}
// version represents a parsed MySQL version.
type version struct {
major int
minor int
patch int
}
// parseVersion returns an integer comparing the 2 versions.
func parseVersion(v string) (*version, bool) {
parts := strings.Split(v, ".")
if len(parts) == 0 {
return nil, false
}
var (
err error
ver = &version{}
)
for i, e := range []*int{&ver.major, &ver.minor, &ver.patch} {
if i == len(parts) {
break
}
if *e, err = strconv.Atoi(strings.Split(parts[i], "-")[0]); err != nil {
return nil, false
}
}
return ver, true
}
func compare(v1, v2 int) int {
if v1 == v2 {
return 0
}
if v1 < v2 {
return -1
}
return 1
}
func indexType(idx *Index, d string) (string, bool) {
ant := idx.Annotation
if ant == nil {
return "", false
}
if ant.Types != nil && ant.Types[d] != "" {
return ant.Types[d], true
}
if ant.Type != "" {
return ant.Type, true
}
return "", false
}
type driver struct {
sqlDialect
schema.Differ
migrate.PlanApplier
}
var drivers = func(v string) map[string]driver {
return map[string]driver{
entdialect.SQLite: {
&SQLite{
WithForeignKeys: true,
Driver: nopDriver{dialect: entdialect.SQLite},
},
sqlite.DefaultDiff,
sqlite.DefaultPlan,
},
entdialect.MySQL: {
&MySQL{
version: v,
Driver: nopDriver{dialect: entdialect.MySQL},
},
mysql.DefaultDiff,
mysql.DefaultPlan,
},
entdialect.Postgres: {
&Postgres{
version: v,
Driver: nopDriver{dialect: entdialect.Postgres},
},
postgres.DefaultDiff,
postgres.DefaultPlan,
},
}
}
type DDLArgs struct {
// Dialect and Version of the target database.
Dialect, Version string
// HashSymbols indicates whether to hash long symbols in the DDL.
HashSymbols bool
// Tables to dump.
Tables []*Table
// Options to pass to the migration plan engine.
Options []migrate.PlanOption
}
// Dump the schema DDL for the given tables.
//
// Deprecated: use DDL instead.
func Dump(ctx context.Context, dialect, version string, tables []*Table, opts ...migrate.PlanOption) (string, error) {
return DDL(ctx, DDLArgs{
Dialect: dialect,
Version: version,
Tables: tables,
Options: opts,
})
}
// DDL the schema DDL for the given tables.
func DDL(ctx context.Context, args DDLArgs) (string, error) {
args.Options = append([]migrate.PlanOption{func(o *migrate.PlanOptions) {
o.Mode = migrate.PlanModeDump
o.Indent = " "
}}, args.Options...)
d, ok := drivers(args.Version)[args.Dialect]
if !ok {
return "", fmt.Errorf("unsupported dialect %q", args.Dialect)
}
a := &Atlas{
sqlDialect: d,
dialect: args.Dialect,
hashSymbols: args.HashSymbols,
}
r, err := a.StateReader(args.Tables...).ReadState(ctx)
if err != nil {
return "", err
}
// Since the Atlas version bundled with Ent does not support view management,
// simply spit out the definition instead of letting Atlas plan them.
var vs []*schema.View
for _, s := range r.Schemas {
vs = append(vs, s.Views...)
s.Views = nil
}
var c schema.Changes
if slices.ContainsFunc(args.Tables, func(t *Table) bool { return t.Schema != "" }) {
c, err = d.RealmDiff(&schema.Realm{}, r)
} else {
c, err = d.SchemaDiff(&schema.Schema{}, r.Schemas[0])
}
if err != nil {
return "", err
}
p, err := d.PlanChanges(ctx, "dump", c, args.Options...)
if err != nil {
return "", err
}
for _, v := range vs {
q, _ := sql.Dialect(args.Dialect).
CreateView(v.Name).
Schema(v.Schema.Name).
Columns(func(cols []*schema.Column) (bs []*sql.ColumnBuilder) {
for _, c := range cols {
bs = append(bs, sql.Dialect(args.Dialect).Column(c.Name).Type(c.Type.Raw))
}
return
}(v.Columns)...).
As(sql.Raw(v.Def)).
Query()
p.Changes = append(p.Changes, &migrate.Change{
Cmd: q,
Comment: fmt.Sprintf("Add %q view", v.Name),
})
}
for _, t := range args.Tables {
p.Directives = append(p.Directives, fmt.Sprintf(
"-- atlas:pos %s%s[type=%s] %s",
func() string {
if t.Schema != "" {
return t.Schema + "[type=schema]."
}
return ""
}(),
t.Name,
func() string {
if t.View {
return "view"
}
return "table"
}(),
t.Pos,
))
}
f, err := migrate.DefaultFormatter.FormatFile(p)
if err != nil {
return "", err
}
return string(f.Bytes()), nil
}

View File

@@ -0,0 +1,390 @@
// 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 schema
import (
"context"
"fmt"
"strings"
"testing"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/schema/field"
"github.com/stretchr/testify/require"
)
func TestColumn_ConvertibleTo(t *testing.T) {
c1 := &Column{Type: field.TypeString, Size: 10}
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 10}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 255}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 9}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat32}))
c1 = &Column{Type: field.TypeFloat32}
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat32}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat64}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeString}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint}))
c1 = &Column{Type: field.TypeFloat64}
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat32}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat64}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeString}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint}))
c1 = &Column{Type: field.TypeUint}
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeUint}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeInt}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeInt64}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeUint64}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeInt8}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint8}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint16}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint32}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 1}))
c1 = &Column{Type: field.TypeInt}
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeInt}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeInt64}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeInt8}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeInt32}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint8}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint16}))
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint32}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString}))
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 1}))
}
func TestColumn_ScanDefault(t *testing.T) {
c1 := &Column{Type: field.TypeString, Size: 10}
require.NoError(t, c1.ScanDefault("Hello World"))
require.Equal(t, "Hello World", c1.Default)
require.NoError(t, c1.ScanDefault("1"))
require.Equal(t, "1", c1.Default)
c1 = &Column{Type: field.TypeInt64}
require.NoError(t, c1.ScanDefault("128"))
require.Equal(t, int64(128), c1.Default)
require.NoError(t, c1.ScanDefault("1"))
require.Equal(t, int64(1), c1.Default)
require.Error(t, c1.ScanDefault("foo"))
c1 = &Column{Type: field.TypeUint64}
require.NoError(t, c1.ScanDefault("128"))
require.Equal(t, uint64(128), c1.Default)
require.NoError(t, c1.ScanDefault("1"))
require.Equal(t, uint64(1), c1.Default)
require.Error(t, c1.ScanDefault("foo"))
c1 = &Column{Type: field.TypeFloat64}
require.NoError(t, c1.ScanDefault("128.1"))
require.Equal(t, 128.1, c1.Default)
require.NoError(t, c1.ScanDefault("1"))
require.Equal(t, float64(1), c1.Default)
require.Error(t, c1.ScanDefault("foo"))
c1 = &Column{Type: field.TypeBool}
require.NoError(t, c1.ScanDefault("1"))
require.Equal(t, true, c1.Default)
require.NoError(t, c1.ScanDefault("true"))
require.Equal(t, true, c1.Default)
require.NoError(t, c1.ScanDefault("0"))
require.Equal(t, false, c1.Default)
require.NoError(t, c1.ScanDefault("false"))
require.Equal(t, false, c1.Default)
require.Error(t, c1.ScanDefault("foo"))
c1 = &Column{Type: field.TypeUUID}
require.NoError(t, c1.ScanDefault("gen_random_uuid()"))
require.Equal(t, nil, c1.Default)
require.NoError(t, c1.ScanDefault("00000000-0000-0000-0000-000000000000"))
require.Equal(t, "00000000-0000-0000-0000-000000000000", c1.Default)
}
func TestCopyTables(t *testing.T) {
users := &Table{
Name: "users",
Columns: []*Column{
{Name: "id", Type: field.TypeInt},
{Name: "name", Type: field.TypeString},
{Name: "spouse_id", Type: field.TypeInt},
},
}
users.PrimaryKey = users.Columns[:1]
users.Indexes = append(users.Indexes, &Index{
Name: "name",
Columns: users.Columns[1:2],
})
users.AddForeignKey(&ForeignKey{
Columns: users.Columns[2:],
RefTable: users,
RefColumns: users.Columns[:1],
OnUpdate: SetNull,
})
users.SetAnnotation(&entsql.Annotation{Table: "Users"})
pets := &Table{
Name: "pets",
Columns: []*Column{
{Name: "id", Type: field.TypeInt},
{Name: "name", Type: field.TypeString},
{Name: "owner_id", Type: field.TypeInt},
},
}
pets.Indexes = append(pets.Indexes, &Index{
Name: "name",
Unique: true,
Columns: pets.Columns[1:2],
Annotation: entsql.Desc(),
})
pets.AddForeignKey(&ForeignKey{
Columns: pets.Columns[2:],
RefTable: users,
RefColumns: users.Columns[:1],
OnDelete: SetDefault,
})
tables := []*Table{users, pets}
copyT, err := CopyTables(tables)
require.NoError(t, err)
require.Equal(t, tables, copyT)
}
func TestDDL(t *testing.T) {
const (
hash = "249590215c5bc8be0106146e65d7fa7f"
idx = "super_duper_mega_ultra_hyper_giga_colossal_unbelievably_long_index_name"
hashMY = "super_duper_mega_ultra_hyper_gi_249590215c5bc8be0106146e65d7fa7f"
hashPG = "super_duper_mega_ultra_hyper_g_249590215c5bc8be0106146e65d7fa7f"
)
tbls := func() []*Table {
users := &Table{
Name: "users",
Pos: "users.go:15",
Columns: []*Column{
{Name: "id", Type: field.TypeInt},
{Name: "name", Type: field.TypeString},
{Name: "spouse_id", Type: field.TypeInt},
},
}
users.PrimaryKey = users.Columns[:1]
users.Indexes = append(users.Indexes, &Index{
Name: "name",
Columns: users.Columns[1:2],
})
users.AddForeignKey(&ForeignKey{
Columns: users.Columns[2:],
RefTable: users,
RefColumns: users.Columns[:1],
OnUpdate: SetDefault,
})
users.SetAnnotation(&entsql.Annotation{Table: "Users"})
pets := &Table{
Name: "pets",
Pos: "pets.go:15",
Columns: []*Column{
{Name: "id", Type: field.TypeInt},
{Name: "name", Type: field.TypeString},
{Name: "fur_color", Type: field.TypeEnum, Enums: []string{"black", "white"}},
{Name: "owner_id", Type: field.TypeInt},
},
}
pets.Indexes = append(pets.Indexes, &Index{
Name: idx,
Unique: true,
Columns: pets.Columns[1:2],
Annotation: entsql.Desc(),
})
pets.AddForeignKey(&ForeignKey{
Columns: pets.Columns[3:],
RefTable: users,
RefColumns: users.Columns[:1],
OnDelete: SetDefault,
})
petsWithoutFur := &Table{
Name: "pets_without_fur",
Pos: "pets.go:30",
View: true,
Columns: append(pets.Columns[:2], pets.Columns[3]),
Annotation: entsql.View("SELECT id, name, owner_id FROM pets"),
}
petNames := &Table{
Name: "pet_names",
Pos: "pets.go:45",
View: true,
Columns: pets.Columns[1:1],
Annotation: entsql.ViewFor(dialect.Postgres, func(s *sql.Selector) {
s.Select("name").From(sql.Table("pets"))
}),
}
return []*Table{users, pets, petsWithoutFur, petNames}
}
my := func(length int, idx string) string {
return fmt.Sprintf(strings.ReplaceAll(`-- Add new schema named "s1"
CREATE DATABASE $s1$;
-- Add new schema named "s2"
CREATE DATABASE $s2$;
-- Add new schema named "s3"
CREATE DATABASE $s3$;
-- Create "users" table
CREATE TABLE $s1$.$users$ (
$id$ bigint NOT NULL,
$name$ varchar(%d) NOT NULL,
$spouse_id$ bigint NOT NULL,
PRIMARY KEY ($id$),
INDEX $name$ ($name$),
FOREIGN KEY ($spouse_id$) REFERENCES $s1$.$users$ ($id$) ON UPDATE SET DEFAULT
) CHARSET utf8mb4 COLLATE utf8mb4_bin;
-- Create "pets" table
CREATE TABLE $s2$.$pets$ (
$id$ bigint NOT NULL,
$name$ varchar(%d) NOT NULL,
$owner_id$ bigint NOT NULL,
$owner_id$ bigint NOT NULL,
UNIQUE INDEX $%s$ ($name$ DESC),
FOREIGN KEY ($owner_id$) REFERENCES $s1$.$users$ ($id$) ON DELETE SET DEFAULT
) CHARSET utf8mb4 COLLATE utf8mb4_bin;
-- Add "pets_without_fur" view
CREATE VIEW $s3$.$pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, owner_id FROM pets;
`, "$", "`"), length, length, idx)
}
pg := func(idx string) string {
return fmt.Sprintf(`-- Add new schema named "s1"
CREATE SCHEMA "s1";
-- Add new schema named "s2"
CREATE SCHEMA "s2";
-- Add new schema named "s3"
CREATE SCHEMA "s3";
-- Create "users" table
CREATE TABLE "s1"."users" (
"id" bigint NOT NULL,
"name" character varying NOT NULL,
"spouse_id" bigint NOT NULL,
PRIMARY KEY ("id"),
FOREIGN KEY ("spouse_id") REFERENCES "s1"."users" ("id") ON UPDATE SET DEFAULT
);
-- Create index "name" to table: "users"
CREATE INDEX "name" ON "s1"."users" ("name");
-- Create "pets" table
CREATE TABLE "s2"."pets" (
"id" bigint NOT NULL,
"name" character varying NOT NULL,
"owner_id" bigint NOT NULL,
"owner_id" bigint NOT NULL,
FOREIGN KEY ("owner_id") REFERENCES "s1"."users" ("id") ON DELETE SET DEFAULT
);
-- Create index "%s" to table: "pets"
CREATE UNIQUE INDEX "%s" ON "s2"."pets" ("name" DESC);
-- Add "pets_without_fur" view
CREATE VIEW "s3"."pets_without_fur" ("id", "name", "owner_id") AS SELECT id, name, owner_id FROM pets;
-- Add "pet_names" view
CREATE VIEW "s3"."pet_names" AS SELECT "name" FROM "pets";
`, idx, idx)
}
for _, tt := range []struct {
dialect, version, expected string
hash bool
}{
{
dialect.SQLite, "",
fmt.Sprintf(strings.ReplaceAll(`-- Create "users" table
CREATE TABLE $users$ (
$id$ integer NOT NULL,
$name$ text NOT NULL,
$spouse_id$ integer NOT NULL,
PRIMARY KEY ($id$),
FOREIGN KEY ($spouse_id$) REFERENCES $users$ ($id$) ON UPDATE SET DEFAULT
);
-- Create index "name" to table: "users"
CREATE INDEX $name$ ON $users$ ($name$);
-- Create "pets" table
CREATE TABLE $pets$ (
$id$ integer NOT NULL,
$name$ text NOT NULL,
$owner_id$ integer NOT NULL,
$owner_id$ integer NOT NULL,
FOREIGN KEY ($owner_id$) REFERENCES $users$ ($id$) ON DELETE SET DEFAULT
);
-- Create index "%s" to table: "pets"
CREATE UNIQUE INDEX $%s$ ON $pets$ ($name$ DESC);
-- Add "pets_without_fur" view
CREATE VIEW $pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, owner_id FROM pets;
`, "$", "`"), idx, idx), false,
},
{dialect.MySQL, "5.6", my(255, idx), false},
{dialect.MySQL, "5.6", my(191, hashMY), true},
{dialect.MySQL, "5.7", my(255, idx), false},
{dialect.MySQL, "8", my(255, hashMY), true},
{dialect.Postgres, "12", pg(idx), false},
{dialect.Postgres, "13", pg(idx), false},
{dialect.Postgres, "14", pg(hashPG), true},
{dialect.Postgres, "15", pg(hashPG), true},
} {
n := tt.dialect
if tt.version != "" {
n += ":" + tt.version
}
pos := `-- atlas:pos users[type=table] users.go:15
-- atlas:pos pets[type=table] pets.go:15
-- atlas:pos pets_without_fur[type=view] pets.go:30
-- atlas:pos pet_names[type=view] pets.go:45
`
tables := tbls()
if tt.dialect != dialect.SQLite {
tables[0].Schema = "s1"
tables[1].Schema = "s2"
tables[2].Schema = "s3"
tables[3].Schema = "s3"
pos = `-- atlas:pos s1[type=schema].users[type=table] users.go:15
-- atlas:pos s2[type=schema].pets[type=table] pets.go:15
-- atlas:pos s3[type=schema].pets_without_fur[type=view] pets.go:30
-- atlas:pos s3[type=schema].pet_names[type=view] pets.go:45
`
}
t.Run(n, func(t *testing.T) {
ac, err := DDL(context.Background(), DDLArgs{
Dialect: tt.dialect,
Version: tt.version,
Tables: tables,
HashSymbols: tt.hash,
})
require.NoError(t, err)
require.Equal(t, pos+tt.expected, ac)
})
t.Run(n+" single schema", func(t *testing.T) {
ac, err := DDL(context.Background(), DDLArgs{
Dialect: tt.dialect,
Version: tt.version,
Tables: tables[0:1],
HashSymbols: tt.hash,
})
require.NoError(t, err)
if tt.dialect != dialect.SQLite {
require.Contains(t, ac, "s1[type=schema].")
require.NotContains(t, ac, "s2[type=schema].")
require.Contains(t, ac, "-- Add new schema named \"s1\"")
}
})
t.Run(n+" no schema", func(t *testing.T) {
tables[0].Schema = ""
ac, err := DDL(context.Background(), DDLArgs{
Dialect: tt.dialect,
Version: tt.version,
Tables: tables[0:1],
HashSymbols: tt.hash,
})
require.NoError(t, err)
require.NotContains(t, ac, "[type=schema].")
require.Contains(t, ac, "[type=table]")
})
}
}

View File

@@ -0,0 +1,227 @@
// 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 schema
import (
"context"
stdsql "database/sql"
"fmt"
"reflect"
"strconv"
"strings"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/schema/field"
"ariga.io/atlas/sql/migrate"
"ariga.io/atlas/sql/schema"
"ariga.io/atlas/sql/sqlite"
)
type (
// SQLite adapter for Atlas migration engine.
SQLite struct {
dialect.Driver
WithForeignKeys bool
}
// SQLiteTx implements dialect.Tx.
SQLiteTx struct {
dialect.Tx
commit func() error // Override Commit to toggle foreign keys back on after Commit.
rollback func() error // Override Rollback to toggle foreign keys back on after Rollback.
}
)
// Tx implements opens a transaction.
func (d *SQLite) Tx(ctx context.Context) (dialect.Tx, error) {
db := &db{d}
if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = off"); err != nil {
return nil, fmt.Errorf("sqlite: set 'foreign_keys = off': %w", err)
}
t, err := d.Driver.Tx(ctx)
if err != nil {
return nil, err
}
tx := &tx{t}
cm, err := sqlite.CommitFunc(ctx, db, tx, true)
if err != nil {
return nil, err
}
return &SQLiteTx{Tx: t, commit: cm, rollback: sqlite.RollbackFunc(ctx, db, tx, true)}, nil
}
// Commit ensures foreign keys are toggled back on after commit.
func (tx *SQLiteTx) Commit() error {
return tx.commit()
}
// Rollback ensures foreign keys are toggled back on after rollback.
func (tx *SQLiteTx) Rollback() error {
return tx.rollback()
}
// init makes sure that foreign_keys support is enabled.
func (d *SQLite) init(ctx context.Context) error {
on, err := exist(ctx, d, "PRAGMA foreign_keys")
if err != nil {
return fmt.Errorf("sqlite: check foreign_keys pragma: %w", err)
}
if !on {
// foreign_keys pragma is off, either enable it by execute "PRAGMA foreign_keys=ON"
// or add the following parameter in the connection string "_fk=1".
return fmt.Errorf("sqlite: foreign_keys pragma is off: missing %q in the connection string", "_fk=1")
}
return nil
}
func (d *SQLite) tableExist(ctx context.Context, conn dialect.ExecQuerier, name string) (bool, error) {
query, args := sql.Select().Count().
From(sql.Table("sqlite_master")).
Where(sql.And(
sql.EQ("type", "table"),
sql.EQ("name", name),
)).
Query()
return exist(ctx, conn, query, args...)
}
func (d *SQLite) atOpen(conn dialect.ExecQuerier) (migrate.Driver, error) {
return sqlite.Open(&db{ExecQuerier: conn})
}
func (d *SQLite) atTable(t1 *Table, t2 *schema.Table) {
if t1.Annotation != nil {
setAtChecks(t1, t2)
}
}
func (d *SQLite) supportsDefault(*Column) bool {
// SQLite supports default values for all standard types.
return true
}
func (d *SQLite) atTypeC(c1 *Column, c2 *schema.Column) error {
if c1.SchemaType != nil && c1.SchemaType[dialect.SQLite] != "" {
t, err := sqlite.ParseType(strings.ToLower(c1.SchemaType[dialect.SQLite]))
if err != nil {
return err
}
c2.Type.Type = t
return nil
}
var t schema.Type
switch c1.Type {
case field.TypeBlob:
return fmt.Errorf("blob fields are not stored in the database")
case field.TypeBool:
t = &schema.BoolType{T: "bool"}
case field.TypeInt8, field.TypeUint8, field.TypeInt16, field.TypeUint16, field.TypeInt32,
field.TypeUint32, field.TypeUint, field.TypeInt, field.TypeInt64, field.TypeUint64:
t = &schema.IntegerType{T: sqlite.TypeInteger}
case field.TypeBytes:
t = &schema.BinaryType{T: sqlite.TypeBlob}
case field.TypeString, field.TypeEnum:
// SQLite does not impose any length restrictions on
// the length of strings, BLOBs or numeric values.
t = &schema.StringType{T: sqlite.TypeText}
case field.TypeFloat32, field.TypeFloat64:
t = &schema.FloatType{T: sqlite.TypeReal}
case field.TypeTime:
t = &schema.TimeType{T: "datetime"}
case field.TypeJSON:
t = &schema.JSONType{T: "json"}
case field.TypeUUID:
t = &sqlite.UUIDType{T: "uuid"}
case field.TypeOther:
t = &schema.UnsupportedType{T: c1.typ}
default:
t, err := sqlite.ParseType(strings.ToLower(c1.typ))
if err != nil {
return err
}
c2.Type.Type = t
}
c2.Type.Type = t
return nil
}
func (d *SQLite) atUniqueC(t1 *Table, c1 *Column, t2 *schema.Table, c2 *schema.Column) {
// For UNIQUE columns, SQLite create an implicit index named
// "sqlite_autoindex_<table>_<i>". Ent uses the PostgreSQL approach
// in its migration, and name these indexes as "<table>_<column>_key".
for _, idx := range t1.Indexes {
// Index also defined explicitly, and will be add in atIndexes.
if idx.Unique && d.atImplicitIndexName(idx, t1, c1) {
return
}
}
t2.AddIndexes(schema.NewUniqueIndex(fmt.Sprintf("%s_%s_key", t2.Name, c1.Name)).AddColumns(c2))
}
func (d *SQLite) atImplicitIndexName(idx *Index, t1 *Table, c1 *Column) bool {
if idx.Name == c1.Name {
return true
}
p := fmt.Sprintf("sqlite_autoindex_%s_", t1.Name)
if !strings.HasPrefix(idx.Name, p) {
return false
}
i, err := strconv.ParseInt(strings.TrimPrefix(idx.Name, p), 10, 64)
return err == nil && i > 0
}
func (d *SQLite) atIncrementC(t *schema.Table, c *schema.Column) {
if c.Default != nil {
t.Attrs = removeAttr(t.Attrs, reflect.TypeOf(&sqlite.AutoIncrement{}))
} else {
c.AddAttrs(&sqlite.AutoIncrement{})
}
}
func (d *SQLite) atIncrementT(t *schema.Table, v int64) {
t.AddAttrs(&sqlite.AutoIncrement{Seq: v})
}
func (d *SQLite) atIndex(idx1 *Index, t2 *schema.Table, idx2 *schema.Index) error {
for _, c1 := range idx1.Columns {
c2, ok := t2.Column(c1.Name)
if !ok {
return fmt.Errorf("unexpected index %q column: %q", idx1.Name, c1.Name)
}
idx2.AddParts(&schema.IndexPart{C: c2})
}
if idx1.Annotation != nil && idx1.Annotation.Where != "" {
idx2.AddAttrs(&sqlite.IndexPredicate{P: idx1.Annotation.Where})
}
return nil
}
func (*SQLite) atTypeRangeSQL(ts ...string) string {
for i := range ts {
ts[i] = fmt.Sprintf("('%s')", ts[i])
}
return fmt.Sprintf("INSERT INTO `%s` (`type`) VALUES %s", TypeTable, strings.Join(ts, ", "))
}
type tx struct {
dialect.Tx
}
func (tx *tx) QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error) {
rows := &sql.Rows{}
if err := tx.Query(ctx, query, args, rows); err != nil {
return nil, err
}
return rows.ColumnScanner.(*stdsql.Rows), nil
}
func (tx *tx) ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error) {
var r stdsql.Result
if err := tx.Exec(ctx, query, args, &r); err != nil {
return nil, err
}
return r, nil
}

View File

@@ -0,0 +1,365 @@
// 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 schema
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"ariga.io/atlas/sql/migrate"
)
type (
// WriteDriver is a driver that writes all driver exec operations to its writer.
// Note that this driver is used only for printing or writing statements to SQL
// files, and may require manual changes to the generated SQL statements.
WriteDriver struct {
dialect.Driver // optional driver for query calls.
io.Writer // target for exec statements.
FormatFunc func(string) (string, error)
}
// DirWriter implements the io.Writer interface
// for writing to an Atlas managed directory.
DirWriter struct {
Dir migrate.Dir // target directory.
Formatter migrate.Formatter // optional formatter.
b bytes.Buffer // working buffer.
changes []*migrate.Change // changes to flush.
}
)
// Write implements the io.Writer interface.
func (d *DirWriter) Write(p []byte) (int, error) {
return d.b.Write(trimReturning(p))
}
// Change converts all written statement so far into a migration
// change with the given comment.
func (d *DirWriter) Change(comment string) {
// Trim semicolon and new line, because formatter adds it.
d.changes = append(d.changes, &migrate.Change{Comment: comment, Cmd: strings.TrimRight(d.b.String(), ";\n")})
d.b.Reset()
}
// Flush flushes the written statements to the directory.
func (d *DirWriter) Flush(name string) error {
switch {
case d.b.Len() != 0:
return fmt.Errorf("writer has undocumented change. Use Change or FlushChange instead")
case len(d.changes) == 0:
return errors.New("writer has no changes to flush")
default:
return migrate.NewPlanner(nil, d.Dir, migrate.PlanFormat(d.Formatter)).
WritePlan(&migrate.Plan{
Name: name,
Changes: d.changes,
})
}
}
// FlushChange combines Change and Flush.
func (d *DirWriter) FlushChange(name, comment string) error {
d.Change(comment)
return d.Flush(name)
}
// NewWriteDriver creates a dialect.Driver that writes all driver exec statement to its writer.
func NewWriteDriver(dialect string, w io.Writer) *WriteDriver {
return &WriteDriver{
Writer: w,
Driver: nopDriver{dialect: dialect},
}
}
// Exec implements the dialect.Driver.Exec method.
func (w *WriteDriver) Exec(_ context.Context, query string, args, res any) error {
if rr, ok := res.(*sql.Result); ok {
*rr = noResult{}
}
if !strings.HasSuffix(query, ";") {
query += ";"
}
if args != nil {
args, ok := args.([]any)
if !ok {
return fmt.Errorf("unexpected args type: %T", args)
}
query = w.expandArgs(query, args)
}
_, err := io.WriteString(w, query+"\n")
return err
}
// Query implements the dialect.Driver.Query method.
func (w *WriteDriver) Query(ctx context.Context, query string, args, res any) error {
if strings.HasPrefix(query, "INSERT") || strings.HasPrefix(query, "UPDATE") {
if err := w.Exec(ctx, query, args, nil); err != nil {
return err
}
if rr, ok := res.(*sql.Rows); ok {
cols := func() []string {
// If the query has a RETURNING clause, mock the result.
var clause string
outer:
for i := 0; i < len(query); i++ {
switch q := query[i]; {
case q == '\'', q == '"', q == '`': // string or identifier
_, skip := skipQuoted(query, i)
if skip == -1 {
return nil // malformed SQL
}
i = skip
continue
case reReturning.MatchString(query[i:]):
var j int
inner:
// Forward until next unquoted ';' appears, or we reach the end of the query.
for j = i; j < len(query); j++ {
switch query[j] {
case '\'', '"', '`': // string or identifier
_, skip := skipQuoted(query, j)
if skip == -1 {
return nil // malformed RETURNING clause
}
j = skip
case ';':
break inner
}
}
clause = query[i:j]
break outer
}
}
cols := strings.Split(reReturning.ReplaceAllString(clause, ""), ",")
for i := range cols {
cols[i] = strings.TrimSpace(cols[i])
}
return cols
}()
*rr = sql.Rows{ColumnScanner: &noRows{cols: cols}}
}
return nil
}
switch w.Driver.(type) {
case nil, nopDriver:
return errors.New("query is not supported by the WriteDriver")
default:
return w.Driver.Query(ctx, query, args, res)
}
}
// expandArgs combines to arguments and statement into a single statement to
// print or write into a file (before editing).
// Note, the output may be incorrect or unsafe SQL and require manual changes.
func (w *WriteDriver) expandArgs(query string, args []any) string {
var (
b strings.Builder
p = w.placeholder()
scan = w.scanPlaceholder()
)
for i := 0; i < len(query); i++ {
Top:
switch query[i] {
case p:
idx, size := scan(query[i+1:])
// Unrecognized placeholder.
if idx < 0 || idx >= len(args) {
return query
}
i += size
v, err := w.formatArg(args[idx])
if err != nil {
// Unexpected formatting error.
return query
}
b.WriteString(v)
// String or identifier.
case '\'', '"', '`':
for j := i + 1; j < len(query); j++ {
switch query[j] {
case '\\':
j++
case query[i]:
b.WriteString(query[i : j+1])
i = j
break Top
}
}
// Unexpected EOS.
return query
default:
b.WriteByte(query[i])
}
}
return b.String()
}
func (w *WriteDriver) scanPlaceholder() func(string) (int, int) {
switch w.Dialect() {
case dialect.Postgres:
return func(s string) (int, int) {
var i int
for i < len(s) && unicode.IsDigit(rune(s[i])) {
i++
}
idx, err := strconv.ParseInt(s[:i], 10, 64)
if err != nil {
return -1, 0
}
// Placeholders are 1-based.
return int(idx) - 1, i
}
default:
idx := -1
return func(string) (int, int) {
idx++
return idx, 0
}
}
}
func (w *WriteDriver) placeholder() byte {
if w.Dialect() == dialect.Postgres {
return '$'
}
return '?'
}
func (w *WriteDriver) formatArg(v any) (string, error) {
if w.FormatFunc != nil {
return w.FormatFunc(fmt.Sprint(v))
}
switch v := v.(type) {
case nil:
return "NULL", nil
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v), nil
case float32, float64:
return fmt.Sprintf("%g", v), nil
case bool:
if v {
return "1", nil
} else {
return "0", nil
}
case string:
return "'" + strings.ReplaceAll(v, "'", "''") + "'", nil
case json.RawMessage:
return "'" + strings.ReplaceAll(string(v), "'", "''") + "'", nil
case []byte:
return "{{ BINARY_VALUE }}", nil
case time.Time:
return "{{ TIME_VALUE }}", nil
case fmt.Stringer:
return "'" + strings.ReplaceAll(v.String(), "'", "''") + "'", nil
default:
return "{{ VALUE }}", nil
}
}
var reReturning = regexp.MustCompile(`(?i)^\s?RETURNING`)
// trimReturning trims any RETURNING suffix from INSERT/UPDATE queries.
// Note, that the output may be incorrect or unsafe SQL and require manual changes.
func trimReturning(query []byte) []byte {
var b bytes.Buffer
loop:
for i := 0; i < len(query); i++ {
switch q := query[i]; {
case q == '\'', q == '"', q == '`': // string or identifier
s, skip := skipQuoted(query, i)
if skip == -1 {
return query
}
b.Write(s)
i = skip
continue
case reReturning.Match(query[i:]):
// Forward until next unquoted ';' appears.
for j := i; j < len(query); j++ { // skip "RETURNING"
switch query[j] {
case '\'', '"', '`': // string or identifier
_, skip := skipQuoted(query, j)
if skip == -1 {
return query
}
j = skip
case ';':
b.WriteString(";")
i += j
continue loop
}
}
}
b.WriteByte(query[i])
}
return b.Bytes()
}
func skipQuoted[T []byte | string](query T, idx int) (T, int) {
for j := idx + 1; j < len(query); j++ {
switch query[j] {
case '\\':
j++
case query[idx]:
return query[idx : j+1], j
}
}
// Unexpected EOS.
return query, -1
}
// Tx writes the transaction start.
func (w *WriteDriver) Tx(context.Context) (dialect.Tx, error) {
return dialect.NopTx(w), nil
}
// noResult represents a zero result.
type noResult struct{}
func (noResult) LastInsertId() (int64, error) { return 0, nil }
func (noResult) RowsAffected() (int64, error) { return 0, nil }
// noRows represents no rows.
type noRows struct {
sql.ColumnScanner
cols []string
done bool
}
func (*noRows) Close() error { return nil }
func (*noRows) Err() error { return nil }
func (r *noRows) Next() bool {
if !r.done {
r.done = true
return true
}
return false
}
func (r *noRows) Columns() ([]string, error) { return r.cols, nil }
func (*noRows) Scan(...any) error { return nil }
type nopDriver struct {
dialect.Driver
dialect string
}
func (d nopDriver) Dialect() string { return d.dialect }
func (nopDriver) Query(context.Context, string, any, any) error {
return nil
}

View File

@@ -0,0 +1,189 @@
// 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 schema
import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqljson"
"ariga.io/atlas/sql/migrate"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestWriteDriver(t *testing.T) {
b := &bytes.Buffer{}
w := NewWriteDriver(dialect.MySQL, b)
ctx := context.Background()
tx, err := w.Tx(ctx)
require.NoError(t, err)
err = tx.Query(ctx, "SELECT `name` FROM `users`", nil, nil)
require.EqualError(t, err, "query is not supported by the WriteDriver")
err = tx.Exec(ctx, "ALTER TABLE `users` ADD COLUMN `age` int", nil, nil)
require.NoError(t, err)
err = tx.Exec(ctx, "ALTER TABLE `users` ADD COLUMN `NAME` varchar(100);", nil, nil)
require.NoError(t, err)
require.NoError(t, tx.Commit())
lines := strings.Split(b.String(), "\n")
require.Len(t, lines, 3)
require.Equal(t, "ALTER TABLE `users` ADD COLUMN `age` int;", lines[0])
require.Equal(t, "ALTER TABLE `users` ADD COLUMN `NAME` varchar(100);", lines[1])
require.Empty(t, lines[2], "file ends with blank line")
b.Reset()
query, args := sql.Update("users").Schema("test").Set("a", 1).Set("b", "a").Set("c", "'c'").Set("d", true).Where(sql.EQ("p", 0.2)).Query()
err = w.Exec(ctx, query, args, nil)
require.NoError(t, err)
require.Equal(t, "UPDATE `test`.`users` SET `a` = 1, `b` = 'a', `c` = '''c''', `d` = 1 WHERE `p` = 0.2;\n", b.String())
b.Reset()
query, args = sql.Dialect(dialect.MySQL).Update("users").Schema("test").Set("a", "{}").Where(sqljson.ValueIsNull("a")).Query()
err = w.Exec(ctx, query, args, nil)
require.NoError(t, err)
require.Equal(t, "UPDATE `test`.`users` SET `a` = '{}' WHERE JSON_CONTAINS(`a`, 'null', '$');\n", b.String())
b.Reset()
w = NewWriteDriver(dialect.Postgres, b)
query, args = sql.Dialect(dialect.Postgres).Update("users").Set("id", uuid.Nil).Set("a", 1).Set("b", time.Now()).Query()
err = w.Exec(ctx, query, args, nil)
require.NoError(t, err)
require.Equal(t, `UPDATE "users" SET "id" = '00000000-0000-0000-0000-000000000000', "a" = 1, "b" = {{ TIME_VALUE }};`+"\n", b.String())
b.Reset()
err = w.Exec(ctx, `INSERT INTO "users" (name) VALUES("a8m") RETURNING id`, nil, nil)
require.NoError(t, err)
require.Equal(t, `INSERT INTO "users" (name) VALUES("a8m") RETURNING id;`+"\n", b.String())
// batchCreator uses tx.Query when doing an insert
b.Reset()
err = w.Query(ctx, `INSERT INTO "users" (name) VALUES("a8m") RETURNING id`, nil, nil)
require.NoError(t, err)
require.Equal(t, `INSERT INTO "users" (name) VALUES("a8m") RETURNING id;`+"\n", b.String())
// correct columns are extracted from a returning clause and returned by sql.ColumnScanner.
for q, cols := range map[string][]string{
`INSERT INTO "users" (name) VALUES("a8m") RETURNING id`: {"id"},
`INSERT INTO "users" (name) VALUES("a8m") RETURNING id, "name"`: {"id", `"name"`},
`INSERT INTO "users" (name) VALUES("a8m") RETURNING "id", "name"`: {`"id"`, `"name"`},
`INSERT INTO "users" (name) VALUES("a8m") RETURNING "id", "name"; DROP "groups"`: {`"id"`, `"name"`},
} {
var rows sql.Rows
err = w.Query(ctx, q, nil, &rows)
require.NoError(t, err)
require.True(t, rows.Next())
c, err := rows.Columns()
require.NoError(t, err)
require.Equal(t, cols, c)
require.NoError(t, rows.Scan())
}
b.Reset()
}
func TestDirWriter(t *testing.T) {
for _, tt := range []struct {
dialect string
exec []string
comments []string
args [][]any
want string
}{
{
dialect.MySQL,
[]string{
"UPDATE `test`.`users` SET `a` = ?",
"UPDATE `test`.`users` SET `b` = ?",
},
[]string{
"Comment 1.",
"Comment 2.",
},
[][]any{
{1},
{2},
},
"-- Comment 1.\nUPDATE `test`.`users` SET `a` = 1;\n-- Comment 2.\nUPDATE `test`.`users` SET `b` = 2;\n",
},
{
dialect.Postgres,
[]string{
"INSERT INTO \"users\" (\"name\", \"email\") VALUES ($1, $2) RETURNING \"id\"",
"INSERT INTO \"groups\" (\"name\") VALUES ($1) RETURNING \"id\"",
},
[]string{
"Seed users table",
"Seed groups table",
},
[][]any{
{"masseelch", "j@ariga.io"},
{"admins"},
},
strings.Join([]string{
"-- Seed users table\nINSERT INTO \"users\" (\"name\", \"email\") VALUES ('masseelch', 'j@ariga.io');\n",
"-- Seed groups table\nINSERT INTO \"groups\" (\"name\") VALUES ('admins');\n",
}, ""),
},
{
dialect.SQLite,
[]string{
"INSERT INTO `users` (`name`, `email`) VALUES (?, ?) RETURNING `id`",
"INSERT INTO `groups` (`name`) VALUES (?) RETURNING `id`",
},
[]string{
"Seed users table",
"Seed groups table",
},
[][]any{
{"masseelch", "j@ariga.io"},
{"admins"},
},
strings.Join([]string{
"-- Seed users table\nINSERT INTO `users` (`name`, `email`) VALUES ('masseelch', 'j@ariga.io');\n",
"-- Seed groups table\nINSERT INTO `groups` (`name`) VALUES ('admins');\n",
}, ""),
},
{
dialect.SQLite + " no space",
[]string{"INSERT INTO `users` (`name`) VALUES (?)RETURNING `id`"},
[]string{"Seed users table"},
[][]any{{"masseelch"}},
"-- Seed users table\nINSERT INTO `users` (`name`) VALUES ('masseelch');\n",
},
} {
t.Run(tt.dialect, func(t *testing.T) {
var (
p = t.TempDir()
dir = func() migrate.Dir {
d, err := migrate.NewLocalDir(p)
require.NoError(t, err)
return d
}()
w = &DirWriter{Dir: dir}
drv = NewWriteDriver(tt.dialect, w)
)
for i := range tt.exec {
require.NoError(t, drv.Exec(context.Background(), tt.exec[i], tt.args[i], nil))
w.Change(tt.comments[i])
}
require.NoError(t, w.Flush("migration_file"))
files, err := os.ReadDir(p)
require.NoError(t, err)
require.Len(t, files, 2)
require.Contains(t, files[0].Name(), "_migration_file.sql")
buf, err := os.ReadFile(filepath.Join(p, files[0].Name()))
require.NoError(t, err)
require.Equal(t, tt.want, string(buf))
require.Equal(t, "atlas.sum", files[1].Name())
})
}
}

428
dialect/sql/sql.go Normal file
View File

@@ -0,0 +1,428 @@
// 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 sql
import (
"fmt"
"strings"
"entgo.io/ent/dialect"
)
// The following helpers exist to simplify the way raw predicates
// are defined and used in both ent/schema and generated code. For
// full predicates API, check out the sql.P in builder.go.
// FieldIsNull returns a raw predicate to check if the given field is NULL.
func FieldIsNull(name string) func(*Selector) {
return func(s *Selector) {
s.Where(IsNull(s.C(name)))
}
}
// FieldNotNull returns a raw predicate to check if the given field is not NULL.
func FieldNotNull(name string) func(*Selector) {
return func(s *Selector) {
s.Where(NotNull(s.C(name)))
}
}
// FieldEQ returns a raw predicate to check if the given field equals to the given value.
func FieldEQ(name string, v any) func(*Selector) {
return func(s *Selector) {
s.Where(EQ(s.C(name), v))
}
}
// FieldsEQ returns a raw predicate to check if the given fields (columns) are equal.
func FieldsEQ(field1, field2 string) func(*Selector) {
return func(s *Selector) {
s.Where(ColumnsEQ(s.C(field1), s.C(field2)))
}
}
// FieldNEQ returns a raw predicate to check if the given field does not equal to the given value.
func FieldNEQ(name string, v any) func(*Selector) {
return func(s *Selector) {
s.Where(NEQ(s.C(name), v))
}
}
// FieldsNEQ returns a raw predicate to check if the given fields (columns) are not equal.
func FieldsNEQ(field1, field2 string) func(*Selector) {
return func(s *Selector) {
s.Where(ColumnsNEQ(s.C(field1), s.C(field2)))
}
}
// FieldGT returns a raw predicate to check if the given field is greater than the given value.
func FieldGT(name string, v any) func(*Selector) {
return func(s *Selector) {
s.Where(GT(s.C(name), v))
}
}
// FieldsGT returns a raw predicate to check if field1 is greater than field2.
func FieldsGT(field1, field2 string) func(*Selector) {
return func(s *Selector) {
s.Where(ColumnsGT(s.C(field1), s.C(field2)))
}
}
// FieldGTE returns a raw predicate to check if the given field is greater than or equal the given value.
func FieldGTE(name string, v any) func(*Selector) {
return func(s *Selector) {
s.Where(GTE(s.C(name), v))
}
}
// FieldsGTE returns a raw predicate to check if field1 is greater than or equal field2.
func FieldsGTE(field1, field2 string) func(*Selector) {
return func(s *Selector) {
s.Where(ColumnsGTE(s.C(field1), s.C(field2)))
}
}
// FieldLT returns a raw predicate to check if the value of the field is less than the given value.
func FieldLT(name string, v any) func(*Selector) {
return func(s *Selector) {
s.Where(LT(s.C(name), v))
}
}
// FieldsLT returns a raw predicate to check if field1 is lower than field2.
func FieldsLT(field1, field2 string) func(*Selector) {
return func(s *Selector) {
s.Where(ColumnsLT(s.C(field1), s.C(field2)))
}
}
// FieldLTE returns a raw predicate to check if the value of the field is less than the given value.
func FieldLTE(name string, v any) func(*Selector) {
return func(s *Selector) {
s.Where(LTE(s.C(name), v))
}
}
// FieldsLTE returns a raw predicate to check if field1 is lower than or equal field2.
func FieldsLTE(field1, field2 string) func(*Selector) {
return func(s *Selector) {
s.Where(ColumnsLTE(s.C(field1), s.C(field2)))
}
}
// FieldsHasPrefix returns a raw predicate to checks if field1 begins with the value of field2.
func FieldsHasPrefix(field1, field2 string) func(*Selector) {
return func(s *Selector) {
s.Where(ColumnsHasPrefix(s.C(field1), s.C(field2)))
}
}
// FieldIn returns a raw predicate to check if the value of the field is IN the given values.
func FieldIn[T any](name string, vs ...T) func(*Selector) {
return func(s *Selector) {
v := make([]any, len(vs))
for i := range v {
v[i] = vs[i]
}
s.Where(In(s.C(name), v...))
}
}
// FieldNotIn returns a raw predicate to check if the value of the field is NOT IN the given values.
func FieldNotIn[T any](name string, vs ...T) func(*Selector) {
return func(s *Selector) {
v := make([]any, len(vs))
for i := range v {
v[i] = vs[i]
}
s.Where(NotIn(s.C(name), v...))
}
}
// FieldEqualFold returns a raw predicate to check if the field is equal to the given string under case-folding.
func FieldEqualFold(name string, substr string) func(*Selector) {
return func(s *Selector) {
s.Where(EqualFold(s.C(name), substr))
}
}
// FieldHasPrefix returns a raw predicate to check if the field has the given prefix.
func FieldHasPrefix(name string, prefix string) func(*Selector) {
return func(s *Selector) {
s.Where(HasPrefix(s.C(name), prefix))
}
}
// FieldHasPrefixFold returns a raw predicate to check if the field has the given prefix with case-folding
func FieldHasPrefixFold(name string, prefix string) func(*Selector) {
return func(s *Selector) {
s.Where(HasPrefixFold(s.C(name), prefix))
}
}
// FieldHasSuffix returns a raw predicate to check if the field has the given suffix.
func FieldHasSuffix(name string, suffix string) func(*Selector) {
return func(s *Selector) {
s.Where(HasSuffix(s.C(name), suffix))
}
}
// FieldHasSuffixFold returns a raw predicate to check if the field has the given suffix with case-folding
func FieldHasSuffixFold(name string, suffix string) func(*Selector) {
return func(s *Selector) {
s.Where(HasSuffixFold(s.C(name), suffix))
}
}
// FieldContains returns a raw predicate to check if the field contains the given substring.
func FieldContains(name string, substr string) func(*Selector) {
return func(s *Selector) {
s.Where(Contains(s.C(name), substr))
}
}
// FieldContainsFold returns a raw predicate to check if the field contains the given substring with case-folding.
func FieldContainsFold(name string, substr string) func(*Selector) {
return func(s *Selector) {
s.Where(ContainsFold(s.C(name), substr))
}
}
// AndPredicates returns a new predicate for joining multiple generated predicates with AND between them.
func AndPredicates[P ~func(*Selector)](predicates ...P) func(*Selector) {
return func(s *Selector) {
s.CollectPredicates()
for _, p := range predicates {
p(s)
}
collected := s.CollectedPredicates()
s.UncollectedPredicates()
switch len(collected) {
case 0:
case 1:
s.Where(collected[0])
default:
s.Where(And(collected...))
}
}
}
// OrPredicates returns a new predicate for joining multiple generated predicates with OR between them.
func OrPredicates[P ~func(*Selector)](predicates ...P) func(*Selector) {
return func(s *Selector) {
s.CollectPredicates()
for _, p := range predicates {
p(s)
}
collected := s.CollectedPredicates()
s.UncollectedPredicates()
switch len(collected) {
case 0:
case 1:
s.Where(collected[0])
default:
s.Where(Or(collected...))
}
}
}
// NotPredicates wraps the generated predicates with NOT. For example, NOT(P), NOT((P1 AND P2)).
func NotPredicates[P ~func(*Selector)](predicates ...P) func(*Selector) {
return func(s *Selector) {
s.CollectPredicates()
for _, p := range predicates {
p(s)
}
collected := s.CollectedPredicates()
s.UncollectedPredicates()
switch len(collected) {
case 0:
case 1:
s.Where(Not(collected[0]))
default:
s.Where(Not(And(collected...)))
}
}
}
// ColumnCheck is a function that verifies whether the
// specified column exists within the given table.
type ColumnCheck func(table, column string) error
// NewColumnCheck returns a function that verifies whether the specified column exists
// within the given table. This function is utilized by the generated code to validate
// column names in ordering functions.
func NewColumnCheck(checks map[string]func(string) bool) ColumnCheck {
return func(table, column string) error {
check, ok := checks[table]
if !ok {
return fmt.Errorf("unknown table %q", table)
}
if !check(column) {
return fmt.Errorf("unknown column %q for table %q", column, table)
}
return nil
}
}
type (
// OrderFieldTerm represents an ordering by a field.
OrderFieldTerm struct {
OrderTermOptions
Field string // Field name.
}
// OrderExprTerm represents an ordering by an expression.
OrderExprTerm struct {
OrderTermOptions
Expr func(*Selector) Querier // Expression.
}
// OrderTerm represents an ordering by a term.
OrderTerm interface {
term()
}
// OrderTermOptions represents options for ordering by a term.
OrderTermOptions struct {
Desc bool // Whether to sort in descending order.
As string // Optional alias.
Selected bool // Whether the term should be selected.
NullsFirst bool // Whether to sort nulls first.
NullsLast bool // Whether to sort nulls last.
}
// OrderTermOption is an option for ordering by a term.
OrderTermOption func(*OrderTermOptions)
)
// OrderDesc returns an option to sort in descending order.
func OrderDesc() OrderTermOption {
return func(o *OrderTermOptions) {
o.Desc = true
}
}
// OrderAsc returns an option to sort in ascending order.
func OrderAsc() OrderTermOption {
return func(o *OrderTermOptions) {
o.Desc = false
}
}
// OrderAs returns an option to set the alias for the ordering.
func OrderAs(as string) OrderTermOption {
return func(o *OrderTermOptions) {
o.As = as
}
}
// OrderSelected returns an option to select the ordering term.
func OrderSelected() OrderTermOption {
return func(o *OrderTermOptions) {
o.Selected = true
}
}
// OrderSelectAs returns an option to set and select the alias for the ordering.
func OrderSelectAs(as string) OrderTermOption {
return func(o *OrderTermOptions) {
o.As = as
o.Selected = true
}
}
// OrderNullsFirst returns an option to sort nulls first.
func OrderNullsFirst() OrderTermOption {
return func(o *OrderTermOptions) {
o.NullsFirst = true
}
}
// OrderNullsLast returns an option to sort nulls last.
func OrderNullsLast() OrderTermOption {
return func(o *OrderTermOptions) {
o.NullsLast = true
}
}
// NewOrderTermOptions returns a new OrderTermOptions from the given options.
func NewOrderTermOptions(opts ...OrderTermOption) *OrderTermOptions {
o := &OrderTermOptions{}
for _, opt := range opts {
opt(o)
}
return o
}
// OrderByField returns an ordering by the given field.
func OrderByField(field string, opts ...OrderTermOption) *OrderFieldTerm {
return &OrderFieldTerm{Field: field, OrderTermOptions: *NewOrderTermOptions(opts...)}
}
// OrderBySum returns an ordering by the sum of the given field.
func OrderBySum(field string, opts ...OrderTermOption) *OrderExprTerm {
return orderByAgg("SUM", field, opts...)
}
// OrderByCount returns an ordering by the count of the given field.
func OrderByCount(field string, opts ...OrderTermOption) *OrderExprTerm {
return orderByAgg("COUNT", field, opts...)
}
// orderByAgg returns an ordering by the aggregation of the given field.
func orderByAgg(fn, field string, opts ...OrderTermOption) *OrderExprTerm {
return &OrderExprTerm{
OrderTermOptions: *NewOrderTermOptions(
append(
// Default alias is "<func>_<field>".
[]OrderTermOption{OrderAs(fmt.Sprintf("%s_%s", strings.ToLower(fn), field))},
opts...,
)...,
),
Expr: func(s *Selector) Querier {
var c string
switch {
case field == "*", isFunc(field):
c = field
default:
c = s.C(field)
}
return Raw(fmt.Sprintf("%s(%s)", fn, c))
},
}
}
// OrderByRand returns a term to natively order by a random value.
func OrderByRand() func(*Selector) {
return func(s *Selector) {
s.OrderExprFunc(func(b *Builder) {
switch s.Dialect() {
case dialect.MySQL:
b.WriteString("RAND()")
default:
b.WriteString("RANDOM()")
}
})
}
}
// ToFunc returns a function that sets the ordering on the given selector.
// This is used by the generated code.
func (f *OrderFieldTerm) ToFunc() func(*Selector) {
return func(s *Selector) {
s.OrderExprFunc(func(b *Builder) {
b.WriteString(s.C(f.Field))
if f.Desc {
b.WriteString(" DESC")
}
if f.NullsFirst {
b.WriteString(" NULLS FIRST")
} else if f.NullsLast {
b.WriteString(" NULLS LAST")
}
})
}
}
func (OrderFieldTerm) term() {}
func (OrderExprTerm) term() {}

471
dialect/sql/sql_test.go Normal file
View File

@@ -0,0 +1,471 @@
// 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 sql
import (
"testing"
"entgo.io/ent/dialect"
"github.com/stretchr/testify/require"
)
func TestFieldIsNull(t *testing.T) {
p := FieldIsNull("name")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` IS NULL", query)
require.Empty(t, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" IS NULL`, query)
require.Empty(t, args)
})
}
func TestFieldNotNull(t *testing.T) {
p := FieldNotNull("name")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` IS NOT NULL", query)
require.Empty(t, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" IS NOT NULL`, query)
require.Empty(t, args)
})
}
func TestFieldEQ(t *testing.T) {
p := FieldEQ("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` = ?", query)
require.Equal(t, []any{"a8m"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" = $1`, query)
require.Equal(t, []any{"a8m"}, args)
})
}
func TestFieldsEQ(t *testing.T) {
p := FieldsEQ("create_time", "update_time")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`create_time` = `users`.`update_time`", query)
require.Empty(t, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."create_time" = "users"."update_time"`, query)
require.Empty(t, args)
})
}
func TestFieldsNEQ(t *testing.T) {
p := FieldsNEQ("create_time", "update_time")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`create_time` <> `users`.`update_time`", query)
require.Empty(t, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."create_time" <> "users"."update_time"`, query)
require.Empty(t, args)
})
}
func TestFieldNEQ(t *testing.T) {
p := FieldNEQ("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` <> ?", query)
require.Equal(t, []any{"a8m"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" <> $1`, query)
require.Equal(t, []any{"a8m"}, args)
})
}
func TestFieldGT(t *testing.T) {
p := FieldGT("stars", 1000)
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`stars` > ?", query)
require.Equal(t, []any{1000}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."stars" > $1`, query)
require.Equal(t, []any{1000}, args)
})
}
func TestFieldsGT(t *testing.T) {
p := FieldsGT("a", "b")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`a` > `users`.`b`", query)
require.Empty(t, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."a" > "users"."b"`, query)
require.Empty(t, args)
})
}
func TestFieldGTE(t *testing.T) {
p := FieldGTE("stars", 1000)
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`stars` >= ?", query)
require.Equal(t, []any{1000}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."stars" >= $1`, query)
require.Equal(t, []any{1000}, args)
})
}
func TestFieldsGTE(t *testing.T) {
p := FieldsGTE("a", "b")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`a` >= `users`.`b`", query)
require.Empty(t, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."a" >= "users"."b"`, query)
require.Empty(t, args)
})
}
func TestFieldLT(t *testing.T) {
p := FieldLT("stars", 1000)
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`stars` < ?", query)
require.Equal(t, []any{1000}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."stars" < $1`, query)
require.Equal(t, []any{1000}, args)
})
}
func TestFieldsLT(t *testing.T) {
p := FieldsLT("a", "b")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`a` < `users`.`b`", query)
require.Empty(t, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."a" < "users"."b"`, query)
require.Empty(t, args)
})
}
func TestFieldLTE(t *testing.T) {
p := FieldLTE("stars", 1000)
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`stars` <= ?", query)
require.Equal(t, []any{1000}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."stars" <= $1`, query)
require.Equal(t, []any{1000}, args)
})
}
func TestFieldsLTE(t *testing.T) {
p := FieldsLTE("a", "b")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`a` <= `users`.`b`", query)
require.Empty(t, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."a" <= "users"."b"`, query)
require.Empty(t, args)
})
}
func TestFieldIn(t *testing.T) {
p := FieldIn("name", "a8m", "foo", "bar")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` IN (?, ?, ?)", query)
require.Equal(t, []any{"a8m", "foo", "bar"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" IN ($1, $2, $3)`, query)
require.Equal(t, []any{"a8m", "foo", "bar"}, args)
})
}
func TestFieldNotIn(t *testing.T) {
p := FieldNotIn("id", 1, 2, 3)
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`id` NOT IN (?, ?, ?)", query)
require.Equal(t, []any{1, 2, 3}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."id" NOT IN ($1, $2, $3)`, query)
require.Equal(t, []any{1, 2, 3}, args)
})
}
func TestFieldEqualFold(t *testing.T) {
p := FieldEqualFold("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci = ?", query)
require.Equal(t, []any{"a8m"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" ILIKE $1`, query)
require.Equal(t, []any{"a8m"}, args)
})
}
func TestFieldHasPrefix(t *testing.T) {
p := FieldHasPrefix("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` LIKE ?", query)
require.Equal(t, []any{"a8m%"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" LIKE $1`, query)
require.Equal(t, []any{"a8m%"}, args)
})
}
func TestFieldHasPrefixFold(t *testing.T) {
p := FieldHasPrefixFold("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci LIKE ?", query)
require.Equal(t, []any{"a8m%"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" ILIKE $1`, query)
require.Equal(t, []any{"a8m%"}, args)
})
}
func TestFieldHasSuffix(t *testing.T) {
p := FieldHasSuffix("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` LIKE ?", query)
require.Equal(t, []any{"%a8m"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" LIKE $1`, query)
require.Equal(t, []any{"%a8m"}, args)
})
}
func TestFieldHasSuffixFold(t *testing.T) {
p := FieldHasSuffixFold("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci LIKE ?", query)
require.Equal(t, []any{"%a8m"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" ILIKE $1`, query)
require.Equal(t, []any{"%a8m"}, args)
})
}
func TestFieldContains(t *testing.T) {
p := FieldContains("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` LIKE ?", query)
require.Equal(t, []any{"%a8m%"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" LIKE $1`, query)
require.Equal(t, []any{"%a8m%"}, args)
})
}
func TestFieldContainsFold(t *testing.T) {
p := FieldContainsFold("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci LIKE ?", query)
require.Equal(t, []any{"%a8m%"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" ILIKE $1`, query)
require.Equal(t, []any{"%a8m%"}, args)
})
}
func TestAndPredicates(t *testing.T) {
s := Select("*").From(Table("users")).Where(EQ("name", "a8m"))
p := AndPredicates(
FieldEQ("a", "foo"),
FieldEQ("b", 1),
func(s *Selector) {
petT := Table("pets").As("p")
s.Join(petT).On(petT.C("owner_id"), s.C("id"))
},
)
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` JOIN `pets` AS `p` ON `p`.`owner_id` = `users`.`id` WHERE `name` = ? AND (`users`.`a` = ? AND `users`.`b` = ?)", query)
require.Equal(t, []any{"a8m", "foo", 1}, args)
}
func TestOrPredicates(t *testing.T) {
s := Select("*").From(Table("users")).Where(EQ("name", "a8m"))
p := OrPredicates(
AndPredicates(
FieldEQ("a", "foo"),
FieldEQ("b", 1),
),
func(s *Selector) {
petT := Table("pets").As("p")
s.Join(petT).On(petT.C("owner_id"), s.C("id"))
s.Where(EQ(petT.C("name"), "c"))
},
)
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` JOIN `pets` AS `p` ON `p`.`owner_id` = `users`.`id` WHERE `name` = ? AND ((`users`.`a` = ? AND `users`.`b` = ?) OR `p`.`name` = ?)", query)
require.Equal(t, []any{"a8m", "foo", 1, "c"}, args)
}
func TestNotPredicates(t *testing.T) {
s := Select("*").From(Table("users")).Where(EQ("name", "a8m"))
NotPredicates(FieldEQ("a", "a"), FieldEQ("b", "b"))(s)
NotPredicates(FieldEQ("c", "c"))(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE (`name` = ? AND (NOT (`users`.`a` = ? AND `users`.`b` = ?))) AND (NOT (`users`.`c` = ?))", query)
require.Equal(t, []any{"a8m", "a", "b", "c"}, args)
}

View 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()
}

View 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())
}

View 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
}
}

View 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)
})
}
}

View 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
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,222 @@
// 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 sqljson
import (
"fmt"
"reflect"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
)
type sqlite struct{}
// Append implements the driver.Append method.
func (d *sqlite) Append(u *sql.UpdateBuilder, column string, elems []any, opts ...Option) {
setCase(u, column, when{
Cond: func(b *sql.Builder) {
typ := func(b *sql.Builder) *sql.Builder {
return b.WriteString("JSON_TYPE").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
identPath(column, opts...).mysqlPath(b)
})
}
typ(b).WriteOp(sql.OpIsNull)
b.WriteString(" OR ")
typ(b).WriteOp(sql.OpEQ).WriteString("'null'")
},
Then: func(b *sql.Builder) {
if len(opts) > 0 {
b.WriteString("JSON_SET").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
identPath(column, opts...).mysqlPath(b)
b.Comma().Argf("JSON(?)", marshalArg(elems))
})
} else {
b.Arg(marshalArg(elems))
}
},
Else: func(b *sql.Builder) {
b.WriteString("JSON_INSERT").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
// If no path was provided the top-level value is
// a JSON array. i.e. JSON_INSERT(c, '$[#]', ?).
path := func(b *sql.Builder) { b.WriteString("'$[#]'") }
if len(opts) > 0 {
p := identPath(column, opts...)
p.Path = append(p.Path, "[#]")
path = p.mysqlPath
}
for i, e := range elems {
if i > 0 {
b.Comma()
}
path(b)
b.Comma()
d.appendArg(b, e)
}
})
},
})
}
func (d *sqlite) appendArg(b *sql.Builder, v any) {
switch {
case !isPrimitive(v):
b.Argf("JSON(?)", marshalArg(v))
default:
b.Arg(v)
}
}
type mysql struct{}
// Append implements the driver.Append method.
func (d *mysql) Append(u *sql.UpdateBuilder, column string, elems []any, opts ...Option) {
setCase(u, column, when{
Cond: func(b *sql.Builder) {
typ := func(b *sql.Builder) *sql.Builder {
b.WriteString("JSON_TYPE(JSON_EXTRACT(")
b.Ident(column).Comma()
identPath(column, opts...).mysqlPath(b)
return b.WriteString("))")
}
typ(b).WriteOp(sql.OpIsNull)
b.WriteString(" OR ")
typ(b).WriteOp(sql.OpEQ).WriteString("'NULL'")
},
Then: func(b *sql.Builder) {
if len(opts) > 0 {
b.WriteString("JSON_SET").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
identPath(column, opts...).mysqlPath(b)
b.Comma().WriteString("JSON_ARRAY(").Args(d.marshalArgs(elems)...).WriteByte(')')
})
} else {
b.WriteString("JSON_ARRAY(").Args(d.marshalArgs(elems)...).WriteByte(')')
}
},
Else: func(b *sql.Builder) {
b.WriteString("JSON_ARRAY_APPEND").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
for i, e := range elems {
if i > 0 {
b.Comma()
}
identPath(column, opts...).mysqlPath(b)
b.Comma()
d.appendArg(b, e)
}
})
},
})
}
func (d *mysql) marshalArgs(args []any) []any {
vs := make([]any, len(args))
for i, v := range args {
if !isPrimitive(v) {
v = marshalArg(v)
}
vs[i] = v
}
return vs
}
func (d *mysql) appendArg(b *sql.Builder, v any) {
switch {
case !isPrimitive(v):
b.Argf("CAST(? AS JSON)", marshalArg(v))
default:
b.Arg(v)
}
}
type postgres struct{}
// Append implements the driver.Append method.
func (*postgres) Append(u *sql.UpdateBuilder, column string, elems []any, opts ...Option) {
setCase(u, column, when{
Cond: func(b *sql.Builder) {
valuePath(b, column, append(opts, Cast("jsonb"))...)
b.WriteOp(sql.OpIsNull)
b.WriteString(" OR ")
valuePath(b, column, append(opts, Cast("jsonb"))...)
b.WriteOp(sql.OpEQ).WriteString("'null'::jsonb")
},
Then: func(b *sql.Builder) {
if len(opts) > 0 {
b.WriteString("jsonb_set").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
identPath(column, opts...).pgArrayPath(b)
b.Comma().Arg(marshalArg(elems))
b.Comma().WriteString("true")
})
} else {
b.Arg(marshalArg(elems))
}
},
Else: func(b *sql.Builder) {
if len(opts) > 0 {
b.WriteString("jsonb_set").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
identPath(column, opts...).pgArrayPath(b)
b.Comma()
path := identPath(column, opts...)
path.value(b)
b.WriteString(" || ").Arg(marshalArg(elems))
b.Comma().WriteString("true")
})
} else {
b.Ident(column).WriteString(" || ").Arg(marshalArg(elems))
}
},
})
}
// driver groups all dialect-specific methods.
type driver interface {
Append(u *sql.UpdateBuilder, column string, elems []any, opts ...Option)
}
func newDriver(name string) (driver, error) {
switch name {
case dialect.SQLite:
return (*sqlite)(nil), nil
case dialect.MySQL:
return (*mysql)(nil), nil
case dialect.Postgres:
return (*postgres)(nil), nil
default:
return nil, fmt.Errorf("sqljson: unknown driver %q", name)
}
}
type when struct{ Cond, Then, Else func(*sql.Builder) }
// setCase sets the column value using the "CASE WHEN" statement.
// The x defines the condition/predicate, t is the true (if) case,
// and 'f' defines the false (else).
func setCase(u *sql.UpdateBuilder, column string, w when) {
u.Set(column, sql.ExprFunc(func(b *sql.Builder) {
b.WriteString("CASE WHEN ").Wrap(func(b *sql.Builder) {
w.Cond(b)
})
b.WriteString(" THEN ")
w.Then(b)
b.WriteString(" ELSE ")
w.Else(b)
b.WriteString(" END")
}))
}
func isPrimitive(v any) bool {
switch reflect.TypeOf(v).Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.Struct, reflect.Ptr, reflect.Interface:
return false
}
return true
}

View File

@@ -0,0 +1,727 @@
// 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 sqljson
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"unicode"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
)
// HasKey return a predicate for checking that a JSON key
// exists and not NULL.
//
// sqljson.HasKey("column", sql.DotPath("a.b[2].c"))
func HasKey(column string, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
switch b.Dialect() {
case dialect.SQLite:
// JSON_TYPE returns NULL in case the path selects an element
// that does not exist. See: https://sqlite.org/json1.html#jtype.
path := identPath(column, opts...)
path.mysqlFunc("JSON_TYPE", b)
b.WriteOp(sql.OpNotNull)
default:
valuePath(b, column, opts...)
b.WriteOp(sql.OpNotNull)
}
})
}
// ValueIsNull return a predicate for checking that a JSON value
// (returned by the path) is a null literal (JSON "null").
//
// In order to check if the column is NULL (database NULL), or if
// the JSON key exists, use sql.IsNull or sqljson.HasKey.
//
// sqljson.ValueIsNull("a", sqljson.Path("b"))
func ValueIsNull(column string, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
switch b.Dialect() {
case dialect.MySQL:
path := identPath(column, opts...)
b.WriteString("JSON_CONTAINS").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
b.WriteString("'null'").Comma()
path.mysqlPath(b)
})
case dialect.Postgres:
valuePath(b, column, append(opts, Cast("jsonb"))...)
b.WriteOp(sql.OpEQ).WriteString("'null'::jsonb")
case dialect.SQLite:
path := identPath(column, opts...)
path.mysqlFunc("JSON_TYPE", b)
b.WriteOp(sql.OpEQ).WriteString("'null'")
}
})
}
// ValueIsNotNull return a predicate for checking that a JSON value
// (returned by the path) is not null literal (JSON "null").
//
// sqljson.ValueIsNotNull("a", sqljson.Path("b"))
func ValueIsNotNull(column string, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
switch b.Dialect() {
case dialect.Postgres:
valuePath(b, column, append(opts, Cast("jsonb"))...)
b.WriteOp(sql.OpNEQ).WriteString("'null'::jsonb")
case dialect.SQLite:
path := identPath(column, opts...)
path.mysqlFunc("JSON_TYPE", b)
b.WriteOp(sql.OpNEQ).WriteString("'null'")
case dialect.MySQL:
path := identPath(column, opts...)
b.WriteString("NOT(JSON_CONTAINS").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
b.WriteString("'null'").Comma()
path.mysqlPath(b)
}).WriteString(")")
}
})
}
// ValueEQ return a predicate for checking that a JSON value
// (returned by the path) is equal to the given argument.
//
// sqljson.ValueEQ("a", 1, sqljson.Path("b"))
func ValueEQ(column string, arg any, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = normalizePG(b, arg, opts)
valuePath(b, column, opts...)
b.WriteOp(sql.OpEQ)
// Inline boolean values, as some drivers (e.g., MySQL) encode them as 0/1.
if v, ok := arg.(bool); ok {
b.WriteString(strconv.FormatBool(v))
} else {
b.Arg(arg)
}
})
}
// ValueNEQ return a predicate for checking that a JSON value
// (returned by the path) is not equal to the given argument.
//
// sqljson.ValueNEQ("a", 1, sqljson.Path("b"))
func ValueNEQ(column string, arg any, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = normalizePG(b, arg, opts)
valuePath(b, column, opts...)
b.WriteOp(sql.OpNEQ).Arg(arg)
})
}
// ValueGT return a predicate for checking that a JSON value
// (returned by the path) is greater than the given argument.
//
// sqljson.ValueGT("a", 1, sqljson.Path("b"))
func ValueGT(column string, arg any, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = normalizePG(b, arg, opts)
valuePath(b, column, opts...)
b.WriteOp(sql.OpGT).Arg(arg)
})
}
// ValueGTE return a predicate for checking that a JSON value
// (returned by the path) is greater than or equal to the given
// argument.
//
// sqljson.ValueGTE("a", 1, sqljson.Path("b"))
func ValueGTE(column string, arg any, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = normalizePG(b, arg, opts)
valuePath(b, column, opts...)
b.WriteOp(sql.OpGTE).Arg(arg)
})
}
// ValueLT return a predicate for checking that a JSON value
// (returned by the path) is less than the given argument.
//
// sqljson.ValueLT("a", 1, sqljson.Path("b"))
func ValueLT(column string, arg any, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = normalizePG(b, arg, opts)
valuePath(b, column, opts...)
b.WriteOp(sql.OpLT).Arg(arg)
})
}
// ValueLTE return a predicate for checking that a JSON value
// (returned by the path) is less than or equal to the given
// argument.
//
// sqljson.ValueLTE("a", 1, sqljson.Path("b"))
func ValueLTE(column string, arg any, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = normalizePG(b, arg, opts)
valuePath(b, column, opts...)
b.WriteOp(sql.OpLTE).Arg(arg)
})
}
// ValueContains return a predicate for checking that a JSON
// value (returned by the path) contains the given argument.
//
// sqljson.ValueContains("a", 1, sqljson.Path("b"))
func ValueContains(column string, arg any, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
path := identPath(column, opts...)
switch b.Dialect() {
case dialect.MySQL:
b.WriteString("JSON_CONTAINS").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
b.Arg(marshalArg(arg)).Comma()
path.mysqlPath(b)
})
b.WriteOp(sql.OpEQ).Arg(1)
case dialect.SQLite:
b.WriteString("EXISTS").Wrap(func(b *sql.Builder) {
b.WriteString("SELECT * FROM JSON_EACH").Wrap(func(b *sql.Builder) {
b.Ident(column).Comma()
path.mysqlPath(b)
})
b.WriteString(" WHERE ").Ident("value").WriteOp(sql.OpEQ).Arg(arg)
})
case dialect.Postgres:
opts = normalizePG(b, arg, opts)
path.Cast = "jsonb"
path.value(b)
b.WriteString(" @> ").Arg(marshalArg(arg))
}
})
}
// StringHasPrefix return a predicate for checking that a JSON string value
// (returned by the path) has the given substring as prefix
func StringHasPrefix(column string, prefix string, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = append([]Option{Unquote(true)}, opts...)
valuePath(b, column, opts...)
b.Join(sql.HasPrefix("", prefix))
})
}
// StringHasSuffix return a predicate for checking that a JSON string value
// (returned by the path) has the given substring as suffix
func StringHasSuffix(column string, suffix string, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = append([]Option{Unquote(true)}, opts...)
valuePath(b, column, opts...)
b.Join(sql.HasSuffix("", suffix))
})
}
// StringContains return a predicate for checking that a JSON string value
// (returned by the path) contains the given substring
func StringContains(column string, sub string, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
opts = append([]Option{Unquote(true)}, opts...)
valuePath(b, column, opts...)
b.Join(sql.Contains("", sub))
})
}
// ValueIn return a predicate for checking that a JSON value
// (returned by the path) is IN the given arguments.
//
// sqljson.ValueIn("a", []any{1, 2, 3}, sqljson.Path("b"))
func ValueIn(column string, args []any, opts ...Option) *sql.Predicate {
return valueInOp(column, args, opts, sql.OpIn)
}
// ValueNotIn return a predicate for checking that a JSON value
// (returned by the path) is NOT IN the given arguments.
//
// sqljson.ValueNotIn("a", []any{1, 2, 3}, sqljson.Path("b"))
func ValueNotIn(column string, args []any, opts ...Option) *sql.Predicate {
if len(args) == 0 {
return sql.NotIn(column)
}
return valueInOp(column, args, opts, sql.OpNotIn)
}
func valueInOp(column string, args []any, opts []Option, op sql.Op) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
if allString(args) {
opts = append(opts, Unquote(true))
}
if len(args) > 0 {
opts = normalizePG(b, args[0], opts)
}
valuePath(b, column, opts...)
b.WriteOp(op)
b.Wrap(func(b *sql.Builder) {
if s, ok := args[0].(*sql.Selector); ok {
b.Join(s)
} else {
b.Args(args...)
}
})
})
}
// LenEQ return a predicate for checking that an array length
// of a JSON (returned by the path) is equal to the given argument.
//
// sqljson.LenEQ("a", 1, sqljson.Path("b"))
func LenEQ(column string, size int, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
lenPath(b, column, opts...)
b.WriteOp(sql.OpEQ).Arg(size)
})
}
// LenNEQ return a predicate for checking that an array length
// of a JSON (returned by the path) is not equal to the given argument.
//
// sqljson.LenEQ("a", 1, sqljson.Path("b"))
func LenNEQ(column string, size int, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
lenPath(b, column, opts...)
b.WriteOp(sql.OpNEQ).Arg(size)
})
}
// LenGT return a predicate for checking that an array length
// of a JSON (returned by the path) is greater than the given
// argument.
//
// sqljson.LenGT("a", 1, sqljson.Path("b"))
func LenGT(column string, size int, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
lenPath(b, column, opts...)
b.WriteOp(sql.OpGT).Arg(size)
})
}
// LenGTE return a predicate for checking that an array length
// of a JSON (returned by the path) is greater than or equal to
// the given argument.
//
// sqljson.LenGTE("a", 1, sqljson.Path("b"))
func LenGTE(column string, size int, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
lenPath(b, column, opts...)
b.WriteOp(sql.OpGTE).Arg(size)
})
}
// LenLT return a predicate for checking that an array length
// of a JSON (returned by the path) is less than the given
// argument.
//
// sqljson.LenLT("a", 1, sqljson.Path("b"))
func LenLT(column string, size int, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
lenPath(b, column, opts...)
b.WriteOp(sql.OpLT).Arg(size)
})
}
// LenLTE return a predicate for checking that an array length
// of a JSON (returned by the path) is less than or equal to
// the given argument.
//
// sqljson.LenLTE("a", 1, sqljson.Path("b"))
func LenLTE(column string, size int, opts ...Option) *sql.Predicate {
return sql.P(func(b *sql.Builder) {
lenPath(b, column, opts...)
b.WriteOp(sql.OpLTE).Arg(size)
})
}
// LenPath returns an SQL expression for getting the length
// of a JSON value (returned by the path).
func LenPath(column string, opts ...Option) sql.Querier {
return sql.ExprFunc(func(b *sql.Builder) {
lenPath(b, column, opts...)
})
}
// OrderLen returns a custom predicate function (as defined in the doc),
// that sets the result order by the length of the given JSON value.
func OrderLen(column string, opts ...Option) func(*sql.Selector) {
return func(s *sql.Selector) {
s.OrderExpr(LenPath(column, opts...))
}
}
// OrderLenDesc returns a custom predicate function (as defined in the doc), that
// sets the result order by the length of the given JSON value, but in descending order.
func OrderLenDesc(column string, opts ...Option) func(*sql.Selector) {
return func(s *sql.Selector) {
s.OrderExpr(
sql.DescExpr(LenPath(column, opts...)),
)
}
}
// LenPath writes to the given SQL builder the JSON path for
// getting the length of a given JSON path.
//
// sqljson.LenPath(b, Path("a", "b", "[1]", "c"))
func lenPath(b *sql.Builder, column string, opts ...Option) {
path := identPath(column, opts...)
path.length(b)
}
// Append writes to the given SQL builder the SQL command for appending JSON values
// into the array, optionally defined as a key. Note, the generated SQL will use the
// Go semantics, the JSON column/key will be set to the given Array in case it is `null`
// or NULL. For example:
//
// Append(u, column, []string{"a", "b"})
// UPDATE "t" SET "c" = CASE
// WHEN ("c" IS NULL OR "c" = 'null'::jsonb)
// THEN $1 ELSE "c" || $2 END
//
// Append(u, column, []any{"a", 1}, sqljson.Path("a"))
// UPDATE "t" SET "c" = CASE
// WHEN (("c"->'a')::jsonb IS NULL OR ("c"->'a')::jsonb = 'null'::jsonb)
// THEN jsonb_set("c", '{a}', $1, true) ELSE jsonb_set("c", '{a}', "c"->'a' || $2, true) END
func Append[T any](u *sql.UpdateBuilder, column string, elems []T, opts ...Option) {
if len(elems) == 0 {
u.AddError(fmt.Errorf("sqljson: cannot append an empty array to column %q", column))
return
}
drv, err := newDriver(u.Dialect())
if err != nil {
u.AddError(err)
return
}
vs := make([]any, len(elems))
for i, e := range elems {
vs[i] = e
}
drv.Append(u, column, vs, opts...)
}
// Option allows for calling database JSON paths with functional options.
type Option func(*PathOptions)
// Path sets the path to the JSON value of a column.
//
// ValuePath(b, "column", Path("a", "b", "[1]", "c"))
func Path(path ...string) Option {
return func(p *PathOptions) {
p.Path = path
}
}
// DotPath is similar to Path, but accepts string with dot format.
//
// ValuePath(b, "column", DotPath("a.b.c"))
// ValuePath(b, "column", DotPath("a.b[2].c"))
//
// Note that DotPath is ignored if the input is invalid.
func DotPath(dotpath string) Option {
path, _ := ParsePath(dotpath)
return func(p *PathOptions) {
p.Path = path
}
}
// Unquote indicates that the result value should be unquoted.
//
// ValuePath(b, "column", Path("a", "b", "[1]", "c"), Unquote(true))
func Unquote(unquote bool) Option {
return func(p *PathOptions) {
p.Unquote = unquote
}
}
// Cast indicates that the result value should be cast to the given type.
//
// ValuePath(b, "column", Path("a", "b", "[1]", "c"), Cast("int"))
func Cast(typ string) Option {
return func(p *PathOptions) {
p.Cast = typ
}
}
// PathOptions holds the options for accessing a JSON value from an identifier.
type PathOptions struct {
Ident string
Path []string
Cast string
Unquote bool
}
// identPath creates a PathOptions for the given identifier.
func identPath(ident string, opts ...Option) *PathOptions {
path := &PathOptions{Ident: ident}
for i := range opts {
opts[i](path)
}
return path
}
func (p *PathOptions) Query() (string, []any) {
return p.Ident, nil
}
// ValuePath returns an SQL expression for getting the JSON
// value of a column with an optional path and cast options.
//
// sqljson.ValueEQ(
// column,
// sqljson.ValuePath(column, Path("a"), Cast("int")),
// sqljson.Path("a"),
// )
func ValuePath(column string, opts ...Option) sql.Querier {
return sql.ExprFunc(func(b *sql.Builder) {
valuePath(b, column, opts...)
})
}
// OrderValue returns a custom predicate function (as defined in the doc),
// that sets the result order by the given JSON value.
func OrderValue(column string, opts ...Option) func(*sql.Selector) {
return func(s *sql.Selector) {
s.OrderExpr(ValuePath(column, opts...))
}
}
// OrderValueDesc returns a custom predicate function (as defined in the doc),
// that sets the result order by the given JSON value, but in descending order.
func OrderValueDesc(column string, opts ...Option) func(*sql.Selector) {
return func(s *sql.Selector) {
s.OrderExpr(
sql.DescExpr(ValuePath(column, opts...)),
)
}
}
// valuePath writes to the given SQL builder the JSON path for
// getting the value of a given JSON path.
// Use sqljson.ValuePath for using a JSON value as an argument.
func valuePath(b *sql.Builder, column string, opts ...Option) {
path := identPath(column, opts...)
path.value(b)
}
// value writes the path for getting the JSON value.
func (p *PathOptions) value(b *sql.Builder) {
switch {
case len(p.Path) == 0:
b.Ident(p.Ident)
case b.Dialect() == dialect.Postgres:
if p.Cast != "" {
b.WriteByte('(')
defer b.WriteString(")::" + p.Cast)
}
p.pgTextPath(b)
default:
if p.Unquote && b.Dialect() == dialect.MySQL {
b.WriteString("JSON_UNQUOTE(")
defer b.WriteByte(')')
}
p.mysqlFunc("JSON_EXTRACT", b)
}
}
// value writes the path for getting the length of a JSON value.
func (p *PathOptions) length(b *sql.Builder) {
switch {
case b.Dialect() == dialect.Postgres:
b.WriteString("JSONB_ARRAY_LENGTH(")
p.pgTextPath(b)
b.WriteByte(')')
case b.Dialect() == dialect.MySQL:
p.mysqlFunc("JSON_LENGTH", b)
default:
p.mysqlFunc("JSON_ARRAY_LENGTH", b)
}
}
// mysqlFunc writes the JSON path in MySQL format for the
// given function. `JSON_EXTRACT("a", '$.b.c')`.
func (p *PathOptions) mysqlFunc(fn string, b *sql.Builder) {
b.WriteString(fn).WriteByte('(')
b.Ident(p.Ident).Comma()
p.mysqlPath(b)
b.WriteByte(')')
}
// mysqlPath writes the JSON path in MySQL (or SQLite) format.
func (p *PathOptions) mysqlPath(b *sql.Builder) {
b.WriteString(`'$`)
for _, p := range p.Path {
switch _, isIndex := isJSONIdx(p); {
case isIndex:
b.WriteString(p)
case p == "*" || isQuoted(p) || isIdentifier(p):
b.WriteString("." + p)
default:
b.WriteString(`."` + p + `"`)
}
}
b.WriteByte('\'')
}
// pgTextPath writes the JSON path in PostgreSQL text format: `"a"->'b'->>'c'`.
func (p *PathOptions) pgTextPath(b *sql.Builder) {
b.Ident(p.Ident)
for i, s := range p.Path {
b.WriteString("->")
if p.Unquote && i == len(p.Path)-1 {
b.WriteString(">")
}
if idx, ok := isJSONIdx(s); ok {
b.WriteString(idx)
} else {
b.WriteString("'" + s + "'")
}
}
}
// pgArrayPath writes the JSON path in PostgreSQL array text[] format: '{a,1,b}'.
func (p *PathOptions) pgArrayPath(b *sql.Builder) {
b.WriteString("'{")
for i, s := range p.Path {
if i > 0 {
b.Comma()
}
if idx, ok := isJSONIdx(s); ok {
s = idx
}
b.WriteString(s)
}
b.WriteString("}'")
}
// ParsePath parses the "dotpath" for the DotPath option.
//
// "a.b" => ["a", "b"]
// "a[1][2]" => ["a", "[1]", "[2]"]
// "a.\"b.c\" => ["a", "\"b.c\""]
func ParsePath(dotpath string) ([]string, error) {
var (
i, p int
path []string
)
for i < len(dotpath) {
switch r := dotpath[i]; {
case r == '"':
if i == len(dotpath)-1 {
return nil, fmt.Errorf("unexpected quote")
}
idx := strings.IndexRune(dotpath[i+1:], '"')
if idx == -1 || idx == 0 {
return nil, fmt.Errorf("unbalanced quote")
}
i += idx + 2
case r == '[':
if p != i {
path = append(path, dotpath[p:i])
}
p = i
if i == len(dotpath)-1 {
return nil, fmt.Errorf("unexpected bracket")
}
idx := strings.IndexRune(dotpath[i:], ']')
if idx == -1 || idx == 1 {
return nil, fmt.Errorf("unbalanced bracket")
}
if !isNumber(dotpath[i+1 : i+idx]) {
return nil, fmt.Errorf("invalid index %q", dotpath[i:i+idx+1])
}
i += idx + 1
case r == '.' || r == ']':
if p != i {
path = append(path, dotpath[p:i])
}
i++
p = i
default:
i++
}
}
if p != i {
path = append(path, dotpath[p:i])
}
return path, nil
}
// normalizePG adds cast option to the JSON path is the argument type is
// not string, in order to avoid "missing type casts" error in Postgres.
func normalizePG(b *sql.Builder, arg any, opts []Option) []Option {
if b.Dialect() != dialect.Postgres {
return opts
}
base := []Option{Unquote(true)}
switch arg.(type) {
case string:
case bool:
base = append(base, Cast("bool"))
case float32, float64:
base = append(base, Cast("float"))
case int8, int16, int32, int64, int, uint8, uint16, uint32, uint64:
base = append(base, Cast("int"))
}
return append(base, opts...)
}
func isIdentifier(name string) bool {
if name == "" {
return false
}
for i, c := range name {
if !unicode.IsLetter(c) && c != '_' && (i == 0 || !unicode.IsDigit(c)) {
return false
}
}
return true
}
func isQuoted(s string) bool {
if s == "" {
return false
}
return s[0] == '"' && s[len(s)-1] == '"'
}
// isJSONIdx reports whether the string represents a JSON index.
func isJSONIdx(s string) (string, bool) {
if len(s) > 2 && s[0] == '[' && s[len(s)-1] == ']' && (isNumber(s[1:len(s)-1]) || s[1] == '#' && isNumber(s[2:len(s)-1])) {
return s[1 : len(s)-1], true
}
return "", false
}
// isNumber reports whether the string is a number (category N).
func isNumber(s string) bool {
for _, r := range s {
if !unicode.IsNumber(r) {
return false
}
}
return true
}
// allString reports if the slice contains only strings.
func allString(v []any) bool {
for i := range v {
if _, ok := v[i].(string); !ok {
return false
}
}
return true
}
// marshalArg stringifies the given argument to a valid JSON document.
func marshalArg(arg any) any {
if buf, err := json.Marshal(arg); err == nil {
arg = string(buf)
}
return arg
}

View File

@@ -0,0 +1,535 @@
// 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 sqljson_test
import (
"strconv"
"testing"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqljson"
"github.com/stretchr/testify/require"
)
func TestWritePath(t *testing.T) {
tests := []struct {
input sql.Querier
wantQuery string
wantArgs []any
}{
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueEQ("a", 1, sqljson.Path("b", "c", "[1]", "d"), sqljson.Cast("int"))),
wantQuery: `SELECT * FROM "users" WHERE ("a"->'b'->'c'->1->>'d')::int = $1`,
wantArgs: []any{1},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueEQ("a", "a", sqljson.DotPath("b.c[1].d"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_EXTRACT(`a`, '$.b.c[1].d') = ?",
wantArgs: []any{"a"},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueEQ("a", true, sqljson.DotPath("b.c[1].d"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_EXTRACT(`a`, '$.b.c[1].d') = true",
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueEQ("a", "a", sqljson.DotPath("b.\"c[1]\".d[1][2].e"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_EXTRACT(`a`, '$.b.\"c[1]\".d[1][2].e') = ?",
wantArgs: []any{"a"},
},
{
input: sql.Select("*").
From(sql.Table("test")).
Where(sqljson.ValueEQ("j", sqljson.ValuePath("j", sqljson.DotPath("a.*.b")), sqljson.DotPath("a.*.c"))),
wantQuery: "SELECT * FROM `test` WHERE JSON_EXTRACT(`j`, '$.a.*.c') = JSON_EXTRACT(`j`, '$.a.*.b')",
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("test")).
Where(sqljson.ValueEQ("j", sqljson.ValuePath("j", sqljson.DotPath("a.*.b")), sqljson.DotPath("a.*.c"))),
wantQuery: `SELECT * FROM "test" WHERE "j"->'a'->'*'->>'c' = "j"->'a'->'*'->'b'`,
},
{
input: sql.Select("*").
From(sql.Table("test")).
Where(sqljson.HasKey("j", sqljson.DotPath("a.*.c"))),
wantQuery: "SELECT * FROM `test` WHERE JSON_EXTRACT(`j`, '$.a.*.c') IS NOT NULL",
},
{
input: sql.Select("*").
From(sql.Table("test")).
Where(sqljson.HasKey("j", sqljson.DotPath("a.*.c"))),
wantQuery: "SELECT * FROM `test` WHERE JSON_EXTRACT(`j`, '$.a.*.c') IS NOT NULL",
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("test")).
Where(sqljson.HasKey("j", sqljson.DotPath("attributes[1].body"))),
wantQuery: "SELECT * FROM `test` WHERE JSON_TYPE(`j`, '$.attributes[1].body') IS NOT NULL",
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("test")).
Where(sqljson.HasKey("j", sqljson.DotPath("a.*.c"))),
wantQuery: "SELECT * FROM `test` WHERE JSON_TYPE(`j`, '$.a.*.c') IS NOT NULL",
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("test")).
Where(
sql.And(
sql.GT("id", 100),
sqljson.HasKey("j", sqljson.DotPath("a.*.c")),
sql.EQ("active", true),
),
),
wantQuery: "SELECT * FROM `test` WHERE `id` > ? AND JSON_TYPE(`j`, '$.a.*.c') IS NOT NULL AND `active`",
wantArgs: []any{100},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("test")).
Where(sql.And(
sql.EQ("e", 10),
sqljson.ValueEQ("a", 1, sqljson.DotPath("b.c")),
)),
wantQuery: `SELECT * FROM "test" WHERE "e" = $1 AND ("a"->'b'->>'c')::int = $2`,
wantArgs: []any{10, 1},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueEQ("a", "a", sqljson.Path("b", "c", "[1]", "d"), sqljson.Unquote(true))),
wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, '$.b.c[1].d')) = ?",
wantArgs: []any{"a"},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueEQ("a", "a", sqljson.Path("b", "c", "[1]", "d"), sqljson.Unquote(true))),
wantQuery: `SELECT * FROM "users" WHERE "a"->'b'->'c'->1->>'d' = $1`,
wantArgs: []any{"a"},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueEQ("a", 1, sqljson.Path("b", "c", "[1]", "d"), sqljson.Cast("int"))),
wantQuery: `SELECT * FROM "users" WHERE ("a"->'b'->'c'->1->>'d')::int = $1`,
wantArgs: []any{1},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(
sql.Or(
sqljson.ValueNEQ("a", 1, sqljson.Path("b")),
sqljson.ValueGT("a", 1, sqljson.Path("c")),
sqljson.ValueGTE("a", 1.1, sqljson.Path("d")),
sqljson.ValueLT("a", 1, sqljson.Path("e")),
sqljson.ValueLTE("a", 1, sqljson.Path("f")),
),
),
wantQuery: `SELECT * FROM "users" WHERE ("a"->>'b')::int <> $1 OR ("a"->>'c')::int > $2 OR ("a"->>'d')::float >= $3 OR ("a"->>'e')::int < $4 OR ("a"->>'f')::int <= $5`,
wantArgs: []any{1, 1, 1.1, 1, 1},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.LenEQ("a", 1)),
wantQuery: `SELECT * FROM "users" WHERE JSONB_ARRAY_LENGTH("a") = $1`,
wantArgs: []any{1},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.LenEQ("a", 1)),
wantQuery: "SELECT * FROM `users` WHERE JSON_LENGTH(`a`, '$') = ?",
wantArgs: []any{1},
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("users")).
Where(sqljson.LenEQ("a", 1)),
wantQuery: "SELECT * FROM `users` WHERE JSON_ARRAY_LENGTH(`a`, '$') = ?",
wantArgs: []any{1},
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("users")).
Where(
sql.Or(
sqljson.LenGT("a", 1, sqljson.Path("b")),
sqljson.LenGTE("a", 1, sqljson.Path("c")),
sqljson.LenLT("a", 1, sqljson.Path("d")),
sqljson.LenLTE("a", 1, sqljson.Path("e")),
),
),
wantQuery: "SELECT * FROM `users` WHERE JSON_ARRAY_LENGTH(`a`, '$.b') > ? OR JSON_ARRAY_LENGTH(`a`, '$.c') >= ? OR JSON_ARRAY_LENGTH(`a`, '$.d') < ? OR JSON_ARRAY_LENGTH(`a`, '$.e') <= ?",
wantArgs: []any{1, 1, 1, 1},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueContains("tags", "foo")),
wantQuery: "SELECT * FROM `users` WHERE JSON_CONTAINS(`tags`, ?, '$') = ?",
wantArgs: []any{"\"foo\"", 1},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueContains("tags", 1, sqljson.Path("a"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_CONTAINS(`tags`, ?, '$.a') = ?",
wantArgs: []any{"1", 1},
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueContains("tags", "foo")),
wantQuery: "SELECT * FROM `users` WHERE EXISTS(SELECT * FROM JSON_EACH(`tags`, '$') WHERE `value` = ?)",
wantArgs: []any{"foo"},
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueContains("tags", 1, sqljson.Path("a"))),
wantQuery: "SELECT * FROM `users` WHERE EXISTS(SELECT * FROM JSON_EACH(`tags`, '$.a') WHERE `value` = ?)",
wantArgs: []any{1},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueContains("tags", "foo")),
wantQuery: "SELECT * FROM \"users\" WHERE \"tags\" @> $1",
wantArgs: []any{"\"foo\""},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueContains("tags", 1, sqljson.Path("a"))),
wantQuery: "SELECT * FROM \"users\" WHERE (\"tags\"->'a')::jsonb @> $1",
wantArgs: []any{"1"},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIsNull("c", sqljson.Path("a"))),
wantQuery: `SELECT * FROM "users" WHERE ("c"->'a')::jsonb = 'null'::jsonb`,
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIsNull("c", sqljson.Path("a"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_CONTAINS(`c`, 'null', '$.a')",
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIsNull("c", sqljson.Path("a"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_TYPE(`c`, '$.a') = 'null'",
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIsNotNull("c", sqljson.Path("a"))),
wantQuery: `SELECT * FROM "users" WHERE ("c"->'a')::jsonb <> 'null'::jsonb`,
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIsNotNull("c", sqljson.Path("a"))),
wantQuery: "SELECT * FROM `users` WHERE NOT(JSON_CONTAINS(`c`, 'null', '$.a'))",
},
{
input: sql.Dialect(dialect.SQLite).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIsNotNull("c", sqljson.Path("a"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_TYPE(`c`, '$.a') <> 'null'",
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.StringContains("a", "substr", sqljson.Path("b", "c", "[1]", "d"))),
wantQuery: `SELECT * FROM "users" WHERE "a"->'b'->'c'->1->>'d' LIKE $1`,
wantArgs: []any{"%substr%"},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(
sql.And(
sqljson.StringContains("a", "c", sqljson.Path("a")),
sqljson.StringContains("b", "d", sqljson.Path("b")),
),
),
wantQuery: `SELECT * FROM "users" WHERE "a"->>'a' LIKE $1 AND "b"->>'b' LIKE $2`,
wantArgs: []any{"%c%", "%d%"},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.StringContains("a", "substr", sqljson.Path("b", "c", "[1]", "d"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, '$.b.c[1].d')) LIKE ?",
wantArgs: []any{"%substr%"},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(
sql.And(
sqljson.StringContains("a", "c", sqljson.Path("a")),
sqljson.StringContains("b", "d", sqljson.Path("b")),
),
),
wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, '$.a')) LIKE ? AND JSON_UNQUOTE(JSON_EXTRACT(`b`, '$.b')) LIKE ?",
wantArgs: []any{"%c%", "%d%"},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.StringHasPrefix("a", "substr", sqljson.Path("b", "c", "[1]", "d"))),
wantQuery: `SELECT * FROM "users" WHERE "a"->'b'->'c'->1->>'d' LIKE $1`,
wantArgs: []any{"substr%"},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.StringHasPrefix("a", "substr", sqljson.Path("b", "c", "[1]", "d"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, '$.b.c[1].d')) LIKE ?",
wantArgs: []any{"substr%"},
},
{
input: sql.Dialect(dialect.Postgres).
Select("*").
From(sql.Table("users")).
Where(sqljson.StringHasSuffix("a", "substr", sqljson.Path("b", "c", "[1]", "d"))),
wantQuery: `SELECT * FROM "users" WHERE "a"->'b'->'c'->1->>'d' LIKE $1`,
wantArgs: []any{"%substr"},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.StringHasSuffix("a", "substr", sqljson.Path("b", "c", "[1]", "d"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, '$.b.c[1].d')) LIKE ?",
wantArgs: []any{"%substr"},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIn("a", []any{"a", "b"}, sqljson.Path("b"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`a`, '$.b')) IN (?, ?)",
wantArgs: []any{"a", "b"},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIn("a", []any{1, 2}, sqljson.Path("b"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_EXTRACT(`a`, '$.b') IN (?, ?)",
wantArgs: []any{1, 2},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIn("a", []any{1, "a"}, sqljson.Path("b"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_EXTRACT(`a`, '$.b') IN (?, ?)",
wantArgs: []any{1, "a"},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
Where(sqljson.ValueIn("a", []any{1, 2}, sqljson.Path("foo-bar", "3000"))),
wantQuery: "SELECT * FROM `users` WHERE JSON_EXTRACT(`a`, '$.\"foo-bar\".\"3000\"') IN (?, ?)",
wantArgs: []any{1, 2},
},
{
input: sql.Dialect(dialect.MySQL).
Select("*").
From(sql.Table("users")).
OrderExpr(
sqljson.LenPath("a", sqljson.Path("b")),
),
wantQuery: "SELECT * FROM `users` ORDER BY JSON_LENGTH(`a`, '$.b')",
},
}
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
query, args := tt.input.Query()
require.Equal(t, tt.wantQuery, query)
require.Equal(t, tt.wantArgs, args)
})
}
}
func TestParsePath(t *testing.T) {
tests := []struct {
input string
wantPath []string
wantErr bool
}{
{
input: "a.b.c",
wantPath: []string{"a", "b", "c"},
},
{
input: "a[1][2]",
wantPath: []string{"a", "[1]", "[2]"},
},
{
input: "a[1][2].b",
wantPath: []string{"a", "[1]", "[2]", "b"},
},
{
input: `a."b.c[0]"`,
wantPath: []string{"a", `"b.c[0]"`},
},
{
input: `a."b.c[0]".d`,
wantPath: []string{"a", `"b.c[0]"`, "d"},
},
{
input: `...`,
},
{
input: `.a.b.`,
wantPath: []string{"a", "b"},
},
{
input: `a."`,
wantErr: true,
},
{
input: `a[`,
wantErr: true,
},
{
input: `a[a]`,
wantErr: true,
},
}
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
path, err := sqljson.ParsePath(tt.input)
require.Equal(t, tt.wantPath, path)
require.Equal(t, tt.wantErr, err != nil)
})
}
}
func TestAppend(t *testing.T) {
tests := []struct {
input sql.Querier
wantQuery string
wantArgs []any
}{
{
input: func() sql.Querier {
u := sql.Dialect(dialect.Postgres).Update("t")
sqljson.Append(u, "c", []string{"a"})
return u
}(),
wantQuery: `UPDATE "t" SET "c" = CASE WHEN ("c" IS NULL OR "c" = 'null'::jsonb) THEN $1 ELSE "c" || $2 END`,
wantArgs: []any{`["a"]`, `["a"]`},
},
{
input: func() sql.Querier {
u := sql.Dialect(dialect.Postgres).Update("t")
sqljson.Append(u, "c", []string{"a"}, sqljson.Path("a"))
return u
}(),
wantQuery: `UPDATE "t" SET "c" = CASE WHEN (("c"->'a')::jsonb IS NULL OR ("c"->'a')::jsonb = 'null'::jsonb) THEN jsonb_set("c", '{a}', $1, true) ELSE jsonb_set("c", '{a}', "c"->'a' || $2, true) END`,
wantArgs: []any{`["a"]`, `["a"]`},
},
{
input: func() sql.Querier {
u := sql.Dialect(dialect.SQLite).Update("t")
sqljson.Append(u, "c", []string{"a"})
return u
}(),
wantQuery: "UPDATE `t` SET `c` = CASE WHEN (JSON_TYPE(`c`, '$') IS NULL OR JSON_TYPE(`c`, '$') = 'null') THEN ? ELSE JSON_INSERT(`c`, '$[#]', ?) END",
wantArgs: []any{`["a"]`, "a"},
},
{
input: func() sql.Querier {
u := sql.Dialect(dialect.SQLite).Update("t")
sqljson.Append(u, "c", []any{"a", struct{}{}}, sqljson.Path("a"))
return u
}(),
wantQuery: "UPDATE `t` SET `c` = CASE WHEN (JSON_TYPE(`c`, '$.a') IS NULL OR JSON_TYPE(`c`, '$.a') = 'null') THEN JSON_SET(`c`, '$.a', JSON(?)) ELSE JSON_INSERT(`c`, '$.a[#]', ?, '$.a[#]', JSON(?)) END",
wantArgs: []any{`["a",{}]`, "a", "{}"},
},
{
input: func() sql.Querier {
u := sql.Dialect(dialect.MySQL).Update("t")
sqljson.Append(u, "c", []string{"a"})
return u
}(),
wantQuery: "UPDATE `t` SET `c` = CASE WHEN (JSON_TYPE(JSON_EXTRACT(`c`, '$')) IS NULL OR JSON_TYPE(JSON_EXTRACT(`c`, '$')) = 'NULL') THEN JSON_ARRAY(?) ELSE JSON_ARRAY_APPEND(`c`, '$', ?) END",
wantArgs: []any{"a", "a"},
},
{
input: func() sql.Querier {
u := sql.Dialect(dialect.MySQL).Update("t")
sqljson.Append(u, "c", []string{"a"}, sqljson.Path("a"))
return u
}(),
wantQuery: "UPDATE `t` SET `c` = CASE WHEN (JSON_TYPE(JSON_EXTRACT(`c`, '$.a')) IS NULL OR JSON_TYPE(JSON_EXTRACT(`c`, '$.a')) = 'NULL') THEN JSON_SET(`c`, '$.a', JSON_ARRAY(?)) ELSE JSON_ARRAY_APPEND(`c`, '$.a', ?) END",
wantArgs: []any{"a", "a"},
},
}
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
query, args := tt.input.Query()
require.Equal(t, tt.wantQuery, query)
require.Equal(t, tt.wantArgs, args)
})
}
}