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:
Hein
2026-07-21 12:41:38 +02:00
parent 2cecb4c11c
commit 5d9ff5df03
65 changed files with 9584 additions and 167 deletions
+24
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,6 @@
package pgdialect
// Version is the current release version.
func Version() string {
return "1.2.18"
}