feat(bun): generate native Go array slices for PostgreSQL array columns
Bun's pgdialect scans/appends native slices directly, so array columns
(text[], integer[], uuid[], ...) always generate as plain []string,
[]int32, etc. with an explicit "array" bun tag, regardless of --types
(sqltypes/stdlib/baselib). The SqlXxxArray wrapper types are no longer
used for Bun array columns (gorm is unaffected and keeps using them).
Adds --array-nullable pointer_slice to represent nullable array columns
as *[]T instead of []T, so callers can distinguish SQL NULL (nil) from
'{}' (pointer to an empty slice). Verified end-to-end against a live
PostgreSQL instance for NULL/{}/populated arrays in every --types mode.
Closes #13
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2021 Vladimir Mihailenco. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"github.com/uptrace/bun/migrate/sqlschema"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
func (d *Dialect) NewMigrator(db *bun.DB, schemaName string) sqlschema.Migrator {
|
||||
return &migrator{db: db, schemaName: schemaName, BaseMigrator: sqlschema.NewBaseMigrator(db)}
|
||||
}
|
||||
|
||||
type migrator struct {
|
||||
*sqlschema.BaseMigrator
|
||||
|
||||
db *bun.DB
|
||||
schemaName string
|
||||
}
|
||||
|
||||
var _ sqlschema.Migrator = (*migrator)(nil)
|
||||
|
||||
func (m *migrator) AppendSQL(b []byte, operation any) (_ []byte, err error) {
|
||||
gen := m.db.QueryGen()
|
||||
|
||||
// Append ALTER TABLE statement to the enclosed query bytes []byte.
|
||||
appendAlterTable := func(query []byte, tableName string) []byte {
|
||||
query = append(query, "ALTER TABLE "...)
|
||||
query = m.appendFQN(gen, query, tableName)
|
||||
return append(query, " "...)
|
||||
}
|
||||
|
||||
switch change := operation.(type) {
|
||||
case *migrate.CreateTableOp:
|
||||
return m.AppendCreateTable(b, change.Model)
|
||||
case *migrate.DropTableOp:
|
||||
return m.AppendDropTable(b, m.schemaName, change.TableName)
|
||||
case *migrate.RenameTableOp:
|
||||
b, err = m.renameTable(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.RenameColumnOp:
|
||||
b, err = m.renameColumn(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.AddColumnOp:
|
||||
b, err = m.addColumn(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.DropColumnOp:
|
||||
b, err = m.dropColumn(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.AddPrimaryKeyOp:
|
||||
b, err = m.addPrimaryKey(gen, appendAlterTable(b, change.TableName), change.PrimaryKey)
|
||||
case *migrate.ChangePrimaryKeyOp:
|
||||
b, err = m.changePrimaryKey(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.DropPrimaryKeyOp:
|
||||
b, err = m.dropConstraint(gen, appendAlterTable(b, change.TableName), change.PrimaryKey.Name)
|
||||
case *migrate.AddUniqueConstraintOp:
|
||||
b, err = m.addUnique(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.DropUniqueConstraintOp:
|
||||
b, err = m.dropConstraint(gen, appendAlterTable(b, change.TableName), change.Unique.Name)
|
||||
case *migrate.ChangeColumnTypeOp:
|
||||
// If column changes to SERIAL, create sequence first.
|
||||
// https://gist.github.com/oleglomako/185df689706c5499612a0d54d3ffe856
|
||||
if !change.From.GetIsAutoIncrement() && change.To.GetIsAutoIncrement() {
|
||||
change.To, b, err = m.createDefaultSequence(gen, b, change)
|
||||
}
|
||||
b, err = m.changeColumnType(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.AddForeignKeyOp:
|
||||
b, err = m.addForeignKey(gen, appendAlterTable(b, change.TableName()), change)
|
||||
case *migrate.DropForeignKeyOp:
|
||||
b, err = m.dropConstraint(gen, appendAlterTable(b, change.TableName()), change.ConstraintName)
|
||||
default:
|
||||
return nil, fmt.Errorf("append sql: unknown operation %T", change)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("append sql: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) appendFQN(gen schema.QueryGen, b []byte, tableName string) []byte {
|
||||
return gen.AppendQuery(b, "?.?", bun.Ident(m.schemaName), bun.Ident(tableName))
|
||||
}
|
||||
|
||||
func (m *migrator) renameTable(gen schema.QueryGen, b []byte, rename *migrate.RenameTableOp) (_ []byte, err error) {
|
||||
b = append(b, "RENAME TO "...)
|
||||
b = gen.AppendName(b, rename.NewName)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) renameColumn(gen schema.QueryGen, b []byte, rename *migrate.RenameColumnOp) (_ []byte, err error) {
|
||||
b = append(b, "RENAME COLUMN "...)
|
||||
b = gen.AppendName(b, rename.OldName)
|
||||
|
||||
b = append(b, " TO "...)
|
||||
b = gen.AppendName(b, rename.NewName)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) addColumn(gen schema.QueryGen, b []byte, add *migrate.AddColumnOp) (_ []byte, err error) {
|
||||
b = append(b, "ADD COLUMN "...)
|
||||
b = gen.AppendName(b, add.ColumnName)
|
||||
b = append(b, " "...)
|
||||
|
||||
b, err = add.Column.AppendQuery(gen, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if add.Column.GetDefaultValue() != "" {
|
||||
b = append(b, " DEFAULT "...)
|
||||
b = append(b, add.Column.GetDefaultValue()...)
|
||||
b = append(b, " "...)
|
||||
}
|
||||
|
||||
if add.Column.GetIsIdentity() {
|
||||
b = appendGeneratedAsIdentity(b)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) dropColumn(gen schema.QueryGen, b []byte, drop *migrate.DropColumnOp) (_ []byte, err error) {
|
||||
b = append(b, "DROP COLUMN "...)
|
||||
b = gen.AppendName(b, drop.ColumnName)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) addPrimaryKey(gen schema.QueryGen, b []byte, pk sqlschema.PrimaryKey) (_ []byte, err error) {
|
||||
b = append(b, "ADD PRIMARY KEY ("...)
|
||||
b, _ = pk.Columns.AppendQuery(gen, b)
|
||||
b = append(b, ")"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) changePrimaryKey(gen schema.QueryGen, b []byte, change *migrate.ChangePrimaryKeyOp) (_ []byte, err error) {
|
||||
b, _ = m.dropConstraint(gen, b, change.Old.Name)
|
||||
b = append(b, ", "...)
|
||||
b, _ = m.addPrimaryKey(gen, b, change.New)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) addUnique(gen schema.QueryGen, b []byte, change *migrate.AddUniqueConstraintOp) (_ []byte, err error) {
|
||||
b = append(b, "ADD CONSTRAINT "...)
|
||||
if change.Unique.Name != "" {
|
||||
b = gen.AppendName(b, change.Unique.Name)
|
||||
} else {
|
||||
// Default naming scheme for unique constraints in Postgres is <table>_<column>_key
|
||||
b = gen.AppendName(b, fmt.Sprintf("%s_%s_key", change.TableName, change.Unique.Columns))
|
||||
}
|
||||
b = append(b, " UNIQUE ("...)
|
||||
b, _ = change.Unique.Columns.AppendQuery(gen, b)
|
||||
b = append(b, ")"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) dropConstraint(gen schema.QueryGen, b []byte, name string) (_ []byte, err error) {
|
||||
b = append(b, "DROP CONSTRAINT "...)
|
||||
b = gen.AppendName(b, name)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) addForeignKey(gen schema.QueryGen, b []byte, add *migrate.AddForeignKeyOp) (_ []byte, err error) {
|
||||
b = append(b, "ADD CONSTRAINT "...)
|
||||
|
||||
name := add.ConstraintName
|
||||
if name == "" {
|
||||
colRef := add.ForeignKey.From
|
||||
columns := strings.Join(colRef.Column.Split(), "_")
|
||||
name = fmt.Sprintf("%s_%s_fkey", colRef.TableName, columns)
|
||||
}
|
||||
b = gen.AppendName(b, name)
|
||||
|
||||
b = append(b, " FOREIGN KEY ("...)
|
||||
if b, err = add.ForeignKey.From.Column.AppendQuery(gen, b); err != nil {
|
||||
return b, err
|
||||
}
|
||||
b = append(b, ")"...)
|
||||
|
||||
b = append(b, " REFERENCES "...)
|
||||
b = m.appendFQN(gen, b, add.ForeignKey.To.TableName)
|
||||
|
||||
b = append(b, " ("...)
|
||||
if b, err = add.ForeignKey.To.Column.AppendQuery(gen, b); err != nil {
|
||||
return b, err
|
||||
}
|
||||
b = append(b, ")"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// createDefaultSequence creates a SEQUENCE to back a serial column.
|
||||
// Having a backing sequence is necessary to change column type to SERIAL.
|
||||
// The updated Column's default is set to "nextval" of the new sequence.
|
||||
func (m *migrator) createDefaultSequence(_ schema.QueryGen, b []byte, op *migrate.ChangeColumnTypeOp) (_ sqlschema.Column, _ []byte, err error) {
|
||||
var last int
|
||||
if err = m.db.NewSelect().Table(op.TableName).
|
||||
ColumnExpr("MAX(?)", op.Column).Scan(context.TODO(), &last); err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
seq := op.TableName + "_" + op.Column + "_seq"
|
||||
fqn := op.TableName + "." + op.Column
|
||||
|
||||
// A sequence that is OWNED BY a table will be dropped
|
||||
// if the table is dropped with CASCADE action.
|
||||
b = append(b, "CREATE SEQUENCE "...)
|
||||
b = append(b, seq...)
|
||||
b = append(b, " START WITH "...)
|
||||
b = append(b, fmt.Sprint(last+1)...) // start with next value
|
||||
b = append(b, " OWNED BY "...)
|
||||
b = append(b, fqn...)
|
||||
b = append(b, ";\n"...)
|
||||
|
||||
return &Column{
|
||||
Name: op.To.GetName(),
|
||||
SQLType: op.To.GetSQLType(),
|
||||
VarcharLen: op.To.GetVarcharLen(),
|
||||
DefaultValue: fmt.Sprintf("nextval('%s'::regclass)", seq),
|
||||
IsNullable: op.To.GetIsNullable(),
|
||||
IsAutoIncrement: op.To.GetIsAutoIncrement(),
|
||||
IsIdentity: op.To.GetIsIdentity(),
|
||||
}, b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) changeColumnType(gen schema.QueryGen, b []byte, colDef *migrate.ChangeColumnTypeOp) (_ []byte, err error) {
|
||||
// alterColumn never re-assigns err, so there is no need to check for err != nil after calling it
|
||||
var i int
|
||||
appendAlterColumn := func() {
|
||||
if i > 0 {
|
||||
b = append(b, ", "...)
|
||||
}
|
||||
b = append(b, "ALTER COLUMN "...)
|
||||
b = gen.AppendName(b, colDef.Column)
|
||||
i++
|
||||
}
|
||||
|
||||
got, want := colDef.From, colDef.To
|
||||
|
||||
inspector := m.db.Dialect().(sqlschema.InspectorDialect)
|
||||
if !inspector.CompareType(want, got) {
|
||||
appendAlterColumn()
|
||||
b = append(b, " SET DATA TYPE "...)
|
||||
if b, err = want.AppendQuery(gen, b); err != nil {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
|
||||
// Column must be declared NOT NULL before identity can be added.
|
||||
// Although PG can resolve the order of operations itself, we make this explicit in the query.
|
||||
if want.GetIsNullable() != got.GetIsNullable() {
|
||||
appendAlterColumn()
|
||||
if !want.GetIsNullable() {
|
||||
b = append(b, " SET NOT NULL"...)
|
||||
} else {
|
||||
b = append(b, " DROP NOT NULL"...)
|
||||
}
|
||||
}
|
||||
|
||||
if want.GetIsIdentity() != got.GetIsIdentity() {
|
||||
appendAlterColumn()
|
||||
if !want.GetIsIdentity() {
|
||||
b = append(b, " DROP IDENTITY"...)
|
||||
} else {
|
||||
b = append(b, " ADD"...)
|
||||
b = appendGeneratedAsIdentity(b)
|
||||
}
|
||||
}
|
||||
|
||||
if want.GetDefaultValue() != got.GetDefaultValue() {
|
||||
appendAlterColumn()
|
||||
if want.GetDefaultValue() == "" {
|
||||
b = append(b, " DROP DEFAULT"...)
|
||||
} else {
|
||||
b = append(b, " SET DEFAULT "...)
|
||||
b = append(b, want.GetDefaultValue()...)
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
driverValuerType = reflect.TypeFor[driver.Valuer]()
|
||||
|
||||
stringType = reflect.TypeFor[string]()
|
||||
sliceStringType = reflect.TypeFor[[]string]()
|
||||
|
||||
intType = reflect.TypeFor[int]()
|
||||
sliceIntType = reflect.TypeFor[[]int]()
|
||||
|
||||
int64Type = reflect.TypeFor[int64]()
|
||||
sliceInt64Type = reflect.TypeFor[[]int64]()
|
||||
|
||||
float64Type = reflect.TypeFor[float64]()
|
||||
sliceFloat64Type = reflect.TypeFor[[]float64]()
|
||||
|
||||
timeType = reflect.TypeFor[time.Time]()
|
||||
sliceTimeType = reflect.TypeFor[[]time.Time]()
|
||||
)
|
||||
|
||||
func appendTime(buf []byte, tm time.Time) []byte {
|
||||
return tm.UTC().AppendFormat(buf, "2006-01-02 15:04:05.999999-07:00")
|
||||
}
|
||||
|
||||
var mapStringStringType = reflect.TypeOf(map[string]string(nil))
|
||||
|
||||
func (d *Dialect) hstoreAppender(typ reflect.Type) schema.AppenderFunc {
|
||||
kind := typ.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if fn := d.hstoreAppender(typ.Elem()); fn != nil {
|
||||
return schema.PtrAppender(fn)
|
||||
}
|
||||
case reflect.Map:
|
||||
// ok:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if typ.Key() == stringType && typ.Elem() == stringType {
|
||||
return appendMapStringStringValue
|
||||
}
|
||||
|
||||
return func(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
err := fmt.Errorf("bun: Hstore(unsupported %s)", v.Type())
|
||||
return dialect.AppendError(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func appendMapStringString(b []byte, m map[string]string) []byte {
|
||||
if m == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
for key, value := range m {
|
||||
b = appendStringElem(b, key)
|
||||
b = append(b, '=', '>')
|
||||
b = appendStringElem(b, value)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(m) > 0 {
|
||||
b = b[:len(b)-1] // Strip trailing comma.
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendMapStringStringValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
m := v.Convert(mapStringStringType).Interface().(map[string]string)
|
||||
return appendMapStringString(b, m)
|
||||
}
|
||||
+608
@@ -0,0 +1,608 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/internal"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
type ArrayValue struct {
|
||||
v reflect.Value
|
||||
|
||||
append schema.AppenderFunc
|
||||
scan schema.ScannerFunc
|
||||
}
|
||||
|
||||
// Array accepts a slice and returns a wrapper for working with PostgreSQL
|
||||
// array data type.
|
||||
//
|
||||
// For struct fields you can use array tag:
|
||||
//
|
||||
// Emails []string `bun:",array"`
|
||||
func Array(vi any) *ArrayValue {
|
||||
v := reflect.ValueOf(vi)
|
||||
if !v.IsValid() {
|
||||
panic(fmt.Errorf("bun: Array(nil)"))
|
||||
}
|
||||
|
||||
return &ArrayValue{
|
||||
v: v,
|
||||
|
||||
append: pgDialect.arrayAppender(v.Type()),
|
||||
scan: arrayScanner(v.Type()),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ schema.QueryAppender = (*ArrayValue)(nil)
|
||||
_ sql.Scanner = (*ArrayValue)(nil)
|
||||
)
|
||||
|
||||
func (a *ArrayValue) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error) {
|
||||
if a.append == nil {
|
||||
panic(fmt.Errorf("bun: Array(unsupported %s)", a.v.Type()))
|
||||
}
|
||||
return a.append(gen, b, a.v), nil
|
||||
}
|
||||
|
||||
func (a *ArrayValue) Scan(src any) error {
|
||||
if a.scan == nil {
|
||||
return fmt.Errorf("bun: Array(unsupported %s)", a.v.Type())
|
||||
}
|
||||
if a.v.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("bun: Array(non-pointer %s)", a.v.Type())
|
||||
}
|
||||
return a.scan(a.v, src)
|
||||
}
|
||||
|
||||
func (a *ArrayValue) Value() any {
|
||||
if a.v.IsValid() {
|
||||
return a.v.Interface()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func (d *Dialect) arrayAppender(typ reflect.Type) schema.AppenderFunc {
|
||||
kind := typ.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if fn := d.arrayAppender(typ.Elem()); fn != nil {
|
||||
return schema.PtrAppender(fn)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
// continue below
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
elemType := typ.Elem()
|
||||
|
||||
if kind == reflect.Slice {
|
||||
switch elemType {
|
||||
case stringType:
|
||||
return appendStringSliceValue
|
||||
case intType:
|
||||
return appendIntSliceValue
|
||||
case int64Type:
|
||||
return appendInt64SliceValue
|
||||
case float64Type:
|
||||
return appendFloat64SliceValue
|
||||
case timeType:
|
||||
return appendTimeSliceValue
|
||||
}
|
||||
}
|
||||
|
||||
appendElem := d.arrayElemAppender(elemType)
|
||||
if appendElem == nil {
|
||||
panic(fmt.Errorf("pgdialect: %s is not supported", typ))
|
||||
}
|
||||
|
||||
return func(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
kind := v.Kind()
|
||||
switch kind {
|
||||
case reflect.Ptr, reflect.Slice:
|
||||
if v.IsNil() {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
}
|
||||
|
||||
if kind == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
b = append(b, "'{"...)
|
||||
|
||||
ln := v.Len()
|
||||
for i := 0; i < ln; i++ {
|
||||
elem := v.Index(i)
|
||||
if i > 0 {
|
||||
b = append(b, ',')
|
||||
}
|
||||
b = appendElem(gen, b, elem)
|
||||
}
|
||||
|
||||
b = append(b, "}'"...)
|
||||
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialect) arrayElemAppender(typ reflect.Type) schema.AppenderFunc {
|
||||
if typ.Implements(driverValuerType) {
|
||||
return arrayAppendDriverValue
|
||||
}
|
||||
if typ == timeType {
|
||||
return appendTimeElemValue
|
||||
}
|
||||
|
||||
switch typ.Kind() {
|
||||
case reflect.String:
|
||||
return appendStringElemValue
|
||||
case reflect.Slice:
|
||||
if typ.Elem().Kind() == reflect.Uint8 {
|
||||
return appendBytesElemValue
|
||||
}
|
||||
case reflect.Ptr:
|
||||
return schema.PtrAppender(d.arrayElemAppender(typ.Elem()))
|
||||
}
|
||||
return schema.Appender(d, typ)
|
||||
}
|
||||
|
||||
func appendTimeElemValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ts := v.Convert(timeType).Interface().(time.Time)
|
||||
|
||||
b = append(b, '"')
|
||||
b = appendTime(b, ts)
|
||||
return append(b, '"')
|
||||
}
|
||||
|
||||
func appendStringElemValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
return appendStringElem(b, v.String())
|
||||
}
|
||||
|
||||
func appendBytesElemValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
return appendBytesElem(b, v.Bytes())
|
||||
}
|
||||
|
||||
func arrayAppendDriverValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
iface, err := v.Interface().(driver.Valuer).Value()
|
||||
if err != nil {
|
||||
return dialect.AppendError(b, err)
|
||||
}
|
||||
return appendElem(b, iface)
|
||||
}
|
||||
|
||||
func appendStringSliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ss := v.Convert(sliceStringType).Interface().([]string)
|
||||
return appendStringSlice(b, ss)
|
||||
}
|
||||
|
||||
func appendStringSlice(b []byte, ss []string) []byte {
|
||||
if ss == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
b = append(b, '{')
|
||||
for _, s := range ss {
|
||||
b = appendStringElem(b, s)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(ss) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendIntSliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ints := v.Convert(sliceIntType).Interface().([]int)
|
||||
return appendIntSlice(b, ints)
|
||||
}
|
||||
|
||||
func appendIntSlice(b []byte, ints []int) []byte {
|
||||
if ints == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
b = append(b, '{')
|
||||
for _, n := range ints {
|
||||
b = strconv.AppendInt(b, int64(n), 10)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(ints) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendInt64SliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ints := v.Convert(sliceInt64Type).Interface().([]int64)
|
||||
return appendInt64Slice(b, ints)
|
||||
}
|
||||
|
||||
func appendInt64Slice(b []byte, ints []int64) []byte {
|
||||
if ints == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
b = append(b, '{')
|
||||
for _, n := range ints {
|
||||
b = strconv.AppendInt(b, n, 10)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(ints) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendFloat64SliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
floats := v.Convert(sliceFloat64Type).Interface().([]float64)
|
||||
return appendFloat64Slice(b, floats)
|
||||
}
|
||||
|
||||
func appendFloat64Slice(b []byte, floats []float64) []byte {
|
||||
if floats == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
b = append(b, '{')
|
||||
for _, n := range floats {
|
||||
b = arrayAppendFloat64(b, n)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(floats) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func arrayAppendFloat64(b []byte, num float64) []byte {
|
||||
switch {
|
||||
case math.IsNaN(num):
|
||||
return append(b, "NaN"...)
|
||||
case math.IsInf(num, 1):
|
||||
return append(b, "Infinity"...)
|
||||
case math.IsInf(num, -1):
|
||||
return append(b, "-Infinity"...)
|
||||
default:
|
||||
return strconv.AppendFloat(b, num, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
|
||||
func appendTimeSliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ts := v.Convert(sliceTimeType).Interface().([]time.Time)
|
||||
return appendTimeSlice(gen, b, ts)
|
||||
}
|
||||
|
||||
func appendTimeSlice(gen schema.QueryGen, b []byte, ts []time.Time) []byte {
|
||||
if ts == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
b = append(b, '\'')
|
||||
b = append(b, '{')
|
||||
for _, t := range ts {
|
||||
b = append(b, '"')
|
||||
b = appendTime(b, t)
|
||||
b = append(b, '"')
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(ts) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
b = append(b, '\'')
|
||||
return b
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func arrayScanner(typ reflect.Type) schema.ScannerFunc {
|
||||
kind := typ.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if fn := arrayScanner(typ.Elem()); fn != nil {
|
||||
return schema.PtrScanner(fn)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
// ok:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
elemType := typ.Elem()
|
||||
|
||||
if kind == reflect.Slice {
|
||||
switch elemType {
|
||||
case stringType:
|
||||
return scanStringSliceValue
|
||||
case intType:
|
||||
return scanIntSliceValue
|
||||
case int64Type:
|
||||
return scanInt64SliceValue
|
||||
case float64Type:
|
||||
return scanFloat64SliceValue
|
||||
}
|
||||
}
|
||||
|
||||
scanElem := schema.Scanner(elemType)
|
||||
return func(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
kind := dest.Kind()
|
||||
|
||||
if src == nil {
|
||||
if kind != reflect.Slice || !dest.IsNil() {
|
||||
dest.Set(reflect.Zero(dest.Type()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if kind == reflect.Slice {
|
||||
if dest.IsNil() {
|
||||
dest.Set(reflect.MakeSlice(dest.Type(), 0, 0))
|
||||
} else if dest.Len() > 0 {
|
||||
dest.Set(dest.Slice(0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := newArrayParser(b)
|
||||
nextValue := internal.MakeSliceNextElemFunc(dest)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
elemValue := nextValue()
|
||||
if err := scanElem(elemValue, elem); err != nil {
|
||||
return fmt.Errorf("scanElem failed: %w", err)
|
||||
}
|
||||
}
|
||||
return p.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func scanStringSliceValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
slice, err := decodeStringSlice(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(slice))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeStringSlice(src any) ([]string, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice := make([]string, 0)
|
||||
|
||||
p := newArrayParser(b)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
slice = append(slice, string(elem))
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func scanIntSliceValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
slice, err := decodeIntSlice(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(slice))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeIntSlice(src any) ([]int, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice := make([]int, 0)
|
||||
|
||||
p := newArrayParser(b)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
|
||||
if elem == nil {
|
||||
slice = append(slice, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(internal.String(elem))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice = append(slice, n)
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func scanInt64SliceValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
slice, err := decodeInt64Slice(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(slice))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeInt64Slice(src any) ([]int64, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice := make([]int64, 0)
|
||||
|
||||
p := newArrayParser(b)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
|
||||
if elem == nil {
|
||||
slice = append(slice, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.ParseInt(internal.String(elem), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice = append(slice, n)
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func scanFloat64SliceValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
slice, err := scanFloat64Slice(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(slice))
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanFloat64Slice(src any) ([]float64, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice := make([]float64, 0)
|
||||
|
||||
p := newArrayParser(b)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
|
||||
if elem == nil {
|
||||
slice = append(slice, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.ParseFloat(internal.String(elem), 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice = append(slice, n)
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func toBytes(src any) ([]byte, error) {
|
||||
switch src := src.(type) {
|
||||
case string:
|
||||
return internal.Bytes(src), nil
|
||||
case []byte:
|
||||
return src, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("pgdialect: got %T, wanted []byte or string", src)
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type arrayParser struct {
|
||||
p pgparser
|
||||
|
||||
elem []byte
|
||||
err error
|
||||
|
||||
isJson bool
|
||||
}
|
||||
|
||||
func newArrayParser(b []byte) *arrayParser {
|
||||
p := new(arrayParser)
|
||||
|
||||
if b[0] == 'n' {
|
||||
p.p.Reset(nil)
|
||||
return p
|
||||
}
|
||||
|
||||
if len(b) < 2 || (b[0] != '{' && b[0] != '[') || (b[len(b)-1] != '}' && b[len(b)-1] != ']') {
|
||||
p.err = fmt.Errorf("pgdialect: can't parse array: %q", b)
|
||||
return p
|
||||
}
|
||||
p.isJson = b[0] == '['
|
||||
|
||||
p.p.Reset(b[1 : len(b)-1])
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *arrayParser) Next() bool {
|
||||
if p.err != nil {
|
||||
return false
|
||||
}
|
||||
p.err = p.readNext()
|
||||
return p.err == nil
|
||||
}
|
||||
|
||||
func (p *arrayParser) Err() error {
|
||||
if p.err != io.EOF {
|
||||
return p.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *arrayParser) Elem() []byte {
|
||||
return p.elem
|
||||
}
|
||||
|
||||
func (p *arrayParser) readNext() error {
|
||||
ch := p.p.Read()
|
||||
if ch == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '}', ']':
|
||||
return io.EOF
|
||||
case '"':
|
||||
b, err := p.p.ReadSubstring(ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.p.Peek() == ',' {
|
||||
p.p.Advance()
|
||||
}
|
||||
|
||||
p.elem = b
|
||||
return nil
|
||||
case '[', '(':
|
||||
rng, err := p.p.ReadRange(ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.p.Peek() == ',' {
|
||||
p.p.Advance()
|
||||
}
|
||||
|
||||
p.elem = rng
|
||||
return nil
|
||||
default:
|
||||
if ch == '{' && p.isJson {
|
||||
json, err := p.p.ReadJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
if p.p.Peek() == ',' || p.p.Peek() == ' ' {
|
||||
p.p.Advance()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
p.elem = json
|
||||
return nil
|
||||
} else {
|
||||
lit := p.p.ReadLiteral(ch)
|
||||
if bytes.Equal(lit, []byte("NULL")) {
|
||||
lit = nil
|
||||
}
|
||||
|
||||
if p.p.Peek() == ',' {
|
||||
p.p.Advance()
|
||||
}
|
||||
|
||||
p.elem = lit
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/dialect/feature"
|
||||
"github.com/uptrace/bun/dialect/sqltype"
|
||||
"github.com/uptrace/bun/migrate/sqlschema"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
var pgDialect = New()
|
||||
|
||||
func init() {
|
||||
if Version() != bun.Version() {
|
||||
panic(fmt.Errorf("pgdialect and Bun must have the same version: v%s != v%s",
|
||||
Version(), bun.Version()))
|
||||
}
|
||||
}
|
||||
|
||||
type Dialect struct {
|
||||
schema.BaseDialect
|
||||
|
||||
tables *schema.Tables
|
||||
features feature.Feature
|
||||
uintAsInt bool
|
||||
}
|
||||
|
||||
var _ schema.Dialect = (*Dialect)(nil)
|
||||
var _ sqlschema.InspectorDialect = (*Dialect)(nil)
|
||||
var _ sqlschema.MigratorDialect = (*Dialect)(nil)
|
||||
|
||||
func New(opts ...DialectOption) *Dialect {
|
||||
d := new(Dialect)
|
||||
d.tables = schema.NewTables(d)
|
||||
d.features = feature.CTE |
|
||||
feature.WithValues |
|
||||
feature.Returning |
|
||||
feature.InsertReturning |
|
||||
feature.DefaultPlaceholder |
|
||||
feature.DoubleColonCast |
|
||||
feature.InsertTableAlias |
|
||||
feature.UpdateTableAlias |
|
||||
feature.DeleteTableAlias |
|
||||
feature.TableCascade |
|
||||
feature.TableIdentity |
|
||||
feature.TableTruncate |
|
||||
feature.TableNotExists |
|
||||
feature.InsertOnConflict |
|
||||
feature.SelectExists |
|
||||
feature.GeneratedIdentity |
|
||||
feature.CompositeIn |
|
||||
feature.FKDefaultOnAction |
|
||||
feature.DeleteReturning |
|
||||
feature.Merge |
|
||||
feature.MergeReturning |
|
||||
feature.AlterColumnExists |
|
||||
feature.CreateIndexIfNotExists
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(d)
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
type DialectOption func(d *Dialect)
|
||||
|
||||
func WithoutFeature(other feature.Feature) DialectOption {
|
||||
return func(d *Dialect) {
|
||||
d.features = d.features.Remove(other)
|
||||
}
|
||||
}
|
||||
|
||||
func WithAppendUintAsInt(on bool) DialectOption {
|
||||
return func(d *Dialect) {
|
||||
d.uintAsInt = on
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialect) Init(*sql.DB) {}
|
||||
|
||||
func (d *Dialect) Name() dialect.Name {
|
||||
return dialect.PG
|
||||
}
|
||||
|
||||
func (d *Dialect) Features() feature.Feature {
|
||||
return d.features
|
||||
}
|
||||
|
||||
func (d *Dialect) Tables() *schema.Tables {
|
||||
return d.tables
|
||||
}
|
||||
|
||||
func (d *Dialect) OnTable(table *schema.Table) {
|
||||
for _, field := range table.FieldMap {
|
||||
d.onField(field)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialect) onField(field *schema.Field) {
|
||||
field.DiscoveredSQLType = fieldSQLType(field)
|
||||
|
||||
if field.AutoIncrement && !field.Identity {
|
||||
switch field.DiscoveredSQLType {
|
||||
case sqltype.SmallInt:
|
||||
field.CreateTableSQLType = pgTypeSmallSerial
|
||||
case sqltype.Integer:
|
||||
field.CreateTableSQLType = pgTypeSerial
|
||||
case sqltype.BigInt:
|
||||
field.CreateTableSQLType = pgTypeBigSerial
|
||||
}
|
||||
}
|
||||
|
||||
if field.Tag.HasOption("array") || strings.HasSuffix(field.UserSQLType, "[]") {
|
||||
field.Append = d.arrayAppender(field.StructField.Type)
|
||||
field.Scan = arrayScanner(field.StructField.Type)
|
||||
return
|
||||
}
|
||||
|
||||
if field.Tag.HasOption("multirange") || strings.HasSuffix(field.UserSQLType, "multirange") {
|
||||
field.Scan = arrayScanner(field.StructField.Type)
|
||||
return
|
||||
}
|
||||
|
||||
switch field.DiscoveredSQLType {
|
||||
case sqltype.HSTORE:
|
||||
field.Append = d.hstoreAppender(field.StructField.Type)
|
||||
field.Scan = hstoreScanner(field.StructField.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialect) IdentQuote() byte {
|
||||
return '"'
|
||||
}
|
||||
|
||||
func (d *Dialect) AppendUint32(b []byte, n uint32) []byte {
|
||||
if d.uintAsInt {
|
||||
return strconv.AppendInt(b, int64(int32(n)), 10)
|
||||
}
|
||||
return strconv.AppendUint(b, uint64(n), 10)
|
||||
}
|
||||
|
||||
func (d *Dialect) AppendUint64(b []byte, n uint64) []byte {
|
||||
if d.uintAsInt {
|
||||
return strconv.AppendInt(b, int64(n), 10)
|
||||
}
|
||||
return strconv.AppendUint(b, n, 10)
|
||||
}
|
||||
|
||||
func (d *Dialect) AppendSequence(b []byte, _ *schema.Table, _ *schema.Field) []byte {
|
||||
return appendGeneratedAsIdentity(b)
|
||||
}
|
||||
|
||||
// appendGeneratedAsIdentity appends GENERATED BY DEFAULT AS IDENTITY to the column definition.
|
||||
func appendGeneratedAsIdentity(b []byte) []byte {
|
||||
return append(b, " GENERATED BY DEFAULT AS IDENTITY"...)
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
||||
func appendElem(buf []byte, val any) []byte {
|
||||
switch val := val.(type) {
|
||||
case int64:
|
||||
return strconv.AppendInt(buf, val, 10)
|
||||
case float64:
|
||||
return arrayAppendFloat64(buf, val)
|
||||
case bool:
|
||||
return dialect.AppendBool(buf, val)
|
||||
case []byte:
|
||||
return appendBytesElem(buf, val)
|
||||
case string:
|
||||
return appendStringElem(buf, val)
|
||||
case time.Time:
|
||||
buf = append(buf, '"')
|
||||
buf = appendTime(buf, val)
|
||||
buf = append(buf, '"')
|
||||
return buf
|
||||
case driver.Valuer:
|
||||
val2, err := val.Value()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("pgdialect: can't append elem value: %w", err)
|
||||
return dialect.AppendError(buf, err)
|
||||
}
|
||||
return appendElem(buf, val2)
|
||||
default:
|
||||
err := fmt.Errorf("pgdialect: can't append elem %T", val)
|
||||
return dialect.AppendError(buf, err)
|
||||
}
|
||||
}
|
||||
|
||||
func appendBytesElem(b []byte, bs []byte) []byte {
|
||||
if bs == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, `"\\x`...)
|
||||
|
||||
s := len(b)
|
||||
b = append(b, make([]byte, hex.EncodedLen(len(bs)))...)
|
||||
hex.Encode(b[s:], bs)
|
||||
|
||||
b = append(b, '"')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendStringElem(b []byte, s string) []byte {
|
||||
b = append(b, '"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case 0:
|
||||
// ignore
|
||||
case '\'':
|
||||
b = append(b, "''"...)
|
||||
case '"':
|
||||
b = append(b, '\\', '"')
|
||||
case '\\':
|
||||
b = append(b, '\\', '\\')
|
||||
default:
|
||||
if r < utf8.RuneSelf {
|
||||
b = append(b, byte(r))
|
||||
break
|
||||
}
|
||||
l := len(b)
|
||||
if cap(b)-l < utf8.UTFMax {
|
||||
b = append(b, make([]byte, utf8.UTFMax)...)
|
||||
}
|
||||
n := utf8.EncodeRune(b[l:l+utf8.UTFMax], r)
|
||||
b = b[:l+n]
|
||||
}
|
||||
}
|
||||
b = append(b, '"')
|
||||
return b
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
type HStoreValue struct {
|
||||
v reflect.Value
|
||||
|
||||
append schema.AppenderFunc
|
||||
scan schema.ScannerFunc
|
||||
}
|
||||
|
||||
// HStore accepts a map[string]string and returns a wrapper for working with PostgreSQL
|
||||
// hstore data type.
|
||||
//
|
||||
// For struct fields you can use hstore tag:
|
||||
//
|
||||
// Attrs map[string]string `bun:",hstore"`
|
||||
func HStore(vi any) *HStoreValue {
|
||||
v := reflect.ValueOf(vi)
|
||||
if !v.IsValid() {
|
||||
panic(fmt.Errorf("bun: HStore(nil)"))
|
||||
}
|
||||
|
||||
typ := v.Type()
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
if typ.Kind() != reflect.Map {
|
||||
panic(fmt.Errorf("bun: Hstore(unsupported %s)", typ))
|
||||
}
|
||||
|
||||
return &HStoreValue{
|
||||
v: v,
|
||||
|
||||
append: pgDialect.hstoreAppender(v.Type()),
|
||||
scan: hstoreScanner(v.Type()),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ schema.QueryAppender = (*HStoreValue)(nil)
|
||||
_ sql.Scanner = (*HStoreValue)(nil)
|
||||
)
|
||||
|
||||
func (h *HStoreValue) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error) {
|
||||
if h.append == nil {
|
||||
panic(fmt.Errorf("bun: HStore(unsupported %s)", h.v.Type()))
|
||||
}
|
||||
return h.append(gen, b, h.v), nil
|
||||
}
|
||||
|
||||
func (h *HStoreValue) Scan(src any) error {
|
||||
if h.scan == nil {
|
||||
return fmt.Errorf("bun: HStore(unsupported %s)", h.v.Type())
|
||||
}
|
||||
if h.v.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("bun: HStore(non-pointer %s)", h.v.Type())
|
||||
}
|
||||
return h.scan(h.v.Elem(), src)
|
||||
}
|
||||
|
||||
func (h *HStoreValue) Value() any {
|
||||
if h.v.IsValid() {
|
||||
return h.v.Interface()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type hstoreParser struct {
|
||||
p pgparser
|
||||
|
||||
key string
|
||||
value string
|
||||
err error
|
||||
}
|
||||
|
||||
func newHStoreParser(b []byte) *hstoreParser {
|
||||
p := new(hstoreParser)
|
||||
if len(b) != 0 && (len(b) < 6 || b[0] != '"') {
|
||||
p.err = fmt.Errorf("pgdialect: can't parse hstore: %q", b)
|
||||
return p
|
||||
}
|
||||
p.p.Reset(b)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *hstoreParser) Next() bool {
|
||||
if p.err != nil {
|
||||
return false
|
||||
}
|
||||
p.err = p.readNext()
|
||||
return p.err == nil
|
||||
}
|
||||
|
||||
func (p *hstoreParser) Err() error {
|
||||
if p.err != io.EOF {
|
||||
return p.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *hstoreParser) Key() string {
|
||||
return p.key
|
||||
}
|
||||
|
||||
func (p *hstoreParser) Value() string {
|
||||
return p.value
|
||||
}
|
||||
|
||||
func (p *hstoreParser) readNext() error {
|
||||
if !p.p.Valid() {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
if err := p.p.Skip('"'); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := p.p.ReadUnescapedSubstring('"')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.key = string(key)
|
||||
|
||||
if err := p.p.SkipPrefix([]byte("=>")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := p.p.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '"':
|
||||
value, err := p.p.ReadUnescapedSubstring(ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.skipComma()
|
||||
p.value = string(value)
|
||||
return nil
|
||||
default:
|
||||
value := p.p.ReadLiteral(ch)
|
||||
if bytes.Equal(value, []byte("NULL")) {
|
||||
p.value = ""
|
||||
}
|
||||
p.skipComma()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *hstoreParser) skipComma() {
|
||||
if p.p.Peek() == ',' {
|
||||
p.p.Advance()
|
||||
}
|
||||
if p.p.Peek() == ' ' {
|
||||
p.p.Advance()
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
func hstoreScanner(typ reflect.Type) schema.ScannerFunc {
|
||||
kind := typ.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if fn := hstoreScanner(typ.Elem()); fn != nil {
|
||||
return schema.PtrScanner(fn)
|
||||
}
|
||||
case reflect.Map:
|
||||
// ok:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if typ.Key() == stringType && typ.Elem() == stringType {
|
||||
return scanMapStringStringValue
|
||||
}
|
||||
return func(dest reflect.Value, src any) error {
|
||||
return fmt.Errorf("bun: Hstore(unsupported %s)", dest.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func scanMapStringStringValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
m, err := decodeMapStringString(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(m))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeMapStringString(src any) (map[string]string, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := make(map[string]string)
|
||||
|
||||
p := newHStoreParser(b)
|
||||
for p.Next() {
|
||||
m[p.Key()] = p.Value()
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate/sqlschema"
|
||||
)
|
||||
|
||||
type (
|
||||
Schema = sqlschema.BaseDatabase
|
||||
Table = sqlschema.BaseTable
|
||||
Column = sqlschema.BaseColumn
|
||||
)
|
||||
|
||||
func (d *Dialect) NewInspector(db *bun.DB, options ...sqlschema.InspectorOption) sqlschema.Inspector {
|
||||
return newInspector(db, options...)
|
||||
}
|
||||
|
||||
type Inspector struct {
|
||||
sqlschema.InspectorConfig
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
var _ sqlschema.Inspector = (*Inspector)(nil)
|
||||
|
||||
func newInspector(db *bun.DB, options ...sqlschema.InspectorOption) *Inspector {
|
||||
i := &Inspector{db: db}
|
||||
sqlschema.ApplyInspectorOptions(&i.InspectorConfig, options...)
|
||||
return i
|
||||
}
|
||||
|
||||
func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) {
|
||||
dbSchema := Schema{
|
||||
ForeignKeys: make(map[sqlschema.ForeignKey]string),
|
||||
}
|
||||
|
||||
exclude := in.ExcludeTables
|
||||
if len(exclude) == 0 {
|
||||
// Avoid getting NOT LIKE ALL (ARRAY[NULL]) if bun.List() is called with an empty slice.
|
||||
exclude = []string{""}
|
||||
}
|
||||
|
||||
var tables []*InformationSchemaTable
|
||||
if err := in.db.NewRaw(sqlInspectTables, in.SchemaName, bun.List(exclude)).Scan(ctx, &tables); err != nil {
|
||||
return dbSchema, err
|
||||
}
|
||||
|
||||
var fks []*ForeignKey
|
||||
if err := in.db.NewRaw(sqlInspectForeignKeys, in.SchemaName, bun.List(exclude), bun.List(exclude)).Scan(ctx, &fks); err != nil {
|
||||
return dbSchema, err
|
||||
}
|
||||
dbSchema.ForeignKeys = make(map[sqlschema.ForeignKey]string, len(fks))
|
||||
|
||||
for _, table := range tables {
|
||||
var columns []*InformationSchemaColumn
|
||||
if err := in.db.NewRaw(sqlInspectColumnsQuery, table.Schema, table.Name).Scan(ctx, &columns); err != nil {
|
||||
return dbSchema, err
|
||||
}
|
||||
|
||||
var colDefs []sqlschema.Column
|
||||
uniqueGroups := make(map[string][]string)
|
||||
|
||||
for _, c := range columns {
|
||||
def := c.Default
|
||||
if c.IsSerial || c.IsIdentity {
|
||||
def = ""
|
||||
} else if !c.IsDefaultLiteral {
|
||||
def = strings.ToLower(def)
|
||||
}
|
||||
|
||||
colDefs = append(colDefs, &Column{
|
||||
Name: c.Name,
|
||||
SQLType: c.DataType,
|
||||
VarcharLen: c.VarcharLen,
|
||||
DefaultValue: def,
|
||||
IsNullable: c.IsNullable,
|
||||
IsAutoIncrement: c.IsSerial,
|
||||
IsIdentity: c.IsIdentity,
|
||||
})
|
||||
|
||||
for _, group := range c.UniqueGroups {
|
||||
uniqueGroups[group] = append(uniqueGroups[group], c.Name)
|
||||
}
|
||||
}
|
||||
|
||||
var unique []sqlschema.Unique
|
||||
for name, columns := range uniqueGroups {
|
||||
unique = append(unique, sqlschema.Unique{
|
||||
Name: name,
|
||||
Columns: sqlschema.NewColumns(columns...),
|
||||
})
|
||||
}
|
||||
|
||||
var pk *sqlschema.PrimaryKey
|
||||
if len(table.PrimaryKey.Columns) > 0 {
|
||||
pk = &sqlschema.PrimaryKey{
|
||||
Name: table.PrimaryKey.ConstraintName,
|
||||
Columns: sqlschema.NewColumns(table.PrimaryKey.Columns...),
|
||||
}
|
||||
}
|
||||
|
||||
dbSchema.Tables = append(dbSchema.Tables, &Table{
|
||||
Schema: table.Schema,
|
||||
Name: table.Name,
|
||||
Columns: colDefs,
|
||||
PrimaryKey: pk,
|
||||
UniqueConstraints: unique,
|
||||
})
|
||||
}
|
||||
|
||||
for _, fk := range fks {
|
||||
dbFK := sqlschema.ForeignKey{
|
||||
From: sqlschema.NewColumnReference(fk.SourceTable, fk.SourceColumns...),
|
||||
To: sqlschema.NewColumnReference(fk.TargetTable, fk.TargetColumns...),
|
||||
}
|
||||
if _, exclude := in.ExcludeForeignKeys[dbFK]; exclude {
|
||||
continue
|
||||
}
|
||||
dbSchema.ForeignKeys[dbFK] = fk.ConstraintName
|
||||
}
|
||||
return dbSchema, nil
|
||||
}
|
||||
|
||||
type InformationSchemaTable struct {
|
||||
Schema string `bun:"table_schema,pk"`
|
||||
Name string `bun:"table_name,pk"`
|
||||
PrimaryKey PrimaryKey `bun:"embed:primary_key_"`
|
||||
|
||||
Columns []*InformationSchemaColumn `bun:"rel:has-many,join:table_schema=table_schema,join:table_name=table_name"`
|
||||
}
|
||||
|
||||
type InformationSchemaColumn struct {
|
||||
Schema string `bun:"table_schema"`
|
||||
Table string `bun:"table_name"`
|
||||
Name string `bun:"column_name"`
|
||||
DataType string `bun:"data_type"`
|
||||
VarcharLen int `bun:"varchar_len"`
|
||||
IsArray bool `bun:"is_array"`
|
||||
ArrayDims int `bun:"array_dims"`
|
||||
Default string `bun:"default"`
|
||||
IsDefaultLiteral bool `bun:"default_is_literal_expr"`
|
||||
IsIdentity bool `bun:"is_identity"`
|
||||
IndentityType string `bun:"identity_type"`
|
||||
IsSerial bool `bun:"is_serial"`
|
||||
IsNullable bool `bun:"is_nullable"`
|
||||
UniqueGroups []string `bun:"unique_groups,array"`
|
||||
}
|
||||
|
||||
type ForeignKey struct {
|
||||
ConstraintName string `bun:"constraint_name"`
|
||||
SourceSchema string `bun:"schema_name"`
|
||||
SourceTable string `bun:"table_name"`
|
||||
SourceColumns []string `bun:"columns,array"`
|
||||
TargetSchema string `bun:"target_schema"`
|
||||
TargetTable string `bun:"target_table"`
|
||||
TargetColumns []string `bun:"target_columns,array"`
|
||||
}
|
||||
|
||||
type PrimaryKey struct {
|
||||
ConstraintName string `bun:"name"`
|
||||
Columns []string `bun:"columns,array"`
|
||||
}
|
||||
|
||||
const (
|
||||
// sqlInspectTables retrieves all user-defined tables in the selected schema.
|
||||
// Pass bun.List([]string{...}) to exclude tables from this inspection or bun.List([]string{''}) to include all results.
|
||||
sqlInspectTables = `
|
||||
SELECT
|
||||
"t".table_schema,
|
||||
"t".table_name,
|
||||
pk.name AS primary_key_name,
|
||||
pk.columns AS primary_key_columns
|
||||
FROM information_schema.tables "t"
|
||||
LEFT JOIN (
|
||||
SELECT i.indrelid, "idx".relname AS "name", ARRAY_AGG("a".attname) AS "columns"
|
||||
FROM pg_index i
|
||||
JOIN pg_attribute "a"
|
||||
ON "a".attrelid = i.indrelid
|
||||
AND "a".attnum = ANY("i".indkey)
|
||||
AND i.indisprimary
|
||||
JOIN pg_class "idx" ON i.indexrelid = "idx".oid
|
||||
GROUP BY 1, 2
|
||||
) pk
|
||||
ON ("t".table_schema || '.' || "t".table_name)::regclass = pk.indrelid
|
||||
WHERE table_type = 'BASE TABLE'
|
||||
AND "t".table_schema = ?
|
||||
AND "t".table_schema NOT LIKE 'pg_%'
|
||||
AND "table_name" NOT LIKE ALL (ARRAY[?])
|
||||
ORDER BY "t".table_schema, "t".table_name
|
||||
`
|
||||
|
||||
// sqlInspectColumnsQuery retrieves column definitions for the specified table.
|
||||
// Unlike sqlInspectTables and sqlInspectSchema, it should be passed to bun.NewRaw
|
||||
// with additional args for table_schema and table_name.
|
||||
sqlInspectColumnsQuery = `
|
||||
SELECT
|
||||
"c".table_schema,
|
||||
"c".table_name,
|
||||
"c".column_name,
|
||||
"c".data_type,
|
||||
"c".character_maximum_length::integer AS varchar_len,
|
||||
"c".data_type = 'ARRAY' AS is_array,
|
||||
COALESCE("c".array_dims, 0) AS array_dims,
|
||||
CASE
|
||||
WHEN "c".column_default ~ '^''.*''::.*$' THEN substring("c".column_default FROM '^''(.*)''::.*$')
|
||||
ELSE "c".column_default
|
||||
END AS "default",
|
||||
"c".column_default ~ '^''.*''::.*$' OR "c".column_default ~ '^[0-9\.]+$' AS default_is_literal_expr,
|
||||
"c".is_identity = 'YES' AS is_identity,
|
||||
"c".column_default = format('nextval(''%s_%s_seq''::regclass)', "c".table_name, "c".column_name) AS is_serial,
|
||||
COALESCE("c".identity_type, '') AS identity_type,
|
||||
"c".is_nullable = 'YES' AS is_nullable,
|
||||
"c"."unique_groups" AS unique_groups
|
||||
FROM (
|
||||
SELECT
|
||||
"table_schema",
|
||||
"table_name",
|
||||
"column_name",
|
||||
"c".data_type,
|
||||
"c".character_maximum_length,
|
||||
"c".column_default,
|
||||
"c".is_identity,
|
||||
"c".is_nullable,
|
||||
att.array_dims,
|
||||
att.identity_type,
|
||||
att."unique_groups",
|
||||
att."constraint_type"
|
||||
FROM information_schema.columns "c"
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
s.nspname AS "table_schema",
|
||||
"t".relname AS "table_name",
|
||||
"c".attname AS "column_name",
|
||||
"c".attndims AS array_dims,
|
||||
"c".attidentity AS identity_type,
|
||||
ARRAY_AGG(con.conname) FILTER (WHERE con.contype = 'u') AS "unique_groups",
|
||||
ARRAY_AGG(con.contype) AS "constraint_type"
|
||||
FROM (
|
||||
SELECT
|
||||
conname,
|
||||
contype,
|
||||
connamespace,
|
||||
conrelid,
|
||||
conrelid AS attrelid,
|
||||
UNNEST(conkey) AS attnum
|
||||
FROM pg_constraint
|
||||
) con
|
||||
LEFT JOIN pg_attribute "c" USING (attrelid, attnum)
|
||||
LEFT JOIN pg_namespace s ON s.oid = con.connamespace
|
||||
LEFT JOIN pg_class "t" ON "t".oid = con.conrelid
|
||||
GROUP BY 1, 2, 3, 4, 5
|
||||
) att USING ("table_schema", "table_name", "column_name")
|
||||
) "c"
|
||||
WHERE "table_schema" = ? AND "table_name" = ?
|
||||
ORDER BY "table_schema", "table_name", "column_name"
|
||||
`
|
||||
|
||||
// sqlInspectForeignKeys get FK definitions for user-defined tables.
|
||||
// Pass bun.List([]string{...}) to exclude tables from this inspection or bun.List([]string{''}) to include all results.
|
||||
sqlInspectForeignKeys = `
|
||||
WITH
|
||||
"schemas" AS (
|
||||
SELECT oid, nspname
|
||||
FROM pg_namespace
|
||||
),
|
||||
"tables" AS (
|
||||
SELECT oid, relnamespace, relname, relkind
|
||||
FROM pg_class
|
||||
),
|
||||
"columns" AS (
|
||||
SELECT attrelid, attname, attnum
|
||||
FROM pg_attribute
|
||||
WHERE attisdropped = false
|
||||
)
|
||||
SELECT DISTINCT
|
||||
co.conname AS "constraint_name",
|
||||
ss.nspname AS schema_name,
|
||||
s.relname AS "table_name",
|
||||
ARRAY_AGG(sc.attname) AS "columns",
|
||||
ts.nspname AS target_schema,
|
||||
"t".relname AS target_table,
|
||||
ARRAY_AGG(tc.attname) AS target_columns
|
||||
FROM pg_constraint co
|
||||
LEFT JOIN "tables" s ON s.oid = co.conrelid
|
||||
LEFT JOIN "schemas" ss ON ss.oid = s.relnamespace
|
||||
LEFT JOIN "columns" sc ON sc.attrelid = s.oid AND sc.attnum = ANY(co.conkey)
|
||||
LEFT JOIN "tables" t ON t.oid = co.confrelid
|
||||
LEFT JOIN "schemas" ts ON ts.oid = "t".relnamespace
|
||||
LEFT JOIN "columns" tc ON tc.attrelid = "t".oid AND tc.attnum = ANY(co.confkey)
|
||||
WHERE co.contype = 'f'
|
||||
AND co.conrelid IN (SELECT oid FROM pg_class WHERE relkind = 'r')
|
||||
AND ARRAY_POSITION(co.conkey, sc.attnum) = ARRAY_POSITION(co.confkey, tc.attnum)
|
||||
AND ss.nspname = ?
|
||||
AND s.relname NOT LIKE ALL (ARRAY[?])
|
||||
AND "t".relname NOT LIKE ALL (ARRAY[?])
|
||||
GROUP BY "constraint_name", "schema_name", "table_name", target_schema, target_table
|
||||
`
|
||||
)
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/uptrace/bun/internal/parser"
|
||||
)
|
||||
|
||||
type pgparser struct {
|
||||
parser.Parser
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func newParser(b []byte) *pgparser {
|
||||
p := new(pgparser)
|
||||
p.Reset(b)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadLiteral(ch byte) []byte {
|
||||
p.Unread()
|
||||
lit, _ := p.ReadSep(',')
|
||||
return lit
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadUnescapedSubstring(ch byte) ([]byte, error) {
|
||||
return p.readSubstring(ch, false)
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadSubstring(ch byte) ([]byte, error) {
|
||||
return p.readSubstring(ch, true)
|
||||
}
|
||||
|
||||
func (p *pgparser) readSubstring(ch byte, escaped bool) ([]byte, error) {
|
||||
ch, err := p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.buf = p.buf[:0]
|
||||
for {
|
||||
if ch == '"' {
|
||||
break
|
||||
}
|
||||
|
||||
next, err := p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ch == '\\' {
|
||||
switch next {
|
||||
case '\\', '"':
|
||||
p.buf = append(p.buf, next)
|
||||
|
||||
ch, err = p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
p.buf = append(p.buf, '\\')
|
||||
ch = next
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if escaped && ch == '\'' && next == '\'' {
|
||||
p.buf = append(p.buf, next)
|
||||
ch, err = p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
p.buf = append(p.buf, ch)
|
||||
ch = next
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(p.buf, []byte("\\x")) && len(p.buf)%2 == 0 {
|
||||
data := p.buf[2:]
|
||||
buf := make([]byte, hex.DecodedLen(len(data)))
|
||||
n, err := hex.Decode(buf, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
return p.buf, nil
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadRange(ch byte) ([]byte, error) {
|
||||
p.buf = p.buf[:0]
|
||||
p.buf = append(p.buf, ch)
|
||||
|
||||
for p.Valid() {
|
||||
ch = p.Read()
|
||||
p.buf = append(p.buf, ch)
|
||||
if ch == ']' || ch == ')' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return p.buf, nil
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadJSON() ([]byte, error) {
|
||||
p.Unread()
|
||||
|
||||
c, err := p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.buf = p.buf[:0]
|
||||
|
||||
depth := 0
|
||||
for {
|
||||
switch c {
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
}
|
||||
|
||||
p.buf = append(p.buf, c)
|
||||
|
||||
if depth == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
next, err := p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c = next
|
||||
}
|
||||
|
||||
return p.buf, nil
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun/internal"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
type Range[T any] struct {
|
||||
Lower, Upper T
|
||||
LowerBound, UpperBound RangeBound
|
||||
}
|
||||
|
||||
type MultiRange[T any] []Range[T]
|
||||
|
||||
type RangeBound byte
|
||||
|
||||
const (
|
||||
// RangeBoundUnset indicates that no bound is set.
|
||||
// This usually means the range is uninitialized or unspecified.
|
||||
RangeBoundUnset RangeBound = 0x0
|
||||
// RangeBoundEmpty is a special marker for an empty range.
|
||||
// This is NOT a valid PostgreSQL bound character, but is used internally
|
||||
// to represent a range that contains no values.
|
||||
RangeBoundEmpty RangeBound = 'E'
|
||||
|
||||
RangeBoundInclusiveLeft RangeBound = '['
|
||||
RangeBoundInclusiveRight RangeBound = ']'
|
||||
RangeBoundExclusiveLeft RangeBound = '('
|
||||
RangeBoundExclusiveRight RangeBound = ')'
|
||||
)
|
||||
|
||||
type RangeOption[T any] func(*Range[T])
|
||||
|
||||
func NewRange[T any](lower, upper T) Range[T] {
|
||||
r := Range[T]{
|
||||
Lower: lower,
|
||||
Upper: upper,
|
||||
LowerBound: RangeBoundInclusiveLeft,
|
||||
UpperBound: RangeBoundExclusiveRight,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func NewEmptyRange[T any]() Range[T] {
|
||||
return Range[T]{LowerBound: RangeBoundEmpty, UpperBound: RangeBoundEmpty}
|
||||
}
|
||||
|
||||
func (r *Range[T]) IsZero() bool {
|
||||
// NOTE: r.LowerBound represent
|
||||
return r == nil || r.LowerBound == 0
|
||||
}
|
||||
|
||||
func (r Range[T]) IsEmpty() bool {
|
||||
return r.LowerBound == RangeBoundEmpty
|
||||
}
|
||||
|
||||
var _ sql.Scanner = (*Range[any])(nil)
|
||||
|
||||
func (r *Range[T]) Scan(raw any) (err error) {
|
||||
var src []byte
|
||||
switch v := raw.(type) {
|
||||
case []byte:
|
||||
src = v
|
||||
case string:
|
||||
src = []byte(v)
|
||||
case nil:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("pgdialect: Range can't scan %T", raw)
|
||||
}
|
||||
|
||||
src = bytes.TrimSpace(src)
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if string(src) == "empty" {
|
||||
r.LowerBound, r.UpperBound = RangeBoundEmpty, RangeBoundEmpty
|
||||
return nil
|
||||
}
|
||||
|
||||
switch src[0] {
|
||||
case byte(RangeBoundInclusiveLeft), byte(RangeBoundExclusiveLeft):
|
||||
r.LowerBound = RangeBound(src[0])
|
||||
default:
|
||||
return fmt.Errorf("unexpected lower bound: %s", string(src[:1]))
|
||||
}
|
||||
switch src[len(src)-1] {
|
||||
case byte(RangeBoundInclusiveRight), byte(RangeBoundExclusiveRight):
|
||||
r.UpperBound = RangeBound(src[len(src)-1])
|
||||
default:
|
||||
return fmt.Errorf("unexpected upper bound: %s", string(src[len(src)-1:]))
|
||||
}
|
||||
|
||||
src = src[1 : len(src)-1]
|
||||
|
||||
ind := bytes.IndexByte(src, ',')
|
||||
if ind == -1 {
|
||||
return fmt.Errorf("invalid range: wanted comma, got %s", string(src))
|
||||
}
|
||||
left, right := src[:ind], src[ind+1:]
|
||||
|
||||
if len(left) > 0 {
|
||||
_, err := scanElem(&r.Lower, left)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r.LowerBound = RangeBoundUnset
|
||||
}
|
||||
|
||||
if len(right) > 0 {
|
||||
_, err = scanElem(&r.Upper, right)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r.UpperBound = RangeBoundUnset
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ schema.QueryAppender = (*Range[any])(nil)
|
||||
|
||||
func (r Range[T]) AppendQuery(_ schema.QueryGen, buf []byte) ([]byte, error) {
|
||||
buf = append(buf, '\'')
|
||||
buf = appendRange(buf, r)
|
||||
buf = append(buf, '\'')
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func appendRange[T any](buf []byte, r Range[T]) []byte {
|
||||
if r.IsEmpty() {
|
||||
buf = append(buf, []byte("empty")...)
|
||||
return buf
|
||||
}
|
||||
|
||||
if r.LowerBound == RangeBoundUnset {
|
||||
// NOTE from pg's document:
|
||||
// > Specifying a missing bound as inclusive is automatically converted to exclusive, e.g., [,] is converted to (,).
|
||||
buf = append(buf, byte(RangeBoundExclusiveLeft))
|
||||
} else {
|
||||
buf = append(buf, byte(r.LowerBound))
|
||||
buf = appendElem(buf, r.Lower)
|
||||
}
|
||||
buf = append(buf, ',')
|
||||
if r.UpperBound == RangeBoundUnset {
|
||||
buf = append(buf, byte(RangeBoundExclusiveRight))
|
||||
} else {
|
||||
buf = appendElem(buf, r.Upper)
|
||||
buf = append(buf, byte(r.UpperBound))
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (m *MultiRange[T]) Len() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return len(([]Range[T])(*m))
|
||||
}
|
||||
|
||||
func (m *MultiRange[T]) IsZero() bool {
|
||||
return m.Len() == 0
|
||||
}
|
||||
|
||||
func (m MultiRange[T]) AppendQuery(_ schema.QueryGen, buf []byte) ([]byte, error) {
|
||||
if m == nil {
|
||||
return append(buf, []byte("'{}'")...), nil
|
||||
}
|
||||
rs := ([]Range[T])(m)
|
||||
buf = append(buf, '\'', '{')
|
||||
for _, r := range rs {
|
||||
buf = appendRange(buf, r)
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
if len(rs) > 0 {
|
||||
buf[len(buf)-1] = '}'
|
||||
} else {
|
||||
buf = append(buf, '}')
|
||||
}
|
||||
buf = append(buf, '\'')
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func scanElem(ptr any, src []byte) ([]byte, error) {
|
||||
// NOTE: for daterange, pg return 2024-12-01, for tzrange, pg return "2024-12-01 12:00:00"
|
||||
if len(src) >= 2 && src[0] == '"' {
|
||||
src = src[1 : len(src)-1]
|
||||
}
|
||||
|
||||
switch ptr := ptr.(type) {
|
||||
case *time.Time:
|
||||
tm, err := internal.ParseTime(internal.String(src))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*ptr = tm
|
||||
|
||||
return src, nil
|
||||
|
||||
case sql.Scanner:
|
||||
if err := ptr.Scan(src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return src, nil
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported range type: %T", ptr))
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun/dialect/sqltype"
|
||||
"github.com/uptrace/bun/migrate/sqlschema"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
// Date / Time
|
||||
pgTypeTimestamp = "TIMESTAMP" // Timestamp
|
||||
pgTypeTimestampWithTz = "TIMESTAMP WITH TIME ZONE" // Timestamp with a time zone
|
||||
pgTypeTimestampTz = "TIMESTAMPTZ" // Timestamp with a time zone (alias)
|
||||
pgTypeDate = "DATE" // Date
|
||||
pgTypeTime = "TIME" // Time without a time zone
|
||||
pgTypeTimeTz = "TIME WITH TIME ZONE" // Time with a time zone
|
||||
pgTypeInterval = "INTERVAL" // Time interval
|
||||
|
||||
// Network Addresses
|
||||
pgTypeInet = "INET" // IPv4 or IPv6 hosts and networks
|
||||
pgTypeCidr = "CIDR" // IPv4 or IPv6 networks
|
||||
pgTypeMacaddr = "MACADDR" // MAC addresses
|
||||
|
||||
// Serial Types
|
||||
pgTypeSmallSerial = "SMALLSERIAL" // 2 byte autoincrementing integer
|
||||
pgTypeSerial = "SERIAL" // 4 byte autoincrementing integer
|
||||
pgTypeBigSerial = "BIGSERIAL" // 8 byte autoincrementing integer
|
||||
|
||||
// Character Types
|
||||
pgTypeChar = "CHAR" // fixed length string (blank padded)
|
||||
pgTypeCharacter = "CHARACTER" // alias for CHAR
|
||||
pgTypeText = "TEXT" // variable length string without limit
|
||||
pgTypeVarchar = "VARCHAR" // variable length string with optional limit
|
||||
pgTypeCharacterVarying = "CHARACTER VARYING" // alias for VARCHAR
|
||||
|
||||
// Binary Data Types
|
||||
pgTypeBytea = "BYTEA" // binary string
|
||||
)
|
||||
|
||||
var (
|
||||
ipType = reflect.TypeFor[net.IP]()
|
||||
ipNetType = reflect.TypeFor[net.IPNet]()
|
||||
jsonRawMessageType = reflect.TypeFor[json.RawMessage]()
|
||||
nullStringType = reflect.TypeFor[sql.NullString]()
|
||||
)
|
||||
|
||||
func (d *Dialect) DefaultVarcharLen() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d *Dialect) DefaultSchema() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
func fieldSQLType(field *schema.Field) string {
|
||||
if field.UserSQLType != "" {
|
||||
return field.UserSQLType
|
||||
}
|
||||
|
||||
if v, ok := field.Tag.Option("composite"); ok {
|
||||
return v
|
||||
}
|
||||
if field.Tag.HasOption("hstore") {
|
||||
return sqltype.HSTORE
|
||||
}
|
||||
|
||||
if field.Tag.HasOption("array") {
|
||||
switch field.IndirectType.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
sqlType := sqlType(field.IndirectType.Elem())
|
||||
return sqlType + "[]"
|
||||
}
|
||||
}
|
||||
|
||||
if field.DiscoveredSQLType == sqltype.Blob {
|
||||
return pgTypeBytea
|
||||
}
|
||||
|
||||
return sqlType(field.IndirectType)
|
||||
}
|
||||
|
||||
func sqlType(typ reflect.Type) string {
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case nullStringType: // typ.Kind() == reflect.Struct, test for exact match
|
||||
return sqltype.VarChar
|
||||
case ipType:
|
||||
return pgTypeInet
|
||||
case ipNetType:
|
||||
return pgTypeCidr
|
||||
case jsonRawMessageType:
|
||||
return sqltype.JSONB
|
||||
}
|
||||
|
||||
sqlType := schema.DiscoverSQLType(typ)
|
||||
switch sqlType {
|
||||
case sqltype.Timestamp:
|
||||
sqlType = pgTypeTimestampTz
|
||||
}
|
||||
|
||||
switch typ.Kind() {
|
||||
case reflect.Map, reflect.Struct: // except typ == nullStringType, see above
|
||||
if sqlType == sqltype.VarChar {
|
||||
return sqltype.JSONB
|
||||
}
|
||||
return sqlType
|
||||
case reflect.Array, reflect.Slice:
|
||||
if typ.Elem().Kind() == reflect.Uint8 {
|
||||
return pgTypeBytea
|
||||
}
|
||||
return sqltype.JSONB
|
||||
}
|
||||
|
||||
return sqlType
|
||||
}
|
||||
|
||||
var (
|
||||
char = newAliases(pgTypeChar, pgTypeCharacter)
|
||||
varchar = newAliases(pgTypeVarchar, pgTypeCharacterVarying)
|
||||
timestampTz = newAliases(sqltype.Timestamp, pgTypeTimestampTz, pgTypeTimestampWithTz)
|
||||
bigint = newAliases(sqltype.BigInt, pgTypeBigSerial)
|
||||
integer = newAliases(sqltype.Integer, pgTypeSerial)
|
||||
smallint = newAliases(sqltype.SmallInt, pgTypeSmallSerial)
|
||||
)
|
||||
|
||||
func (d *Dialect) CompareType(col1, col2 sqlschema.Column) bool {
|
||||
typ1, typ2 := strings.ToUpper(col1.GetSQLType()), strings.ToUpper(col2.GetSQLType())
|
||||
|
||||
if typ1 == typ2 {
|
||||
return checkVarcharLen(col1, col2, d.DefaultVarcharLen())
|
||||
}
|
||||
|
||||
switch {
|
||||
case char.IsAlias(typ1) && char.IsAlias(typ2):
|
||||
return checkVarcharLen(col1, col2, d.DefaultVarcharLen())
|
||||
case varchar.IsAlias(typ1) && varchar.IsAlias(typ2):
|
||||
return checkVarcharLen(col1, col2, d.DefaultVarcharLen())
|
||||
case timestampTz.IsAlias(typ1) && timestampTz.IsAlias(typ2):
|
||||
return true
|
||||
case bigint.IsAlias(typ1) && bigint.IsAlias(typ2),
|
||||
integer.IsAlias(typ1) && integer.IsAlias(typ2),
|
||||
smallint.IsAlias(typ1) && smallint.IsAlias(typ2):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checkVarcharLen returns true if columns have the same VarcharLen, or,
|
||||
// if one specifies no VarcharLen and the other one has the default lenght for pgdialect.
|
||||
// We assume that the types are otherwise equivalent and that any non-character column
|
||||
// would have VarcharLen == 0;
|
||||
func checkVarcharLen(col1, col2 sqlschema.Column, defaultLen int) bool {
|
||||
vl1, vl2 := col1.GetVarcharLen(), col2.GetVarcharLen()
|
||||
|
||||
if vl1 == vl2 {
|
||||
return true
|
||||
}
|
||||
|
||||
if (vl1 == 0 && vl2 == defaultLen) || (vl1 == defaultLen && vl2 == 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// typeAlias defines aliases for common data types. It is a lightweight string set implementation.
|
||||
type typeAlias map[string]struct{}
|
||||
|
||||
// IsAlias checks if typ1 and typ2 are aliases of the same data type.
|
||||
func (t typeAlias) IsAlias(typ string) bool {
|
||||
_, ok := t[typ]
|
||||
return ok
|
||||
}
|
||||
|
||||
// newAliases creates a set of aliases.
|
||||
func newAliases(aliases ...string) typeAlias {
|
||||
types := make(typeAlias)
|
||||
for _, a := range aliases {
|
||||
types[a] = struct{}{}
|
||||
}
|
||||
return types
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package pgdialect
|
||||
|
||||
// Version is the current release version.
|
||||
func Version() string {
|
||||
return "1.2.18"
|
||||
}
|
||||
Reference in New Issue
Block a user