fix(go.sum): update ResolveSpec dependency to v1.0.87
CI / build-and-test (push) Failing after 1s
Release / release (push) Failing after 19m26s

This commit is contained in:
Hein
2026-06-23 13:17:16 +02:00
parent 0227912325
commit 1adf50e3db
2436 changed files with 1078758 additions and 114 deletions
+4
View File
@@ -0,0 +1,4 @@
# Patterns for files created by this project.
# For other files, use global gitignore.
*.s3db
.idea
+6
View File
@@ -0,0 +1,6 @@
trailingComma: all
tabWidth: 2
semi: false
singleQuote: true
proseWrap: always
printWidth: 100
+1118
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
## Running tests
To run tests, you need Docker which starts PostgreSQL and MySQL servers:
```shell
cd internal/dbtest
./test.sh
```
To ease debugging, you can run tests and print all executed queries:
```shell
BUNDEBUG=2 TZ= go test -run=TestName
```
## Releasing
1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub:
```shell
TAG=v1.0.0 ./scripts/release.sh
```
2. Open a pull request and wait for the build to finish.
3. Merge the pull request and run `tag.sh` to create tags for packages:
```shell
TAG=v1.0.0 ./scripts/tag.sh
```
## Documentation
To contribute to the docs visit https://github.com/uptrace/bun-docs
+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.
+30
View File
@@ -0,0 +1,30 @@
ALL_GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
EXAMPLE_GO_MOD_DIRS := $(shell find ./example/ -type f -name 'go.mod' -exec dirname {} \; | sort)
test:
set -e; for dir in $(ALL_GO_MOD_DIRS); do \
echo "go test in $${dir}"; \
(cd "$${dir}" && \
go test && \
go test -race && \
env GOOS=linux GOARCH=386 TZ= go test && \
go vet); \
done
go_mod_tidy:
set -e; for dir in $(ALL_GO_MOD_DIRS); do \
echo "go mod tidy in $${dir}"; \
(cd "$${dir}" && \
go get -u ./... && \
go mod tidy); \
done
fmt:
gofmt -w -s ./
goimports -w -local github.com/uptrace/bun ./
run-examples:
set -e; for dir in $(EXAMPLE_GO_MOD_DIRS); do \
echo "go run . in $${dir}"; \
(cd "$${dir}" && go run .); \
done
+286
View File
@@ -0,0 +1,286 @@
# Bun: SQL-first Golang ORM
[![build workflow](https://github.com/uptrace/bun/actions/workflows/build.yml/badge.svg)](https://github.com/uptrace/bun/actions)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/uptrace/bun)](https://pkg.go.dev/github.com/uptrace/bun)
[![Documentation](https://img.shields.io/badge/bun-documentation-informational)](https://bun.uptrace.dev/)
[![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj)
[![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Bun%20Guru-006BFF)](https://gurubase.io/g/bun)
**Lightweight, SQL-first Golang ORM for PostgreSQL, MySQL, MSSQL, SQLite, and Oracle**
Bun is a modern ORM that embraces SQL rather than hiding it. Write complex queries in Go with type
safety, powerful scanning capabilities, and database-agnostic code that works across multiple SQL
databases.
## ✨ Key Features
- **SQL-first approach** - Write elegant, readable queries that feel like SQL
- **Multi-database support** - PostgreSQL, MySQL/MariaDB, MSSQL, SQLite, and Oracle
- **Type-safe operations** - Leverage Go's static typing for compile-time safety
- **Flexible scanning** - Query results into structs, maps, scalars, or slices
- **Performance optimized** - Built on `database/sql` with minimal overhead
- **Rich relationships** - Define complex table relationships with struct tags
- **Production ready** - Migrations, fixtures, soft deletes, and OpenTelemetry support
## 🚀 Quick Start
```bash
go get github.com/uptrace/bun
```
### Basic Example
```go
package main
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim"
)
func main() {
ctx := context.Background()
// Open database
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:")
if err != nil {
panic(err)
}
// Create Bun instance
db := bun.NewDB(sqldb, sqlitedialect.New())
// Define model
type User struct {
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
}
// Create table
db.NewCreateTable().Model((*User)(nil)).Exec(ctx)
// Insert user
user := &User{Name: "John Doe"}
db.NewInsert().Model(user).Exec(ctx)
// Query user
err = db.NewSelect().Model(user).Where("id = ?", user.ID).Scan(ctx)
fmt.Printf("User: %+v\n", user)
}
```
## 🎯 Why Choose Bun?
### Elegant Complex Queries
Write sophisticated queries that remain readable and maintainable:
```go
regionalSales := db.NewSelect().
ColumnExpr("region").
ColumnExpr("SUM(amount) AS total_sales").
TableExpr("orders").
GroupExpr("region")
topRegions := db.NewSelect().
ColumnExpr("region").
TableExpr("regional_sales").
Where("total_sales > (SELECT SUM(total_sales) / 10 FROM regional_sales)")
var results []struct {
Region string `bun:"region"`
Product string `bun:"product"`
ProductUnits int `bun:"product_units"`
ProductSales int `bun:"product_sales"`
}
err := db.NewSelect().
With("regional_sales", regionalSales).
With("top_regions", topRegions).
ColumnExpr("region, product").
ColumnExpr("SUM(quantity) AS product_units").
ColumnExpr("SUM(amount) AS product_sales").
TableExpr("orders").
Where("region IN (SELECT region FROM top_regions)").
GroupExpr("region, product").
Scan(ctx, &results)
```
### Flexible Result Scanning
Scan query results into various Go types:
```go
// Into structs
var users []User
db.NewSelect().Model(&users).Scan(ctx)
// Into maps
var userMaps []map[string]interface{}
db.NewSelect().Table("users").Scan(ctx, &userMaps)
// Into scalars
var count int
db.NewSelect().Table("users").ColumnExpr("COUNT(*)").Scan(ctx, &count)
// Into individual variables
var id int64
var name string
db.NewSelect().Table("users").Column("id", "name").Limit(1).Scan(ctx, &id, &name)
```
## 📊 Database Support
| Database | Driver | Dialect |
| ------------- | ------------------------------------------ | --------------------- |
| PostgreSQL | `github.com/uptrace/bun/driver/pgdriver` | `pgdialect.New()` |
| MySQL/MariaDB | `github.com/go-sql-driver/mysql` | `mysqldialect.New()` |
| SQLite | `github.com/uptrace/bun/driver/sqliteshim` | `sqlitedialect.New()` |
| SQL Server | `github.com/denisenkom/go-mssqldb` | `mssqldialect.New()` |
| Oracle | `github.com/sijms/go-ora/v2` | `oracledialect.New()` |
## 🔧 Advanced Features
### Table Relationships
Define complex relationships with struct tags:
```go
type User struct {
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Posts []Post `bun:"rel:has-many,join:id=user_id"`
Profile Profile `bun:"rel:has-one,join:id=user_id"`
}
type Post struct {
ID int64 `bun:",pk,autoincrement"`
Title string
UserID int64
User *User `bun:"rel:belongs-to,join:user_id=id"`
}
// Load users with their posts
var users []User
err := db.NewSelect().
Model(&users).
Relation("Posts").
Scan(ctx)
```
### Bulk Operations
Efficient bulk operations for large datasets:
```go
// Bulk insert
users := []User{{Name: "John"}, {Name: "Jane"}, {Name: "Bob"}}
_, err := db.NewInsert().Model(&users).Exec(ctx)
// Bulk update with CTE
_, err = db.NewUpdate().
Model(&users).
Set("updated_at = NOW()").
Where("active = ?", true).
Exec(ctx)
// Bulk delete
_, err = db.NewDelete().
Model((*User)(nil)).
Where("created_at < ?", time.Now().AddDate(-1, 0, 0)).
Exec(ctx)
```
### Migrations
Version your database schema:
```go
import "github.com/uptrace/bun/migrate"
migrations := migrate.NewMigrations()
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
_, err := db.NewCreateTable().Model((*User)(nil)).Exec(ctx)
return err
}, func(ctx context.Context, db *bun.DB) error {
_, err := db.NewDropTable().Model((*User)(nil)).Exec(ctx)
return err
})
migrator := migrate.NewMigrator(db, migrations)
err := migrator.Init(ctx)
err = migrator.Up(ctx)
```
## 📈 Monitoring & Observability
### Debug Queries
Enable query logging for development:
```go
import "github.com/uptrace/bun/extra/bundebug"
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
))
```
### OpenTelemetry Integration
Production-ready observability with distributed tracing:
```go
import "github.com/uptrace/bun/extra/bunotel"
db.AddQueryHook(bunotel.NewQueryHook(
bunotel.WithDBName("myapp"),
))
```
> **Monitoring made easy**: Bun is brought to you by ⭐
> [**uptrace/uptrace**](https://github.com/uptrace/uptrace). Uptrace is an open-source APM tool that
> supports distributed tracing, metrics, and logs. You can use it to monitor applications and set up
> automatic alerts to receive notifications via email, Slack, Telegram, and others.
>
> See [OpenTelemetry example](example/opentelemetry) which demonstrates how you can use Uptrace to
> monitor Bun.
## 📚 Documentation & Resources
- **[Getting Started Guide](https://bun.uptrace.dev/guide/golang-orm.html)** - Comprehensive
tutorial
- **[API Reference](https://pkg.go.dev/github.com/uptrace/bun)** - Complete package documentation
- **[Examples](https://github.com/uptrace/bun/tree/master/example)** - Working code samples
- **[Starter Kit](https://github.com/go-bun/bun-starter-kit)** - Production-ready template
- **[Community Discussions](https://github.com/uptrace/bun/discussions)** - Get help and share ideas
## 🤝 Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to
get started.
**Thanks to all our contributors:**
<a href="https://github.com/uptrace/bun/graphs/contributors">
<img src="https://contributors-img.web.app/image?repo=uptrace/bun" alt="Contributors" />
</a>
## 🔗 Related Projects
- **[Golang HTTP router](https://github.com/uptrace/bunrouter)** - Fast and flexible HTTP router
- **[Golang msgpack](https://github.com/vmihailenco/msgpack)** - High-performance MessagePack
serialization
---
<div align="center">
<strong>Star ⭐ this repo if you find Bun useful!</strong><br>
<sub>Join our community on <a href="https://discord.gg/rWtp5Aj">Discord</a> • Follow updates on <a href="https://github.com/uptrace/bun">GitHub</a></sub>
</div>
+248
View File
@@ -0,0 +1,248 @@
package bun
import (
"context"
"fmt"
"reflect"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
type (
// Safe marks a SQL fragment as trusted and prevents further escaping.
Safe = schema.Safe
// Name represents a SQL identifier such as a column or table name.
Name = schema.Name
// Ident is a fully qualified SQL identifier.
Ident = schema.Ident
// Order denotes sorting direction used in ORDER BY clauses.
Order = schema.Order
// NullTime is a nullable time value compatible with Bun.
NullTime = schema.NullTime
// BaseModel provides default metadata embedded into user models.
BaseModel = schema.BaseModel
// Query is implemented by all Bun query builders.
Query = schema.Query
// BeforeAppendModelHook is called before a model is appended to a query.
BeforeAppendModelHook = schema.BeforeAppendModelHook
// BeforeScanRowHook runs before scanning an individual row.
BeforeScanRowHook = schema.BeforeScanRowHook
// AfterScanRowHook runs after scanning an individual row.
AfterScanRowHook = schema.AfterScanRowHook
)
const (
// OrderAsc sorts values in ascending order.
OrderAsc = schema.OrderAsc
// OrderAscNullsFirst sorts ascending with NULL values first.
OrderAscNullsFirst = schema.OrderAscNullsFirst
// OrderAscNullsLast sorts ascending with NULL values last.
OrderAscNullsLast = schema.OrderAscNullsLast
// OrderDesc sorts values in descending order.
OrderDesc = schema.OrderDesc
// OrderDescNullsFirst sorts descending with NULL values first.
OrderDescNullsFirst = schema.OrderDescNullsFirst
// OrderDescNullsLast sorts descending with NULL values last.
OrderDescNullsLast = schema.OrderDescNullsLast
)
// SafeQuery wraps a raw query string and arguments and marks it safe for Bun.
func SafeQuery(query string, args ...any) schema.QueryWithArgs {
return schema.SafeQuery(query, args)
}
// BeforeSelectHook is invoked before executing SELECT queries.
type BeforeSelectHook interface {
BeforeSelect(ctx context.Context, query *SelectQuery) error
}
// AfterSelectHook is invoked after executing SELECT queries.
type AfterSelectHook interface {
AfterSelect(ctx context.Context, query *SelectQuery) error
}
// BeforeInsertHook is invoked before executing INSERT queries.
type BeforeInsertHook interface {
BeforeInsert(ctx context.Context, query *InsertQuery) error
}
// AfterInsertHook is invoked after executing INSERT queries.
type AfterInsertHook interface {
AfterInsert(ctx context.Context, query *InsertQuery) error
}
// BeforeUpdateHook is invoked before executing UPDATE queries.
type BeforeUpdateHook interface {
BeforeUpdate(ctx context.Context, query *UpdateQuery) error
}
// AfterUpdateHook is invoked after executing UPDATE queries.
type AfterUpdateHook interface {
AfterUpdate(ctx context.Context, query *UpdateQuery) error
}
// BeforeDeleteHook is invoked before executing DELETE queries.
type BeforeDeleteHook interface {
BeforeDelete(ctx context.Context, query *DeleteQuery) error
}
// AfterDeleteHook is invoked after executing DELETE queries.
type AfterDeleteHook interface {
AfterDelete(ctx context.Context, query *DeleteQuery) error
}
// BeforeCreateTableHook is invoked before executing CREATE TABLE queries.
type BeforeCreateTableHook interface {
BeforeCreateTable(ctx context.Context, query *CreateTableQuery) error
}
// AfterCreateTableHook is invoked after executing CREATE TABLE queries.
type AfterCreateTableHook interface {
AfterCreateTable(ctx context.Context, query *CreateTableQuery) error
}
// BeforeDropTableHook is invoked before executing DROP TABLE queries.
type BeforeDropTableHook interface {
BeforeDropTable(ctx context.Context, query *DropTableQuery) error
}
// AfterDropTableHook is invoked after executing DROP TABLE queries.
type AfterDropTableHook interface {
AfterDropTable(ctx context.Context, query *DropTableQuery) error
}
// SetLogger overwrites default Bun logger.
func SetLogger(logger internal.Logging) {
internal.SetLogger(logger)
}
// In wraps a slice so it can be used with the IN clause.
//
// Deprecated: Use bun.List or bun.Tuple instead.
func In(slice any) schema.QueryAppender {
return schema.In(slice)
}
// NullZero forces zero values to be treated as NULL when building queries.
func NullZero(value any) schema.QueryAppender {
return schema.NullZero(value)
}
//------------------------------------------------------------------------------
// ListValues formats a Go slice as a comma-separated SQL list (e.g., "1, 2, 3").
type ListValues struct {
slice any
}
var _ schema.QueryAppender = ListValues{}
// List creates a ListValues from a Go slice for use in SQL IN expressions.
func List(slice any) ListValues {
return ListValues{
slice: slice,
}
}
// AppendQuery appends the comma-separated list values to the byte slice.
func (in ListValues) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
v := reflect.ValueOf(in.slice)
if v.Kind() != reflect.Slice {
return nil, fmt.Errorf("ch: List(non-slice %T)", in.slice)
}
b = appendValues(gen, b, v)
return b, nil
}
func appendValues(gen schema.QueryGen, b []byte, slice reflect.Value) []byte {
sliceLen := slice.Len()
if sliceLen == 0 {
return append(b, "NULL"...)
}
for i := range sliceLen {
if i > 0 {
b = append(b, ", "...)
}
elem := slice.Index(i)
if elem.Kind() == reflect.Interface {
elem = elem.Elem()
}
b = gen.AppendValue(b, elem)
}
return b
}
//------------------------------------------------------------------------------
// TupleValues formats a Go slice as a parenthesized SQL tuple (e.g., "(1, 2, 3)").
type TupleValues struct {
slice any
}
var _ schema.QueryAppender = TupleValues{}
// Tuple creates a TupleValues from a slice for use in SQL expressions.
func Tuple(slice any) TupleValues {
return TupleValues{
slice: slice,
}
}
// AppendQuery appends the parenthesized tuple to the byte slice.
func (in TupleValues) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
v := reflect.ValueOf(in.slice)
if !v.IsValid() {
b = append(b, "(NULL)"...)
return b, nil
}
if v.Kind() != reflect.Slice {
return nil, fmt.Errorf("ch: Tuple(non-slice %T)", in.slice)
}
b = append(b, '(')
b = appendTuples(gen, b, v)
b = append(b, ')')
return b, nil
}
func appendTuples(gen schema.QueryGen, b []byte, slice reflect.Value) []byte {
sliceLen := slice.Len()
if sliceLen == 0 {
return append(b, "NULL"...)
}
for i := range sliceLen {
if i > 0 {
b = append(b, ", "...)
}
elem := slice.Index(i)
if elem.Kind() == reflect.Interface {
elem = elem.Elem()
}
switch elem.Kind() {
case reflect.Array, reflect.Slice:
if elem.Type().Elem().Kind() == reflect.Uint8 {
b = gen.AppendValue(b, elem)
} else {
b = append(b, '(')
b = appendTuples(gen, b, elem)
b = append(b, ')')
}
default:
b = gen.AppendValue(b, elem)
}
}
return b
}
+1
View File
@@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] }
+881
View File
@@ -0,0 +1,881 @@
package bun
import (
"context"
cryptorand "crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"reflect"
"strings"
"sync/atomic"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
const (
discardUnknownColumns internal.Flag = 1 << iota
)
// DBStats tracks aggregate query counters collected by Bun.
type DBStats struct {
Queries uint32
Errors uint32
}
// DBOption mutates DB configuration during construction.
type DBOption func(db *DB)
// WithOptions applies multiple DBOption values at once.
func WithOptions(opts ...DBOption) DBOption {
return func(db *DB) {
for _, opt := range opts {
opt(db)
}
}
}
// WithDiscardUnknownColumns ignores columns returned by queries that are not present in models.
func WithDiscardUnknownColumns() DBOption {
return func(db *DB) {
db.flags = db.flags.Set(discardUnknownColumns)
}
}
// ConnResolver enables routing queries to multiple databases.
type ConnResolver interface {
ResolveConn(ctx context.Context, query Query) IConn
Close() error
}
// WithConnResolver registers a connection resolver that chooses a connection per query.
func WithConnResolver(resolver ConnResolver) DBOption {
return func(db *DB) {
db.resolver = resolver
}
}
// DB is the central access point for building and executing Bun queries.
type DB struct {
// Must be a pointer so we copy the whole state, not individual fields.
*noCopyState
gen schema.QueryGen
queryHooks []QueryHook
}
// noCopyState contains DB fields that must not be copied on clone(),
// for example, it is forbidden to copy atomic.Pointer.
type noCopyState struct {
*sql.DB
dialect schema.Dialect
resolver ConnResolver
flags internal.Flag
closed atomic.Bool
stats DBStats
}
// NewDB wraps an existing *sql.DB with Bun using the given dialect and options.
func NewDB(sqldb *sql.DB, dialect schema.Dialect, opts ...DBOption) *DB {
dialect.Init(sqldb)
db := &DB{
noCopyState: &noCopyState{
DB: sqldb,
dialect: dialect,
},
gen: schema.NewQueryGen(dialect),
}
for _, opt := range opts {
opt(db)
}
return db
}
// String returns a string representation of the DB showing its dialect.
func (db *DB) String() string {
var b strings.Builder
b.WriteString("DB<dialect=")
b.WriteString(db.dialect.Name().String())
b.WriteString(">")
return b.String()
}
// Close closes the database connection and any registered connection resolver.
// It returns the first error encountered during closure.
func (db *DB) Close() error {
if db.closed.Swap(true) {
return nil
}
firstErr := db.DB.Close()
if db.resolver != nil {
if err := db.resolver.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
// DBStats returns aggregated query statistics including total queries and errors.
func (db *DB) DBStats() DBStats {
return DBStats{
Queries: atomic.LoadUint32(&db.stats.Queries),
Errors: atomic.LoadUint32(&db.stats.Errors),
}
}
// NewValues creates a VALUES query for inserting multiple rows efficiently.
func (db *DB) NewValues(model any) *ValuesQuery {
return NewValuesQuery(db, model)
}
// NewMerge creates a MERGE (UPSERT) query for insert-or-update operations.
func (db *DB) NewMerge() *MergeQuery {
return NewMergeQuery(db)
}
// NewSelect creates a SELECT query builder.
func (db *DB) NewSelect() *SelectQuery {
return NewSelectQuery(db)
}
// NewInsert creates an INSERT query builder.
func (db *DB) NewInsert() *InsertQuery {
return NewInsertQuery(db)
}
// NewUpdate creates an UPDATE query builder.
func (db *DB) NewUpdate() *UpdateQuery {
return NewUpdateQuery(db)
}
// NewDelete creates a DELETE query builder.
func (db *DB) NewDelete() *DeleteQuery {
return NewDeleteQuery(db)
}
// NewRaw creates a raw SQL query with the given query string and arguments.
func (db *DB) NewRaw(query string, args ...any) *RawQuery {
return NewRawQuery(db, query, args...)
}
// NewCreateTable creates a CREATE TABLE DDL query builder.
func (db *DB) NewCreateTable() *CreateTableQuery {
return NewCreateTableQuery(db)
}
// NewDropTable creates a DROP TABLE DDL query builder.
func (db *DB) NewDropTable() *DropTableQuery {
return NewDropTableQuery(db)
}
// NewCreateIndex creates a CREATE INDEX DDL query builder.
func (db *DB) NewCreateIndex() *CreateIndexQuery {
return NewCreateIndexQuery(db)
}
// NewDropIndex creates a DROP INDEX DDL query builder.
func (db *DB) NewDropIndex() *DropIndexQuery {
return NewDropIndexQuery(db)
}
// NewTruncateTable creates a TRUNCATE TABLE DDL query builder.
func (db *DB) NewTruncateTable() *TruncateTableQuery {
return NewTruncateTableQuery(db)
}
// NewAddColumn creates an ALTER TABLE ADD COLUMN DDL query builder.
func (db *DB) NewAddColumn() *AddColumnQuery {
return NewAddColumnQuery(db)
}
// NewDropColumn creates an ALTER TABLE DROP COLUMN DDL query builder.
func (db *DB) NewDropColumn() *DropColumnQuery {
return NewDropColumnQuery(db)
}
// ResetModel drops and recreates tables for the given models.
// This is useful for testing and development but should not be used in production.
func (db *DB) ResetModel(ctx context.Context, models ...any) error {
for _, model := range models {
if _, err := db.NewDropTable().Model(model).IfExists().Cascade().Exec(ctx); err != nil {
return err
}
if _, err := db.NewCreateTable().Model(model).Exec(ctx); err != nil {
return err
}
}
return nil
}
// Dialect returns the database dialect being used (e.g., PostgreSQL, MySQL, SQLite).
func (db *DB) Dialect() schema.Dialect {
return db.dialect
}
// ScanRows scans all rows from the result set into the destination values.
// It closes the rows when complete.
func (db *DB) ScanRows(ctx context.Context, rows *sql.Rows, dest ...any) error {
defer rows.Close()
model, err := newModel(db, dest)
if err != nil {
return err
}
_, err = model.ScanRows(ctx, rows)
if err != nil {
return err
}
return rows.Err()
}
// ScanRow scans a single row from the result set into the destination values.
func (db *DB) ScanRow(ctx context.Context, rows *sql.Rows, dest ...any) error {
model, err := newModel(db, dest)
if err != nil {
return err
}
rs, ok := model.(rowScanner)
if !ok {
return fmt.Errorf("bun: %T does not support ScanRow", model)
}
return rs.ScanRow(ctx, rows)
}
// Table returns the schema table metadata for the given type.
func (db *DB) Table(typ reflect.Type) *schema.Table {
return db.dialect.Tables().Get(typ)
}
// RegisterModel registers models by name so they can be referenced in table relations
// and fixtures.
func (db *DB) RegisterModel(models ...any) {
db.dialect.Tables().Register(models...)
}
// clone creates a shallow copy of the DB with independent query hooks.
func (db *DB) clone() *DB {
clone := *db
l := len(clone.queryHooks)
clone.queryHooks = clone.queryHooks[:l:l]
return &clone
}
// WithNamedArg returns a copy of the DB with an additional named argument
// bound into its query generator. Named arguments can later be referenced
// in SQL queries using placeholders (e.g. ?name). This method does not
// mutate the original DB instance but instead creates a cloned copy.
func (db *DB) WithNamedArg(name string, value any) *DB {
clone := db.clone()
clone.gen = clone.gen.WithNamedArg(name, value)
return clone
}
// QueryGen returns the query generator used for formatting SQL queries.
func (db *DB) QueryGen() schema.QueryGen {
return db.gen
}
type queryHookIniter interface {
Init(db *DB)
}
// WithQueryHook returns a copy of the DB with the provided query hook
// attached. A query hook allows inspection or modification of queries
// before/after execution (e.g. for logging, tracing, metrics).
// If the hook implements queryHookIniter, its Init method is invoked
// with the current DB before cloning. Like other modifiers, this
// method leaves the original DB unmodified.
func (db *DB) WithQueryHook(hook QueryHook) *DB {
if initer, ok := hook.(queryHookIniter); ok {
initer.Init(db)
}
clone := db.clone()
clone.queryHooks = append(clone.queryHooks, hook)
return clone
}
// DEPRECATED: use WithQueryHook instead
func (db *DB) AddQueryHook(hook QueryHook) {
if initer, ok := hook.(queryHookIniter); ok {
initer.Init(db)
}
db.queryHooks = append(db.queryHooks, hook)
}
// DEPRECATED: use WithQueryHook instead
func (db *DB) ResetQueryHooks() {
for i := range db.queryHooks {
db.queryHooks[i] = nil
}
db.queryHooks = nil
}
// UpdateFQN returns a fully qualified column name. For MySQL, it returns the column name with
// the table alias. For other RDBMS, it returns just the column name.
func (db *DB) UpdateFQN(alias, column string) Ident {
if db.HasFeature(feature.UpdateMultiTable) {
return Ident(alias + "." + column)
}
return Ident(column)
}
// HasFeature uses feature package to report whether the underlying DBMS supports this feature.
func (db *DB) HasFeature(feat feature.Feature) bool {
return db.dialect.Features().Has(feat)
}
//------------------------------------------------------------------------------
// Exec executes a query without returning rows using a background context.
// Arguments are formatted using the dialect's placeholder syntax.
func (db *DB) Exec(query string, args ...any) (sql.Result, error) {
return db.ExecContext(context.Background(), query, args...)
}
// ExecContext executes a query without returning rows.
// Arguments are formatted using the dialect's placeholder syntax.
// Query hooks are invoked before and after execution.
func (db *DB) ExecContext(
ctx context.Context, query string, args ...any,
) (sql.Result, error) {
formattedQuery := db.format(query, args)
ctx, event := db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
res, err := db.DB.ExecContext(ctx, formattedQuery)
db.afterQuery(ctx, event, res, err)
return res, err
}
// Query executes a query returning rows using a background context.
// Arguments are formatted using the dialect's placeholder syntax.
func (db *DB) Query(query string, args ...any) (*sql.Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
// QueryContext executes a query returning rows.
// Arguments are formatted using the dialect's placeholder syntax.
// Query hooks are invoked before and after execution.
func (db *DB) QueryContext(
ctx context.Context, query string, args ...any,
) (*sql.Rows, error) {
formattedQuery := db.format(query, args)
ctx, event := db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
rows, err := db.DB.QueryContext(ctx, formattedQuery)
db.afterQuery(ctx, event, nil, err)
return rows, err
}
// QueryRow executes a query expected to return at most one row using a background context.
// Arguments are formatted using the dialect's placeholder syntax.
func (db *DB) QueryRow(query string, args ...any) *sql.Row {
return db.QueryRowContext(context.Background(), query, args...)
}
// QueryRowContext executes a query expected to return at most one row.
// Arguments are formatted using the dialect's placeholder syntax.
// Query hooks are invoked before and after execution.
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
formattedQuery := db.format(query, args)
ctx, event := db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
row := db.DB.QueryRowContext(ctx, formattedQuery)
db.afterQuery(ctx, event, nil, row.Err())
return row
}
func (db *DB) format(query string, args []any) string {
return db.gen.FormatQuery(query, args...)
}
//------------------------------------------------------------------------------
// Conn wraps *sql.Conn so queries continue to use Bun features and hooks.
type Conn struct {
db *DB
*sql.Conn
}
// Conn returns a Conn wrapping a dedicated *sql.Conn from the connection pool.
// Query hooks and dialect features remain active on the returned connection.
func (db *DB) Conn(ctx context.Context) (Conn, error) {
conn, err := db.DB.Conn(ctx)
if err != nil {
return Conn{}, err
}
return Conn{
db: db,
Conn: conn,
}, nil
}
// ExecContext executes a query without returning rows on this connection.
func (c Conn) ExecContext(
ctx context.Context, query string, args ...any,
) (sql.Result, error) {
formattedQuery := c.db.format(query, args)
ctx, event := c.db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
res, err := c.Conn.ExecContext(ctx, formattedQuery)
c.db.afterQuery(ctx, event, res, err)
return res, err
}
// QueryContext executes a query returning rows on this connection.
func (c Conn) QueryContext(
ctx context.Context, query string, args ...any,
) (*sql.Rows, error) {
formattedQuery := c.db.format(query, args)
ctx, event := c.db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
rows, err := c.Conn.QueryContext(ctx, formattedQuery)
c.db.afterQuery(ctx, event, nil, err)
return rows, err
}
// QueryRowContext executes a query expected to return at most one row on this connection.
func (c Conn) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
formattedQuery := c.db.format(query, args)
ctx, event := c.db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
row := c.Conn.QueryRowContext(ctx, formattedQuery)
c.db.afterQuery(ctx, event, nil, row.Err())
return row
}
// Dialect returns the database dialect for this connection.
func (c Conn) Dialect() schema.Dialect {
return c.db.Dialect()
}
// NewValues creates a VALUES query bound to this connection.
func (c Conn) NewValues(model any) *ValuesQuery {
return NewValuesQuery(c.db, model).Conn(c)
}
// NewMerge creates a MERGE query bound to this connection.
func (c Conn) NewMerge() *MergeQuery {
return NewMergeQuery(c.db).Conn(c)
}
// NewSelect creates a SELECT query bound to this connection.
func (c Conn) NewSelect() *SelectQuery {
return NewSelectQuery(c.db).Conn(c)
}
// NewInsert creates an INSERT query bound to this connection.
func (c Conn) NewInsert() *InsertQuery {
return NewInsertQuery(c.db).Conn(c)
}
// NewUpdate creates an UPDATE query bound to this connection.
func (c Conn) NewUpdate() *UpdateQuery {
return NewUpdateQuery(c.db).Conn(c)
}
// NewDelete creates a DELETE query bound to this connection.
func (c Conn) NewDelete() *DeleteQuery {
return NewDeleteQuery(c.db).Conn(c)
}
// NewRaw creates a raw SQL query bound to this connection.
func (c Conn) NewRaw(query string, args ...any) *RawQuery {
return NewRawQuery(c.db, query, args...).Conn(c)
}
// NewCreateTable creates a CREATE TABLE query bound to this connection.
func (c Conn) NewCreateTable() *CreateTableQuery {
return NewCreateTableQuery(c.db).Conn(c)
}
// NewDropTable creates a DROP TABLE query bound to this connection.
func (c Conn) NewDropTable() *DropTableQuery {
return NewDropTableQuery(c.db).Conn(c)
}
// NewCreateIndex creates a CREATE INDEX query bound to this connection.
func (c Conn) NewCreateIndex() *CreateIndexQuery {
return NewCreateIndexQuery(c.db).Conn(c)
}
// NewDropIndex creates a DROP INDEX query bound to this connection.
func (c Conn) NewDropIndex() *DropIndexQuery {
return NewDropIndexQuery(c.db).Conn(c)
}
// NewTruncateTable creates a TRUNCATE TABLE query bound to this connection.
func (c Conn) NewTruncateTable() *TruncateTableQuery {
return NewTruncateTableQuery(c.db).Conn(c)
}
// NewAddColumn creates an ALTER TABLE ADD COLUMN query bound to this connection.
func (c Conn) NewAddColumn() *AddColumnQuery {
return NewAddColumnQuery(c.db).Conn(c)
}
// NewDropColumn creates an ALTER TABLE DROP COLUMN query bound to this connection.
func (c Conn) NewDropColumn() *DropColumnQuery {
return NewDropColumnQuery(c.db).Conn(c)
}
// RunInTx runs the function in a transaction. If the function returns an error,
// the transaction is rolled back. Otherwise, the transaction is committed.
func (c Conn) RunInTx(
ctx context.Context, opts *sql.TxOptions, fn func(ctx context.Context, tx Tx) error,
) error {
tx, err := c.BeginTx(ctx, opts)
if err != nil {
return err
}
var done bool
defer func() {
if !done {
_ = tx.Rollback()
}
}()
if err := fn(ctx, tx); err != nil {
return err
}
done = true
return tx.Commit()
}
// BeginTx starts a transaction on this connection with the given options.
func (c Conn) BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) {
ctx, event := c.db.beforeQuery(ctx, nil, "BEGIN", nil, "BEGIN", nil)
tx, err := c.Conn.BeginTx(ctx, opts)
c.db.afterQuery(ctx, event, nil, err)
if err != nil {
return Tx{}, err
}
return Tx{
ctx: ctx,
db: c.db,
Tx: tx,
}, nil
}
//------------------------------------------------------------------------------
// Stmt wraps *sql.Stmt so prepared statements participate in Bun logging.
type Stmt struct {
*sql.Stmt
}
// Prepare creates a prepared statement using a background context.
func (db *DB) Prepare(query string) (Stmt, error) {
return db.PrepareContext(context.Background(), query)
}
// PrepareContext creates a prepared statement for repeated execution.
func (db *DB) PrepareContext(ctx context.Context, query string) (Stmt, error) {
stmt, err := db.DB.PrepareContext(ctx, query)
if err != nil {
return Stmt{}, err
}
return Stmt{Stmt: stmt}, nil
}
//------------------------------------------------------------------------------
// Tx wraps *sql.Tx and preserves Bun-specific context such as hooks and dialect.
type Tx struct {
ctx context.Context
db *DB
// name is the name of a savepoint
name string
*sql.Tx
}
// RunInTx runs the function in a transaction. If the function returns an error,
// the transaction is rolled back. Otherwise, the transaction is committed.
func (db *DB) RunInTx(
ctx context.Context, opts *sql.TxOptions, fn func(ctx context.Context, tx Tx) error,
) error {
tx, err := db.BeginTx(ctx, opts)
if err != nil {
return err
}
var done bool
defer func() {
if !done {
_ = tx.Rollback()
}
}()
if err := fn(ctx, tx); err != nil {
return err
}
done = true
return tx.Commit()
}
// Begin starts a transaction with default options using a background context.
func (db *DB) Begin() (Tx, error) {
return db.BeginTx(context.Background(), nil)
}
// BeginTx starts a transaction with the given options.
func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) {
ctx, event := db.beforeQuery(ctx, nil, "BEGIN", nil, "BEGIN", nil)
tx, err := db.DB.BeginTx(ctx, opts)
db.afterQuery(ctx, event, nil, err)
if err != nil {
return Tx{}, err
}
return Tx{
ctx: ctx,
db: db,
Tx: tx,
}, nil
}
// Commit commits the transaction or releases the savepoint if this is a nested transaction.
func (tx Tx) Commit() error {
if tx.name == "" {
return tx.commitTX()
}
return tx.commitSP()
}
func (tx Tx) commitTX() error {
ctx, event := tx.db.beforeQuery(tx.ctx, nil, "COMMIT", nil, "COMMIT", nil)
err := tx.Tx.Commit()
tx.db.afterQuery(ctx, event, nil, err)
return err
}
func (tx Tx) commitSP() error {
if tx.db.HasFeature(feature.MSSavepoint) {
return nil
}
query := "RELEASE SAVEPOINT " + tx.name
_, err := tx.ExecContext(tx.ctx, query)
return err
}
// Rollback rolls back the transaction or rolls back to the savepoint if this is a nested transaction.
func (tx Tx) Rollback() error {
if tx.name == "" {
return tx.rollbackTX()
}
return tx.rollbackSP()
}
func (tx Tx) rollbackTX() error {
ctx, event := tx.db.beforeQuery(tx.ctx, nil, "ROLLBACK", nil, "ROLLBACK", nil)
err := tx.Tx.Rollback()
tx.db.afterQuery(ctx, event, nil, err)
return err
}
func (tx Tx) rollbackSP() error {
query := "ROLLBACK TO SAVEPOINT " + tx.name
if tx.db.HasFeature(feature.MSSavepoint) {
query = "ROLLBACK TRANSACTION " + tx.name
}
_, err := tx.ExecContext(tx.ctx, query)
return err
}
// Exec executes a query without returning rows within this transaction.
func (tx Tx) Exec(query string, args ...any) (sql.Result, error) {
return tx.ExecContext(context.TODO(), query, args...)
}
// ExecContext executes a query without returning rows within this transaction.
func (tx Tx) ExecContext(
ctx context.Context, query string, args ...any,
) (sql.Result, error) {
formattedQuery := tx.db.format(query, args)
ctx, event := tx.db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
res, err := tx.Tx.ExecContext(ctx, formattedQuery)
tx.db.afterQuery(ctx, event, res, err)
return res, err
}
// Query executes a query returning rows within this transaction.
func (tx Tx) Query(query string, args ...any) (*sql.Rows, error) {
return tx.QueryContext(context.TODO(), query, args...)
}
// QueryContext executes a query returning rows within this transaction.
func (tx Tx) QueryContext(
ctx context.Context, query string, args ...any,
) (*sql.Rows, error) {
formattedQuery := tx.db.format(query, args)
ctx, event := tx.db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
rows, err := tx.Tx.QueryContext(ctx, formattedQuery)
tx.db.afterQuery(ctx, event, nil, err)
return rows, err
}
// QueryRow executes a query expected to return at most one row within this transaction.
func (tx Tx) QueryRow(query string, args ...any) *sql.Row {
return tx.QueryRowContext(context.TODO(), query, args...)
}
// QueryRowContext executes a query expected to return at most one row within this transaction.
func (tx Tx) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
formattedQuery := tx.db.format(query, args)
ctx, event := tx.db.beforeQuery(ctx, nil, query, args, formattedQuery, nil)
row := tx.Tx.QueryRowContext(ctx, formattedQuery)
tx.db.afterQuery(ctx, event, nil, row.Err())
return row
}
//------------------------------------------------------------------------------
// Begin creates a savepoint, effectively starting a nested transaction.
func (tx Tx) Begin() (Tx, error) {
return tx.BeginTx(tx.ctx, nil)
}
// BeginTx will save a point in the running transaction.
func (tx Tx) BeginTx(ctx context.Context, _ *sql.TxOptions) (Tx, error) {
// mssql savepoint names are limited to 32 characters
sp := make([]byte, 14)
_, err := cryptorand.Read(sp)
if err != nil {
return Tx{}, err
}
qName := "SP_" + hex.EncodeToString(sp)
query := "SAVEPOINT " + qName
if tx.db.HasFeature(feature.MSSavepoint) {
query = "SAVE TRANSACTION " + qName
}
_, err = tx.ExecContext(ctx, query)
if err != nil {
return Tx{}, err
}
return Tx{
ctx: ctx,
db: tx.db,
Tx: tx.Tx,
name: qName,
}, nil
}
// RunInTx creates a savepoint and runs the function within that savepoint.
// If the function returns an error, the savepoint is rolled back.
func (tx Tx) RunInTx(
ctx context.Context, _ *sql.TxOptions, fn func(ctx context.Context, tx Tx) error,
) error {
sp, err := tx.BeginTx(ctx, nil)
if err != nil {
return err
}
var done bool
defer func() {
if !done {
_ = sp.Rollback()
}
}()
if err := fn(ctx, sp); err != nil {
return err
}
done = true
return sp.Commit()
}
// Dialect returns the database dialect for this transaction.
func (tx Tx) Dialect() schema.Dialect {
return tx.db.Dialect()
}
// NewValues creates a VALUES query bound to this transaction.
func (tx Tx) NewValues(model any) *ValuesQuery {
return NewValuesQuery(tx.db, model).Conn(tx)
}
// NewMerge creates a MERGE query bound to this transaction.
func (tx Tx) NewMerge() *MergeQuery {
return NewMergeQuery(tx.db).Conn(tx)
}
// NewSelect creates a SELECT query bound to this transaction.
func (tx Tx) NewSelect() *SelectQuery {
return NewSelectQuery(tx.db).Conn(tx)
}
// NewInsert creates an INSERT query bound to this transaction.
func (tx Tx) NewInsert() *InsertQuery {
return NewInsertQuery(tx.db).Conn(tx)
}
// NewUpdate creates an UPDATE query bound to this transaction.
func (tx Tx) NewUpdate() *UpdateQuery {
return NewUpdateQuery(tx.db).Conn(tx)
}
// NewDelete creates a DELETE query bound to this transaction.
func (tx Tx) NewDelete() *DeleteQuery {
return NewDeleteQuery(tx.db).Conn(tx)
}
// NewRaw creates a raw SQL query bound to this transaction.
func (tx Tx) NewRaw(query string, args ...any) *RawQuery {
return NewRawQuery(tx.db, query, args...).Conn(tx)
}
// NewCreateTable creates a CREATE TABLE query bound to this transaction.
func (tx Tx) NewCreateTable() *CreateTableQuery {
return NewCreateTableQuery(tx.db).Conn(tx)
}
// NewDropTable creates a DROP TABLE query bound to this transaction.
func (tx Tx) NewDropTable() *DropTableQuery {
return NewDropTableQuery(tx.db).Conn(tx)
}
// NewCreateIndex creates a CREATE INDEX query bound to this transaction.
func (tx Tx) NewCreateIndex() *CreateIndexQuery {
return NewCreateIndexQuery(tx.db).Conn(tx)
}
// NewDropIndex creates a DROP INDEX query bound to this transaction.
func (tx Tx) NewDropIndex() *DropIndexQuery {
return NewDropIndexQuery(tx.db).Conn(tx)
}
// NewTruncateTable creates a TRUNCATE TABLE query bound to this transaction.
func (tx Tx) NewTruncateTable() *TruncateTableQuery {
return NewTruncateTableQuery(tx.db).Conn(tx)
}
// NewAddColumn creates an ALTER TABLE ADD COLUMN query bound to this transaction.
func (tx Tx) NewAddColumn() *AddColumnQuery {
return NewAddColumnQuery(tx.db).Conn(tx)
}
// NewDropColumn creates an ALTER TABLE DROP COLUMN query bound to this transaction.
func (tx Tx) NewDropColumn() *DropColumnQuery {
return NewDropColumnQuery(tx.db).Conn(tx)
}
func (db *DB) makeQueryBytes() []byte {
return internal.MakeQueryBytes()
}
+105
View File
@@ -0,0 +1,105 @@
package dialect
import (
"math"
"strconv"
"github.com/uptrace/bun/internal"
)
func AppendError(b []byte, err error) []byte {
b = append(b, "?!("...)
b = append(b, err.Error()...)
b = append(b, ')')
return b
}
func AppendNull(b []byte) []byte {
return append(b, "NULL"...)
}
func AppendBool(b []byte, v bool) []byte {
if v {
return append(b, "TRUE"...)
}
return append(b, "FALSE"...)
}
func AppendFloat32(b []byte, num float32) []byte {
return appendFloat(b, float64(num), 32)
}
func AppendFloat64(b []byte, num float64) []byte {
return appendFloat(b, num, 64)
}
func appendFloat(b []byte, num float64, bitSize int) []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, bitSize)
}
}
//------------------------------------------------------------------------------
func AppendName(b []byte, ident string, quote byte) []byte {
return appendName(b, internal.Bytes(ident), quote)
}
func appendName(b, ident []byte, quote byte) []byte {
b = append(b, quote)
for _, c := range ident {
if c == quote {
b = append(b, quote, quote)
} else {
b = append(b, c)
}
}
b = append(b, quote)
return b
}
func AppendIdent(b []byte, name string, quote byte) []byte {
return appendIdent(b, internal.Bytes(name), quote)
}
func appendIdent(b, name []byte, quote byte) []byte {
var quoted bool
loop:
for _, c := range name {
switch c {
case '*':
if !quoted {
b = append(b, '*')
continue loop
}
case '.':
if quoted {
b = append(b, quote)
quoted = false
}
b = append(b, '.')
continue loop
}
if !quoted {
b = append(b, quote)
quoted = true
}
if c == quote {
b = append(b, quote, quote)
} else {
b = append(b, c)
}
}
if quoted {
b = append(b, quote)
}
return b
}
+31
View File
@@ -0,0 +1,31 @@
package dialect
type Name int
func (n Name) String() string {
switch n {
case Invalid:
return "invalid"
case PG:
return "pg"
case SQLite:
return "sqlite"
case MySQL:
return "mysql"
case MSSQL:
return "mssql"
case Oracle:
return "oracle"
default:
return "custom"
}
}
const (
Invalid Name = iota
PG
SQLite
MySQL
MSSQL
Oracle
)
+187
View File
@@ -0,0 +1,187 @@
// Package feature defines flags that represent optional SQL dialect capabilities.
// Each dialect (PostgreSQL, MySQL, SQLite, MSSQL) declares which features it supports
// by combining flags with the bitwise OR operator.
package feature
import (
"fmt"
"strconv"
"github.com/uptrace/bun/internal"
)
// Feature is a bit flag representing an optional SQL dialect capability.
type Feature = internal.Flag
const (
// Common query features.
// CTE enables Common Table Expressions (WITH ... AS ...) syntax.
CTE Feature = 1 << iota
// WithValues enables WITH ... (VALUES ...) syntax for inline value lists.
WithValues
// Returning enables the RETURNING clause to return rows affected by DML statements.
Returning
// Output enables the OUTPUT clause, the MSSQL equivalent of RETURNING.
Output
// DefaultPlaceholder enables the DEFAULT keyword as a placeholder in INSERT value lists.
DefaultPlaceholder
// DoubleColonCast enables PostgreSQL-style :: type cast syntax.
DoubleColonCast
// ValuesRow enables VALUES ROW(...) syntax.
ValuesRow
// CompositeIn enables WHERE (A, B) IN ((N, NN), (N, NN), ...) composite comparison syntax.
CompositeIn
// SELECT features.
// OffsetFetch enables OFFSET ... FETCH NEXT syntax (MSSQL).
OffsetFetch
// SelectExists enables EXISTS subquery expressions.
SelectExists
// INSERT features.
// InsertReturning enables INSERT ... RETURNING syntax.
InsertReturning
// InsertTableAlias enables table alias support in INSERT statements.
InsertTableAlias
// InsertOnConflict enables INSERT ... ON CONFLICT syntax (PostgreSQL, SQLite).
InsertOnConflict
// InsertOnDuplicateKey enables INSERT ... ON DUPLICATE KEY syntax (MySQL).
InsertOnDuplicateKey
// InsertIgnore enables INSERT IGNORE syntax to silently skip conflicting rows (MySQL).
InsertIgnore
// UPDATE features.
// UpdateFromTable enables UPDATE ... FROM ... syntax for joining tables in updates.
UpdateFromTable
// UpdateMultiTable enables multi-table UPDATE syntax (MySQL).
UpdateMultiTable
// UpdateTableAlias enables table alias support in UPDATE statements.
UpdateTableAlias
// UpdateOrderLimit enables UPDATE ... ORDER BY ... LIMIT syntax.
UpdateOrderLimit
// DELETE features.
// DeleteReturning enables DELETE ... RETURNING syntax.
DeleteReturning
// DeleteTableAlias enables table alias support in DELETE statements.
DeleteTableAlias
// DeleteOrderLimit enables DELETE ... ORDER BY ... LIMIT syntax.
DeleteOrderLimit
// MERGE features.
// Merge enables MERGE ... USING ... ON ... WHEN syntax for upsert operations.
Merge
// MergeReturning enables MERGE ... RETURNING syntax.
MergeReturning
// Table DDL features.
// TableCascade enables CASCADE support for DROP TABLE and related operations.
TableCascade
// TableIdentity enables table-level IDENTITY property (MSSQL).
TableIdentity
// TableTruncate enables TRUNCATE TABLE support.
TableTruncate
// TableNotExists enables IF NOT EXISTS / IF EXISTS syntax for CREATE TABLE and DROP TABLE.
TableNotExists
// AlterColumnExists enables ADD/DROP COLUMN IF NOT EXISTS / IF EXISTS syntax.
AlterColumnExists
// CreateIndexIfNotExists enables CREATE INDEX IF NOT EXISTS syntax.
CreateIndexIfNotExists
// Column definition features.
// AutoIncrement enables AUTO_INCREMENT syntax for auto-generated columns (MySQL).
AutoIncrement
// Identity enables IDENTITY column syntax for auto-generated columns (MSSQL).
Identity
// GeneratedIdentity enables GENERATED ALWAYS AS IDENTITY syntax (PostgreSQL).
GeneratedIdentity
// Dialect-specific features.
// FKDefaultOnAction indicates that FK ON UPDATE/ON DELETE has the default value NO ACTION.
FKDefaultOnAction
// MSSavepoint enables Microsoft SQL Server savepoint support.
MSSavepoint
)
// NotSupportError is returned when an operation requires a feature
// that the current dialect does not support.
type NotSupportError struct {
Flag Feature
}
func (err *NotSupportError) Error() string {
name, ok := flag2str[err.Flag]
if !ok {
name = strconv.FormatInt(int64(err.Flag), 10)
}
return fmt.Sprintf("bun: feature %s is not supported by current dialect", name)
}
// NewNotSupportError returns a NotSupportError for the given feature flag.
func NewNotSupportError(flag Feature) *NotSupportError {
return &NotSupportError{Flag: flag}
}
var flag2str = map[Feature]string{
// Common query features.
CTE: "CTE",
WithValues: "WithValues",
Returning: "Returning",
Output: "Output",
DefaultPlaceholder: "DefaultPlaceholder",
DoubleColonCast: "DoubleColonCast",
ValuesRow: "ValuesRow",
CompositeIn: "CompositeIn",
// SELECT features.
OffsetFetch: "OffsetFetch",
SelectExists: "SelectExists",
// INSERT features.
InsertReturning: "InsertReturning",
InsertTableAlias: "InsertTableAlias",
InsertOnConflict: "InsertOnConflict",
InsertOnDuplicateKey: "InsertOnDuplicateKey",
InsertIgnore: "InsertIgnore",
// UPDATE features.
UpdateFromTable: "UpdateFromTable",
UpdateMultiTable: "UpdateMultiTable",
UpdateTableAlias: "UpdateTableAlias",
UpdateOrderLimit: "UpdateOrderLimit",
// DELETE features.
DeleteReturning: "DeleteReturning",
DeleteTableAlias: "DeleteTableAlias",
DeleteOrderLimit: "DeleteOrderLimit",
// MERGE features.
Merge: "Merge",
MergeReturning: "MergeReturning",
// Table DDL features.
TableCascade: "TableCascade",
TableIdentity: "TableIdentity",
TableTruncate: "TableTruncate",
TableNotExists: "TableNotExists",
AlterColumnExists: "AlterColumnExists",
CreateIndexIfNotExists: "CreateIndexIfNotExists",
// Column definition features.
AutoIncrement: "AutoIncrement",
Identity: "Identity",
GeneratedIdentity: "GeneratedIdentity",
// Dialect-specific features.
FKDefaultOnAction: "FKDefaultOnAction",
MSSavepoint: "MSSavepoint",
}
+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.
+186
View File
@@ -0,0 +1,186 @@
package mssqldialect
import (
"database/sql"
"encoding/hex"
"fmt"
"log"
"strconv"
"strings"
"time"
"golang.org/x/mod/semver"
"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/schema"
)
const (
datetimeType = "DATETIME"
bitType = "BIT"
nvarcharType = "NVARCHAR(MAX)"
varbinaryType = "VARBINARY(MAX)"
)
func init() {
if Version() != bun.Version() {
panic(fmt.Errorf("mssqldialect 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
unicode bool
}
func New(opts ...DialectOption) *Dialect {
d := new(Dialect)
d.tables = schema.NewTables(d)
d.features = feature.CTE |
feature.DefaultPlaceholder |
feature.Identity |
feature.Output |
feature.OffsetFetch |
feature.FKDefaultOnAction |
feature.UpdateFromTable |
feature.MSSavepoint
d.unicode = true
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 WithUnicode(on bool) DialectOption {
return func(d *Dialect) {
d.unicode = on
}
}
func (d *Dialect) Init(db *sql.DB) {
var version string
if err := db.QueryRow("SELECT @@VERSION").Scan(&version); err != nil {
log.Printf("can't discover MSSQL version: %s", err)
return
}
version = semver.MajorMinor("v" + cleanupVersion(version))
}
func cleanupVersion(v string) string {
if s := strings.Index(v, " - "); s != -1 {
if e := strings.Index(v[s+3:], " "); e != -1 {
return v[s+3 : s+3+e]
}
}
return ""
}
func (d *Dialect) Name() dialect.Name {
return dialect.MSSQL
}
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 {
field.DiscoveredSQLType = sqlType(field)
if strings.ToUpper(field.UserSQLType) == sqltype.JSON {
field.UserSQLType = nvarcharType
}
}
}
func (d *Dialect) IdentQuote() byte {
return '"'
}
func (*Dialect) AppendTime(b []byte, tm time.Time) []byte {
b = append(b, '\'')
b = tm.AppendFormat(b, "2006-01-02 15:04:05.999")
b = append(b, '\'')
return b
}
func (*Dialect) AppendBytes(b, bs []byte) []byte {
if bs == nil {
return dialect.AppendNull(b)
}
b = append(b, "0x"...)
s := len(b)
b = append(b, make([]byte, hex.EncodedLen(len(bs)))...)
hex.Encode(b[s:], bs)
return b
}
func (*Dialect) AppendBool(b []byte, v bool) []byte {
num := 0
if v {
num = 1
}
return strconv.AppendUint(b, uint64(num), 10)
}
func (d *Dialect) AppendString(b []byte, s string) []byte {
if d.unicode {
// 'N' prefix means the string uses Unicode encoding.
b = append(b, 'N')
}
return d.BaseDialect.AppendString(b, s)
}
func (d *Dialect) AppendSequence(b []byte, _ *schema.Table, _ *schema.Field) []byte {
return append(b, " IDENTITY"...)
}
func (*Dialect) DefaultVarcharLen() int {
return 255
}
func (*Dialect) DefaultSchema() string {
return "dbo"
}
func sqlType(field *schema.Field) string {
switch field.DiscoveredSQLType {
case sqltype.Timestamp:
return datetimeType
case sqltype.Boolean:
return bitType
case sqltype.JSON:
return nvarcharType
case sqltype.Blob:
return varbinaryType
}
return field.DiscoveredSQLType
}
+6
View File
@@ -0,0 +1,6 @@
package mssqldialect
// Version is the current release version.
func Version() string {
return "1.2.16"
}
+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
}
}
}
+161
View File
@@ -0,0 +1,161 @@
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.MergeReturning |
feature.AlterColumnExists
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.In() is called with an empty slice.
exclude = []string{""}
}
var tables []*InformationSchemaTable
if err := in.db.NewRaw(sqlInspectTables, in.SchemaName, bun.In(exclude)).Scan(ctx, &tables); err != nil {
return dbSchema, err
}
var fks []*ForeignKey
if err := in.db.NewRaw(sqlInspectForeignKeys, in.SchemaName, bun.In(exclude), bun.In(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.In([]string{...}) to exclude tables from this inspection or bun.In([]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.In([]string{...}) to exclude tables from this inspection or bun.In([]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.16"
}
+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.
+149
View File
@@ -0,0 +1,149 @@
package sqlitedialect
import (
"database/sql"
"encoding/hex"
"fmt"
"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/schema"
)
func init() {
if Version() != bun.Version() {
panic(fmt.Errorf("sqlitedialect 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
}
func New(opts ...DialectOption) *Dialect {
d := new(Dialect)
d.tables = schema.NewTables(d)
d.features = feature.CTE |
feature.WithValues |
feature.Returning |
feature.InsertReturning |
feature.InsertTableAlias |
feature.UpdateTableAlias |
feature.DeleteTableAlias |
feature.InsertOnConflict |
feature.TableNotExists |
feature.SelectExists |
feature.AutoIncrement |
feature.CompositeIn |
feature.FKDefaultOnAction |
feature.DeleteReturning
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 (d *Dialect) Init(*sql.DB) {}
func (d *Dialect) Name() dialect.Name {
return dialect.SQLite
}
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)
}
func (d *Dialect) IdentQuote() byte {
return '"'
}
func (d *Dialect) AppendBytes(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 (*Dialect) DefaultVarcharLen() int {
return 0
}
// AppendSequence adds AUTOINCREMENT keyword to the column definition. As per [documentation],
// AUTOINCREMENT is only valid for INTEGER PRIMARY KEY, and this method will be a noop for other columns.
//
// Because this is a valid construct:
//
// CREATE TABLE ("id" INTEGER PRIMARY KEY AUTOINCREMENT);
//
// and this is not:
//
// CREATE TABLE ("id" INTEGER AUTOINCREMENT, PRIMARY KEY ("id"));
//
// AppendSequence adds a primary key constraint as a *side-effect*. Callers should expect it to avoid building invalid SQL.
// SQLite also [does not support] AUTOINCREMENT column in composite primary keys.
//
// [documentation]: https://www.sqlite.org/autoinc.html
// [does not support]: https://stackoverflow.com/a/6793274/14726116
func (d *Dialect) AppendSequence(b []byte, table *schema.Table, field *schema.Field) []byte {
if field.IsPK && len(table.PKs) == 1 && field.CreateTableSQLType == sqltype.Integer {
b = append(b, " PRIMARY KEY AUTOINCREMENT"...)
}
return b
}
// DefaultSchemaName is the "schema-name" of the main database.
// The details might differ from other dialects, but for all means and purposes
// "main" is the default schema in an SQLite database.
func (*Dialect) DefaultSchema() string {
return "main"
}
func fieldSQLType(field *schema.Field) string {
switch field.DiscoveredSQLType {
case sqltype.SmallInt, sqltype.BigInt:
// INTEGER PRIMARY KEY is an alias for the ROWID.
// It is safe to convert all ints to INTEGER, because SQLite types don't have size.
return sqltype.Integer
default:
return field.DiscoveredSQLType
}
}
+6
View File
@@ -0,0 +1,6 @@
package sqlitedialect
// Version is the current release version.
func Version() string {
return "1.2.16"
}
+16
View File
@@ -0,0 +1,16 @@
package sqltype
const (
Boolean = "BOOLEAN"
SmallInt = "SMALLINT"
Integer = "INTEGER"
BigInt = "BIGINT"
Real = "REAL"
DoublePrecision = "DOUBLE PRECISION"
VarChar = "VARCHAR"
Blob = "BLOB"
Timestamp = "TIMESTAMP"
JSON = "JSON"
JSONB = "JSONB"
HSTORE = "HSTORE"
)
+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.
+38
View File
@@ -0,0 +1,38 @@
# pgdriver
[![PkgGoDev](https://pkg.go.dev/badge/github.com/uptrace/bun/driver/pgdriver)](https://pkg.go.dev/github.com/uptrace/bun/driver/pgdriver)
pgdriver is a database/sql driver for PostgreSQL based on [go-pg](https://github.com/go-pg/pg) code.
You can install it with:
```shell
go get github.com/uptrace/bun/driver/pgdriver
```
And then create a `sql.DB` using it:
```go
import _ "github.com/uptrace/bun/driver/pgdriver"
dsn := "postgres://postgres:@localhost:5432/test"
db, err := sql.Open("pg", dsn)
```
Alternatively:
```go
dsn := "postgres://postgres:@localhost:5432/test"
db := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
```
[Benchmark](https://github.com/go-bun/bun-benchmark):
```
BenchmarkInsert/pg-12 7254 148380 ns/op 900 B/op 13 allocs/op
BenchmarkInsert/pgx-12 6494 166391 ns/op 2076 B/op 26 allocs/op
BenchmarkSelect/pg-12 9100 132952 ns/op 1417 B/op 18 allocs/op
BenchmarkSelect/pgx-12 8199 154920 ns/op 3679 B/op 60 allocs/op
```
See [documentation](https://bun.uptrace.dev/postgres/) for more details.
+193
View File
@@ -0,0 +1,193 @@
package pgdriver
import (
"encoding/hex"
"fmt"
"io"
"strconv"
"strings"
"time"
)
const (
pgBool = 16
pgInt2 = 21
pgInt4 = 23
pgInt8 = 20
pgFloat4 = 700
pgFloat8 = 701
pgText = 25
pgVarchar = 1043
pgBytea = 17
pgDate = 1082
pgTimestamp = 1114
pgTimestamptz = 1184
)
func readColumnValue(rd *reader, dataType int32, dataLen int) (interface{}, error) {
if dataLen == -1 {
return nil, nil
}
switch dataType {
case pgBool:
return readBoolCol(rd, dataLen)
case pgInt2:
return readIntCol(rd, dataLen, 16)
case pgInt4:
return readIntCol(rd, dataLen, 32)
case pgInt8:
return readIntCol(rd, dataLen, 64)
case pgFloat4:
return readFloatCol(rd, dataLen, 32)
case pgFloat8:
return readFloatCol(rd, dataLen, 64)
case pgTimestamp:
return readTimeCol(rd, dataLen)
case pgTimestamptz:
return readTimeCol(rd, dataLen)
case pgDate:
// Return a string and let the scanner to convert string to time.Time if necessary.
return readStringCol(rd, dataLen)
case pgText, pgVarchar:
return readStringCol(rd, dataLen)
case pgBytea:
return readBytesCol(rd, dataLen)
}
b := make([]byte, dataLen)
if _, err := io.ReadFull(rd, b); err != nil {
return nil, err
}
return b, nil
}
func readBoolCol(rd *reader, n int) (interface{}, error) {
tmp, err := rd.ReadTemp(n)
if err != nil {
return nil, err
}
return len(tmp) == 1 && (tmp[0] == 't' || tmp[0] == '1'), nil
}
func readIntCol(rd *reader, n int, bitSize int) (interface{}, error) {
if n <= 0 {
return 0, nil
}
tmp, err := rd.ReadTemp(n)
if err != nil {
return 0, err
}
return strconv.ParseInt(bytesToString(tmp), 10, bitSize)
}
func readFloatCol(rd *reader, n int, bitSize int) (interface{}, error) {
if n <= 0 {
return 0, nil
}
tmp, err := rd.ReadTemp(n)
if err != nil {
return 0, err
}
return strconv.ParseFloat(bytesToString(tmp), bitSize)
}
func readStringCol(rd *reader, n int) (interface{}, error) {
if n <= 0 {
return "", nil
}
b := make([]byte, n)
if _, err := io.ReadFull(rd, b); err != nil {
return nil, err
}
return bytesToString(b), nil
}
func readBytesCol(rd *reader, n int) (interface{}, error) {
if n <= 0 {
return []byte{}, nil
}
tmp, err := rd.ReadTemp(n)
if err != nil {
return nil, err
}
if len(tmp) < 2 || tmp[0] != '\\' || tmp[1] != 'x' {
return nil, fmt.Errorf("pgdriver: can't parse bytea: %q", tmp)
}
tmp = tmp[2:] // Cut off "\x".
b := make([]byte, hex.DecodedLen(len(tmp)))
if _, err := hex.Decode(b, tmp); err != nil {
return nil, err
}
return b, nil
}
func readTimeCol(rd *reader, n int) (interface{}, error) {
if n <= 0 {
return time.Time{}, nil
}
tmp, err := rd.ReadTemp(n)
if err != nil {
return time.Time{}, err
}
tm, err := ParseTime(bytesToString(tmp))
if err != nil {
return time.Time{}, err
}
return tm, nil
}
const (
dateFormat = "2006-01-02"
timeFormat = "15:04:05.999999999"
timestampFormat = "2006-01-02 15:04:05.999999999"
timestamptzFormat = "2006-01-02 15:04:05.999999999-07:00:00"
timestamptzFormat2 = "2006-01-02 15:04:05.999999999-07:00"
timestamptzFormat3 = "2006-01-02 15:04:05.999999999-07"
)
func ParseTime(s string) (time.Time, error) {
switch l := len(s); {
case l < len("15:04:05"):
return time.Time{}, fmt.Errorf("pgdriver: can't parse time=%q", s)
case l <= len(timeFormat):
if s[2] == ':' {
return time.ParseInLocation(timeFormat, s, time.UTC)
}
return time.ParseInLocation(dateFormat, s, time.UTC)
default:
if s[10] == 'T' {
return time.Parse(time.RFC3339Nano, s)
}
if c := s[l-9]; c == '+' || c == '-' {
return time.Parse(timestamptzFormat, s)
}
if c := s[l-6]; c == '+' || c == '-' {
return time.Parse(timestamptzFormat2, s)
}
if c := s[l-3]; c == '+' || c == '-' {
if strings.HasSuffix(s, "+00") {
s = s[:len(s)-3]
return time.ParseInLocation(timestampFormat, s, time.UTC)
}
return time.Parse(timestamptzFormat3, s)
}
return time.ParseInLocation(timestampFormat, s, time.UTC)
}
}
+419
View File
@@ -0,0 +1,419 @@
package pgdriver
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
// Network type, either tcp or unix.
// Default is tcp.
Network string
// TCP host:port or Unix socket depending on Network.
Addr string
// Dial timeout for establishing new connections.
// Default is 5 seconds.
DialTimeout time.Duration
// Dialer creates new network connection and has priority over
// Network and Addr options.
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
// TLS config for secure connections.
TLSConfig *tls.Config
User string
Password string
Database string
AppName string
// PostgreSQL session parameters updated with `SET` command when a connection is created.
ConnParams map[string]interface{}
// Timeout for socket reads. If reached, commands fail with a timeout instead of blocking.
ReadTimeout time.Duration
// Timeout for socket writes. If reached, commands fail with a timeout instead of blocking.
WriteTimeout time.Duration
// ResetSessionFunc is called prior to executing a query on a connection that has been used before.
ResetSessionFunc func(context.Context, *Conn) error
}
func newDefaultConfig() *Config {
host := env("PGHOST", "localhost")
port := env("PGPORT", "5432")
cfg := &Config{
Network: "tcp",
Addr: net.JoinHostPort(host, port),
DialTimeout: 5 * time.Second,
TLSConfig: &tls.Config{InsecureSkipVerify: true},
User: env("PGUSER", "postgres"),
Database: env("PGDATABASE", "postgres"),
ReadTimeout: 10 * time.Second,
WriteTimeout: 5 * time.Second,
}
cfg.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
netDialer := &net.Dialer{
Timeout: cfg.DialTimeout,
KeepAlive: 5 * time.Minute,
}
return netDialer.DialContext(ctx, network, addr)
}
return cfg
}
type Option func(cfg *Config)
// Deprecated. Use Option instead.
type DriverOption = Option
func WithNetwork(network string) Option {
if network == "" {
panic("network is empty")
}
return func(cfg *Config) {
cfg.Network = network
}
}
func WithAddr(addr string) Option {
if addr == "" {
panic("addr is empty")
}
return func(cfg *Config) {
cfg.Addr = addr
}
}
func WithTLSConfig(tlsConfig *tls.Config) Option {
return func(cfg *Config) {
cfg.TLSConfig = tlsConfig
}
}
func WithInsecure(on bool) Option {
return func(cfg *Config) {
if on {
cfg.TLSConfig = nil
} else {
cfg.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
}
}
func WithUser(user string) Option {
if user == "" {
panic("user is empty")
}
return func(cfg *Config) {
cfg.User = user
}
}
func WithPassword(password string) Option {
return func(cfg *Config) {
cfg.Password = password
}
}
func WithDatabase(database string) Option {
if database == "" {
panic("database is empty")
}
return func(cfg *Config) {
cfg.Database = database
}
}
func WithApplicationName(appName string) Option {
return func(cfg *Config) {
cfg.AppName = appName
}
}
func WithConnParams(params map[string]interface{}) Option {
return func(cfg *Config) {
cfg.ConnParams = params
}
}
func WithTimeout(timeout time.Duration) Option {
return func(cfg *Config) {
cfg.DialTimeout = timeout
cfg.ReadTimeout = timeout
cfg.WriteTimeout = timeout
}
}
func WithDialTimeout(dialTimeout time.Duration) Option {
return func(cfg *Config) {
cfg.DialTimeout = dialTimeout
}
}
func WithReadTimeout(readTimeout time.Duration) Option {
return func(cfg *Config) {
cfg.ReadTimeout = readTimeout
}
}
func WithWriteTimeout(writeTimeout time.Duration) Option {
return func(cfg *Config) {
cfg.WriteTimeout = writeTimeout
}
}
// WithResetSessionFunc configures a function that is called prior to executing
// a query on a connection that has been used before.
// If the func returns driver.ErrBadConn, the connection is discarded.
func WithResetSessionFunc(fn func(context.Context, *Conn) error) Option {
return func(cfg *Config) {
cfg.ResetSessionFunc = fn
}
}
func WithDSN(dsn string) Option {
return func(cfg *Config) {
opts, err := parseDSN(dsn)
if err != nil {
panic(err)
}
for _, opt := range opts {
opt(cfg)
}
}
}
func env(key, defValue string) string {
if s := os.Getenv(key); s != "" {
return s
}
return defValue
}
//------------------------------------------------------------------------------
func parseDSN(dsn string) ([]Option, error) {
u, err := url.Parse(dsn)
if err != nil {
return nil, err
}
q := queryOptions{q: u.Query()}
var opts []Option
switch u.Scheme {
case "postgres", "postgresql":
if u.Host != "" {
addr := u.Host
if !strings.Contains(addr, ":") {
addr += ":5432"
}
opts = append(opts, WithAddr(addr))
}
if len(u.Path) > 1 {
opts = append(opts, WithDatabase(u.Path[1:]))
}
if host := q.string("host"); host != "" {
opts = append(opts, WithAddr(host))
if host[0] == '/' {
opts = append(opts, WithNetwork("unix"))
}
}
case "unix":
if len(u.Path) == 0 {
return nil, fmt.Errorf("unix socket DSN requires a path: %s", dsn)
}
opts = append(opts, WithNetwork("unix"))
if u.Host != "" {
opts = append(opts, WithDatabase(u.Host))
}
opts = append(opts, WithAddr(u.Path))
default:
return nil, errors.New("pgdriver: invalid scheme: " + u.Scheme)
}
if u.User != nil {
opts = append(opts, WithUser(u.User.Username()))
if password, ok := u.User.Password(); ok {
opts = append(opts, WithPassword(password))
}
}
if appName := q.string("application_name"); appName != "" {
opts = append(opts, WithApplicationName(appName))
}
if sslMode, sslRootCert := q.string("sslmode"), q.string("sslrootcert"); sslMode != "" || sslRootCert != "" {
tlsConfig := &tls.Config{}
switch sslMode {
case "disable":
tlsConfig = nil
case "allow", "prefer", "":
tlsConfig.InsecureSkipVerify = true
case "require":
if sslRootCert == "" {
tlsConfig.InsecureSkipVerify = true
break
}
// For backwards compatibility reasons, in the presence of `sslrootcert`,
// `sslmode` = `require` must act as if `sslmode` = `verify-ca`. See the note at
// https://www.postgresql.org/docs/current/libpq-ssl.html#LIBQ-SSL-CERTIFICATES .
fallthrough
case "verify-ca":
// The default certificate verification will also verify the host name
// which is not the behavior of `verify-ca`. As such, we need to manually
// check the certificate chain.
// At the time of writing, tls.Config has no option for this behavior
// (verify chain, but skip server name).
// See https://github.com/golang/go/issues/21971 .
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
certs := make([]*x509.Certificate, 0, len(rawCerts))
for _, rawCert := range rawCerts {
cert, err := x509.ParseCertificate(rawCert)
if err != nil {
return fmt.Errorf("pgdriver: failed to parse certificate: %w", err)
}
certs = append(certs, cert)
}
intermediates := x509.NewCertPool()
for _, cert := range certs[1:] {
intermediates.AddCert(cert)
}
_, err := certs[0].Verify(x509.VerifyOptions{
Roots: tlsConfig.RootCAs,
Intermediates: intermediates,
})
return err
}
case "verify-full":
tlsConfig.ServerName = u.Host
if host, _, err := net.SplitHostPort(u.Host); err == nil {
tlsConfig.ServerName = host
}
default:
return nil, fmt.Errorf("pgdriver: sslmode '%s' is not supported", sslMode)
}
if tlsConfig != nil && sslRootCert != "" {
rawCA, err := ioutil.ReadFile(sslRootCert)
if err != nil {
return nil, fmt.Errorf("pgdriver: failed to read root CA: %w", err)
}
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(rawCA) {
return nil, fmt.Errorf("pgdriver: failed to append root CA")
}
tlsConfig.RootCAs = certPool
}
opts = append(opts, WithTLSConfig(tlsConfig))
}
if d := q.duration("timeout"); d != 0 {
opts = append(opts, WithTimeout(d))
}
if d := q.duration("dial_timeout"); d != 0 {
opts = append(opts, WithDialTimeout(d))
}
if d := q.duration("connect_timeout"); d != 0 {
opts = append(opts, WithDialTimeout(d))
}
if d := q.duration("read_timeout"); d != 0 {
opts = append(opts, WithReadTimeout(d))
}
if d := q.duration("write_timeout"); d != 0 {
opts = append(opts, WithWriteTimeout(d))
}
rem, err := q.remaining()
if err != nil {
return nil, q.err
}
if len(rem) > 0 {
params := make(map[string]interface{}, len(rem))
for k, v := range rem {
params[k] = v
}
opts = append(opts, WithConnParams(params))
}
return opts, nil
}
// verify is a method to make sure if the config is legitimate
// in the case it detects any errors, it returns with a non-nil error
// it can be extended to check other parameters
func (c *Config) verify() error {
if c.User == "" {
return errors.New("pgdriver: User option is empty (to configure, use WithUser).")
}
return nil
}
type queryOptions struct {
q url.Values
err error
}
func (o *queryOptions) string(name string) string {
vs := o.q[name]
if len(vs) == 0 {
return ""
}
delete(o.q, name) // enable detection of unknown parameters
return vs[len(vs)-1]
}
func (o *queryOptions) duration(name string) time.Duration {
s := o.string(name)
if s == "" {
return 0
}
// try plain number first
if i, err := strconv.Atoi(s); err == nil {
if i <= 0 {
// disable timeouts
return -1
}
return time.Duration(i) * time.Second
}
dur, err := time.ParseDuration(s)
if err == nil {
return dur
}
if o.err == nil {
o.err = fmt.Errorf("pgdriver: invalid %s duration: %w", name, err)
}
return 0
}
func (o *queryOptions) remaining() (map[string]string, error) {
if o.err != nil {
return nil, o.err
}
if len(o.q) == 0 {
return nil, nil
}
m := make(map[string]string, len(o.q))
for k, ss := range o.q {
m[k] = ss[len(ss)-1]
}
return m, nil
}
+249
View File
@@ -0,0 +1,249 @@
package pgdriver
import (
"bufio"
"context"
"database/sql"
"fmt"
"io"
"github.com/uptrace/bun"
)
// CopyFrom copies data from the reader to the query destination.
func CopyFrom(
ctx context.Context, conn bun.Conn, r io.Reader, query string, args ...interface{},
) (res sql.Result, err error) {
query, err = formatQueryArgs(query, args)
if err != nil {
return nil, err
}
if err := conn.Raw(func(driverConn interface{}) error {
cn := driverConn.(*Conn)
if err := writeQuery(ctx, cn, query); err != nil {
return err
}
if err := readCopyIn(ctx, cn); err != nil {
return err
}
if err := writeCopyData(ctx, cn, r); err != nil {
return err
}
if err := writeCopyDone(ctx, cn); err != nil {
return err
}
res, err = readQuery(ctx, cn)
return err
}); err != nil {
return nil, err
}
return res, nil
}
func readCopyIn(ctx context.Context, cn *Conn) error {
rd := cn.reader(ctx, -1)
var firstErr error
for {
c, msgLen, err := readMessageType(rd)
if err != nil {
return err
}
switch c {
case errorResponseMsg:
e, err := readError(rd)
if err != nil {
return err
}
if firstErr == nil {
firstErr = e
}
case readyForQueryMsg:
if err := rd.Discard(msgLen); err != nil {
return err
}
return firstErr
case copyInResponseMsg:
if err := rd.Discard(msgLen); err != nil {
return err
}
return firstErr
case noticeResponseMsg, parameterStatusMsg:
if err := rd.Discard(msgLen); err != nil {
return err
}
default:
return fmt.Errorf("pgdriver: readCopyIn: unexpected message %q", c)
}
}
}
func writeCopyData(ctx context.Context, cn *Conn, r io.Reader) error {
wb := getWriteBuffer()
defer putWriteBuffer(wb)
for {
wb.StartMessage(copyDataMsg)
if _, err := wb.ReadFrom(r); err != nil {
if err == io.EOF {
break
}
return err
}
wb.FinishMessage()
if err := cn.write(ctx, wb); err != nil {
return err
}
}
return nil
}
func writeCopyDone(ctx context.Context, cn *Conn) error {
wb := getWriteBuffer()
defer putWriteBuffer(wb)
wb.StartMessage(copyDoneMsg)
wb.FinishMessage()
return cn.write(ctx, wb)
}
//------------------------------------------------------------------------------
// CopyTo copies data from the query source to the writer.
func CopyTo(
ctx context.Context, conn bun.Conn, w io.Writer, query string, args ...interface{},
) (res sql.Result, err error) {
query, err = formatQueryArgs(query, args)
if err != nil {
return nil, err
}
if err := conn.Raw(func(driverConn interface{}) error {
cn := driverConn.(*Conn)
if err := writeQuery(ctx, cn, query); err != nil {
return err
}
if err := readCopyOut(ctx, cn); err != nil {
return err
}
res, err = readCopyData(ctx, cn, w)
return err
}); err != nil {
return nil, err
}
return res, nil
}
func readCopyOut(ctx context.Context, cn *Conn) error {
rd := cn.reader(ctx, -1)
var firstErr error
for {
c, msgLen, err := readMessageType(rd)
if err != nil {
return err
}
switch c {
case errorResponseMsg:
e, err := readError(rd)
if err != nil {
return err
}
if firstErr == nil {
firstErr = e
}
case readyForQueryMsg:
if err := rd.Discard(msgLen); err != nil {
return err
}
return firstErr
case copyOutResponseMsg:
if err := rd.Discard(msgLen); err != nil {
return err
}
return nil
case noticeResponseMsg, parameterStatusMsg:
if err := rd.Discard(msgLen); err != nil {
return err
}
default:
return fmt.Errorf("pgdriver: readCopyOut: unexpected message %q", c)
}
}
}
func readCopyData(ctx context.Context, cn *Conn, w io.Writer) (res sql.Result, err error) {
rd := cn.reader(ctx, -1)
var firstErr error
for {
c, msgLen, err := readMessageType(rd)
if err != nil {
return nil, err
}
switch c {
case errorResponseMsg:
e, err := readError(rd)
if err != nil {
return nil, err
}
if firstErr == nil {
firstErr = e
}
case copyDataMsg:
for msgLen > 0 {
b, err := rd.ReadTemp(msgLen)
if err != nil && err != bufio.ErrBufferFull {
return nil, err
}
if _, err := w.Write(b); err != nil {
if firstErr == nil {
firstErr = err
}
break
}
msgLen -= len(b)
}
case copyDoneMsg:
if err := rd.Discard(msgLen); err != nil {
return nil, err
}
case commandCompleteMsg:
tmp, err := rd.ReadTemp(msgLen)
if err != nil {
firstErr = err
break
}
r, err := parseResult(tmp)
if err != nil {
firstErr = err
} else {
res = r
}
case readyForQueryMsg:
if err := rd.Discard(msgLen); err != nil {
return nil, err
}
return res, firstErr
case noticeResponseMsg, parameterStatusMsg:
if err := rd.Discard(msgLen); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("pgdriver: readCopyData: unexpected message %q", c)
}
}
}
+600
View File
@@ -0,0 +1,600 @@
package pgdriver
import (
"bytes"
"context"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"strconv"
"sync/atomic"
"time"
)
func init() {
sql.Register("pg", NewDriver())
}
type logging interface {
Printf(ctx context.Context, format string, v ...interface{})
}
type logger struct {
log *log.Logger
}
func (l *logger) Printf(ctx context.Context, format string, v ...interface{}) {
_ = l.log.Output(2, fmt.Sprintf(format, v...))
}
var Logger logging = &logger{
log: log.New(os.Stderr, "pgdriver: ", log.LstdFlags|log.Lshortfile),
}
//------------------------------------------------------------------------------
type Driver struct {
connector *Connector
}
var _ driver.DriverContext = (*Driver)(nil)
func NewDriver() Driver {
return Driver{}
}
func (d Driver) OpenConnector(name string) (driver.Connector, error) {
opts, err := parseDSN(name)
if err != nil {
return nil, err
}
return NewConnector(opts...), nil
}
func (d Driver) Open(name string) (driver.Conn, error) {
connector, err := d.OpenConnector(name)
if err != nil {
return nil, err
}
return connector.Connect(context.TODO())
}
//------------------------------------------------------------------------------
type Connector struct {
cfg *Config
}
func NewConnector(opts ...Option) *Connector {
c := &Connector{cfg: newDefaultConfig()}
for _, opt := range opts {
opt(c.cfg)
}
return c
}
var _ driver.Connector = (*Connector)(nil)
func (c *Connector) Connect(ctx context.Context) (driver.Conn, error) {
if err := c.cfg.verify(); err != nil {
return nil, err
}
return newConn(ctx, c.cfg)
}
func (c *Connector) Driver() driver.Driver {
return Driver{connector: c}
}
func (c *Connector) Config() *Config {
return c.cfg
}
//------------------------------------------------------------------------------
type Conn struct {
cfg *Config
netConn net.Conn
rd *reader
processID int32
secretKey int32
stmtCount int
closed int32
}
func newConn(ctx context.Context, cfg *Config) (*Conn, error) {
netConn, err := cfg.Dialer(ctx, cfg.Network, cfg.Addr)
if err != nil {
return nil, err
}
cn := &Conn{
cfg: cfg,
netConn: netConn,
rd: newReader(netConn),
}
if cfg.TLSConfig != nil {
if err := enableSSL(ctx, cn, cfg.TLSConfig); err != nil {
return nil, err
}
}
if err := startup(ctx, cn); err != nil {
return nil, err
}
for k, v := range cfg.ConnParams {
if v != nil {
_, err = cn.ExecContext(ctx, fmt.Sprintf("SET %s TO $1", k), []driver.NamedValue{
{Value: v},
})
} else {
_, err = cn.ExecContext(ctx, fmt.Sprintf("SET %s TO DEFAULT", k), nil)
}
if err != nil {
return nil, err
}
}
return cn, nil
}
func (cn *Conn) reader(ctx context.Context, timeout time.Duration) *reader {
cn.setReadDeadline(ctx, timeout)
return cn.rd
}
func (cn *Conn) write(ctx context.Context, wb *writeBuffer) error {
cn.setWriteDeadline(ctx, -1)
n, err := cn.netConn.Write(wb.Bytes)
wb.Reset()
if err != nil {
if n == 0 {
Logger.Printf(ctx, "pgdriver: Conn.Write failed (zero-length): %s", err)
return driver.ErrBadConn
}
return err
}
return nil
}
var _ driver.Conn = (*Conn)(nil)
func (cn *Conn) Prepare(query string) (driver.Stmt, error) {
if cn.isClosed() {
return nil, driver.ErrBadConn
}
ctx := context.TODO()
name := fmt.Sprintf("pgdriver-%d", cn.stmtCount)
cn.stmtCount++
if err := writeParseDescribeSync(ctx, cn, name, query); err != nil {
return nil, err
}
rowDesc, err := readParseDescribeSync(ctx, cn)
if err != nil {
return nil, err
}
return newStmt(cn, name, rowDesc), nil
}
func (cn *Conn) Close() error {
if !atomic.CompareAndSwapInt32(&cn.closed, 0, 1) {
return nil
}
return cn.netConn.Close()
}
func (cn *Conn) isClosed() bool {
return atomic.LoadInt32(&cn.closed) == 1
}
func (cn *Conn) Begin() (driver.Tx, error) {
return cn.BeginTx(context.Background(), driver.TxOptions{})
}
var _ driver.ConnBeginTx = (*Conn)(nil)
func (cn *Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
// No need to check if the conn is closed. ExecContext below handles that.
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
return nil, errors.New("pgdriver: custom IsolationLevel is not supported")
}
if opts.ReadOnly {
return nil, errors.New("pgdriver: ReadOnly transactions are not supported")
}
if _, err := cn.ExecContext(ctx, "BEGIN", nil); err != nil {
return nil, err
}
return tx{cn: cn}, nil
}
var _ driver.ExecerContext = (*Conn)(nil)
func (cn *Conn) ExecContext(
ctx context.Context, query string, args []driver.NamedValue,
) (driver.Result, error) {
if cn.isClosed() {
return nil, driver.ErrBadConn
}
res, err := cn.exec(ctx, query, args)
if err != nil {
return nil, cn.checkBadConn(err)
}
return res, nil
}
func (cn *Conn) exec(
ctx context.Context, query string, args []driver.NamedValue,
) (driver.Result, error) {
query, err := formatQuery(query, args)
if err != nil {
return nil, err
}
if err := writeQuery(ctx, cn, query); err != nil {
return nil, err
}
return readQuery(ctx, cn)
}
var _ driver.QueryerContext = (*Conn)(nil)
func (cn *Conn) QueryContext(
ctx context.Context, query string, args []driver.NamedValue,
) (driver.Rows, error) {
if cn.isClosed() {
return nil, driver.ErrBadConn
}
rows, err := cn.query(ctx, query, args)
if err != nil {
return nil, cn.checkBadConn(err)
}
return rows, nil
}
func (cn *Conn) query(
ctx context.Context, query string, args []driver.NamedValue,
) (driver.Rows, error) {
query, err := formatQuery(query, args)
if err != nil {
return nil, err
}
if err := writeQuery(ctx, cn, query); err != nil {
return nil, err
}
return readQueryData(ctx, cn)
}
var _ driver.Pinger = (*Conn)(nil)
func (cn *Conn) Ping(ctx context.Context) error {
_, err := cn.ExecContext(ctx, "SELECT 1", nil)
return err
}
func (cn *Conn) setReadDeadline(ctx context.Context, timeout time.Duration) {
if timeout == -1 {
timeout = cn.cfg.ReadTimeout
}
_ = cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout))
}
func (cn *Conn) setWriteDeadline(ctx context.Context, timeout time.Duration) {
if timeout == -1 {
timeout = cn.cfg.WriteTimeout
}
_ = cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout))
}
func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time {
deadline, ok := ctx.Deadline()
if !ok {
if timeout == 0 {
return time.Time{}
}
return time.Now().Add(timeout)
}
if timeout == 0 {
return deadline
}
if tm := time.Now().Add(timeout); tm.Before(deadline) {
return tm
}
return deadline
}
var _ driver.Validator = (*Conn)(nil)
func (cn *Conn) IsValid() bool {
return !cn.isClosed()
}
var _ driver.SessionResetter = (*Conn)(nil)
func (cn *Conn) ResetSession(ctx context.Context) error {
if cn.isClosed() {
return driver.ErrBadConn
}
if cn.cfg.ResetSessionFunc != nil {
return cn.cfg.ResetSessionFunc(ctx, cn)
}
return nil
}
func (cn *Conn) checkBadConn(err error) error {
if isBadConn(err, false) {
// Close and return driver.ErrBadConn next time the conn is used.
_ = cn.Close()
}
// Always return the original error.
return err
}
func (cn *Conn) Conn() net.Conn { return cn.netConn }
//------------------------------------------------------------------------------
type rows struct {
cn *Conn
rowDesc *rowDescription
reusable bool
closed bool
}
var _ driver.Rows = (*rows)(nil)
func newRows(cn *Conn, rowDesc *rowDescription, reusable bool) *rows {
return &rows{
cn: cn,
rowDesc: rowDesc,
reusable: reusable,
}
}
func (r *rows) Columns() []string {
if r.closed || r.rowDesc == nil {
return nil
}
return r.rowDesc.names
}
func (r *rows) Close() error {
if r.closed {
return nil
}
defer r.close()
for {
switch err := r.Next(nil); err {
case nil, io.EOF:
return nil
default: // unexpected error
_ = r.cn.Close()
return err
}
}
}
func (r *rows) close() {
r.closed = true
if r.rowDesc != nil {
if r.reusable {
rowDescPool.Put(r.rowDesc)
}
r.rowDesc = nil
}
}
func (r *rows) Next(dest []driver.Value) error {
if r.closed {
return io.EOF
}
eof, err := r.next(dest)
if err == io.EOF {
return io.ErrUnexpectedEOF
} else if err != nil {
return err
}
if eof {
return io.EOF
}
return nil
}
func (r *rows) next(dest []driver.Value) (eof bool, _ error) {
rd := r.cn.reader(context.TODO(), -1)
var firstErr error
for {
c, msgLen, err := readMessageType(rd)
if err != nil {
return false, err
}
switch c {
case dataRowMsg:
return false, r.readDataRow(rd, dest)
case commandCompleteMsg:
if err := rd.Discard(msgLen); err != nil {
return false, err
}
case readyForQueryMsg:
r.close()
if err := rd.Discard(msgLen); err != nil {
return false, err
}
if firstErr != nil {
return false, firstErr
}
return true, nil
case parameterStatusMsg, noticeResponseMsg:
if err := rd.Discard(msgLen); err != nil {
return false, err
}
case errorResponseMsg:
e, err := readError(rd)
if err != nil {
return false, err
}
if firstErr == nil {
firstErr = e
}
default:
return false, fmt.Errorf("pgdriver: Next: unexpected message %q", c)
}
}
}
func (r *rows) readDataRow(rd *reader, dest []driver.Value) error {
numCol, err := readInt16(rd)
if err != nil {
return err
}
if len(dest) != int(numCol) {
return fmt.Errorf("pgdriver: query returned %d columns, but Scan dest has %d items",
numCol, len(dest))
}
for colIdx := int16(0); colIdx < numCol; colIdx++ {
dataLen, err := readInt32(rd)
if err != nil {
return err
}
value, err := readColumnValue(rd, r.rowDesc.types[colIdx], int(dataLen))
if err != nil {
return err
}
if dest != nil {
dest[colIdx] = value
}
}
return nil
}
//------------------------------------------------------------------------------
func parseResult(b []byte) (driver.RowsAffected, error) {
i := bytes.LastIndexByte(b, ' ')
if i == -1 {
return 0, nil
}
b = b[i+1 : len(b)-1]
affected, err := strconv.ParseUint(bytesToString(b), 10, 64)
if err != nil {
return 0, nil
}
return driver.RowsAffected(affected), nil
}
//------------------------------------------------------------------------------
type tx struct {
cn *Conn
}
var _ driver.Tx = (*tx)(nil)
func (tx tx) Commit() error {
_, err := tx.cn.ExecContext(context.Background(), "COMMIT", nil)
return err
}
func (tx tx) Rollback() error {
_, err := tx.cn.ExecContext(context.Background(), "ROLLBACK", nil)
return err
}
//------------------------------------------------------------------------------
type stmt struct {
cn *Conn
name string
rowDesc *rowDescription
}
var (
_ driver.Stmt = (*stmt)(nil)
_ driver.StmtExecContext = (*stmt)(nil)
_ driver.StmtQueryContext = (*stmt)(nil)
)
func newStmt(cn *Conn, name string, rowDesc *rowDescription) *stmt {
return &stmt{
cn: cn,
name: name,
rowDesc: rowDesc,
}
}
func (stmt *stmt) Close() error {
if stmt.rowDesc != nil {
rowDescPool.Put(stmt.rowDesc)
stmt.rowDesc = nil
}
ctx := context.TODO()
if err := writeCloseStmt(ctx, stmt.cn, stmt.name); err != nil {
return err
}
if err := readCloseStmtComplete(ctx, stmt.cn); err != nil {
return err
}
return nil
}
func (stmt *stmt) NumInput() int {
if stmt.rowDesc == nil {
return -1
}
return int(stmt.rowDesc.numInput)
}
func (stmt *stmt) Exec(args []driver.Value) (driver.Result, error) {
panic("not implemented")
}
func (stmt *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
if err := writeBindExecute(ctx, stmt.cn, stmt.name, args); err != nil {
return nil, err
}
return readExtQuery(ctx, stmt.cn)
}
func (stmt *stmt) Query(args []driver.Value) (driver.Rows, error) {
panic("not implemented")
}
func (stmt *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
if err := writeBindExecute(ctx, stmt.cn, stmt.name, args); err != nil {
return nil, err
}
return readExtQueryData(ctx, stmt.cn, stmt.rowDesc)
}
+75
View File
@@ -0,0 +1,75 @@
package pgdriver
import (
"database/sql/driver"
"fmt"
"net"
)
// Error represents an error returned by PostgreSQL server
// using PostgreSQL ErrorResponse protocol.
//
// https://www.postgresql.org/docs/current/static/protocol-message-formats.html
type Error struct {
m map[byte]string
}
// Field returns a string value associated with an error field.
//
// https://www.postgresql.org/docs/current/static/protocol-error-fields.html
func (err Error) Field(k byte) string {
return err.m[k]
}
// IntegrityViolation reports whether the error is a part of
// Integrity Constraint Violation class of errors.
//
// https://www.postgresql.org/docs/current/static/errcodes-appendix.html
func (err Error) IntegrityViolation() bool {
switch err.Field('C') {
case "23000", "23001", "23502", "23503", "23505", "23514", "23P01":
return true
default:
return false
}
}
// StatementTimeout reports whether the error is a statement timeout error.
func (err Error) StatementTimeout() bool {
return err.Field('C') == "57014"
}
func (err Error) Error() string {
return fmt.Sprintf("%s: %s (SQLSTATE=%s)",
err.Field('S'), err.Field('M'), err.Field('C'))
}
func isBadConn(err error, allowTimeout bool) bool {
switch err {
case nil:
return false
case driver.ErrBadConn:
return true
}
if err, ok := err.(Error); ok {
switch err.Field('V') {
case "FATAL", "PANIC":
return true
}
switch err.Field('C') {
case "25P02", // current transaction is aborted
"57014": // canceling statement due to user request
return true
}
return false
}
if allowTimeout {
if err, ok := err.(net.Error); ok && err.Timeout() {
return !err.Temporary()
}
}
return true
}
+199
View File
@@ -0,0 +1,199 @@
package pgdriver
import (
"database/sql/driver"
"encoding/hex"
"fmt"
"math"
"strconv"
"time"
"unicode/utf8"
)
func formatQueryArgs(query string, args []interface{}) (string, error) {
namedArgs := make([]driver.NamedValue, len(args))
for i, arg := range args {
namedArgs[i] = driver.NamedValue{Value: arg}
}
return formatQuery(query, namedArgs)
}
func formatQuery(query string, args []driver.NamedValue) (string, error) {
if len(args) == 0 {
return query, nil
}
dst := make([]byte, 0, 2*len(query))
p := newParser(query)
for p.Valid() {
switch c := p.Next(); c {
case '$':
if i, ok := p.Number(); ok {
if i < 1 {
return "", fmt.Errorf("pgdriver: got $%d, but the minimal arg index is 1", i)
}
if i > len(args) {
return "", fmt.Errorf("pgdriver: got %d args, wanted %d", len(args), i)
}
var err error
dst, err = appendArg(dst, args[i-1].Value)
if err != nil {
return "", err
}
} else {
dst = append(dst, '$')
}
case '\'':
if b, ok := p.QuotedString(); ok {
dst = append(dst, b...)
} else {
dst = append(dst, '\'')
}
default:
dst = append(dst, c)
}
}
return bytesToString(dst), nil
}
func appendArg(b []byte, v interface{}) ([]byte, error) {
switch v := v.(type) {
case nil:
return append(b, "NULL"...), nil
case int64:
return strconv.AppendInt(b, v, 10), nil
case float64:
switch {
case math.IsNaN(v):
return append(b, "'NaN'"...), nil
case math.IsInf(v, 1):
return append(b, "'Infinity'"...), nil
case math.IsInf(v, -1):
return append(b, "'-Infinity'"...), nil
default:
return strconv.AppendFloat(b, v, 'f', -1, 64), nil
}
case bool:
if v {
return append(b, "TRUE"...), nil
}
return append(b, "FALSE"...), nil
case []byte:
if v == nil {
return append(b, "NULL"...), nil
}
b = append(b, `'\x`...)
s := len(b)
b = append(b, make([]byte, hex.EncodedLen(len(v)))...)
hex.Encode(b[s:], v)
b = append(b, "'"...)
return b, nil
case string:
b = append(b, '\'')
for _, r := range v {
if r == '\000' {
continue
}
if r == '\'' {
b = append(b, '\'', '\'')
continue
}
if r < utf8.RuneSelf {
b = append(b, byte(r))
continue
}
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, nil
case time.Time:
if v.IsZero() {
return append(b, "NULL"...), nil
}
return v.UTC().AppendFormat(b, "'2006-01-02 15:04:05.999999-07:00'"), nil
default:
return nil, fmt.Errorf("pgdriver: unexpected arg: %T", v)
}
}
type parser struct {
b []byte
i int
}
func newParser(s string) *parser {
return &parser{
b: stringToBytes(s),
}
}
func (p *parser) Valid() bool {
return p.i < len(p.b)
}
func (p *parser) Next() byte {
c := p.b[p.i]
p.i++
return c
}
func (p *parser) Number() (int, bool) {
start := p.i
end := len(p.b)
for i := p.i; i < len(p.b); i++ {
c := p.b[i]
if !isNum(c) {
end = i
break
}
}
p.i = end
b := p.b[start:end]
n, err := strconv.Atoi(bytesToString(b))
if err != nil {
return 0, false
}
return n, true
}
func (p *parser) QuotedString() ([]byte, bool) {
start := p.i - 1
end := len(p.b)
var c byte
for i := p.i; i < len(p.b); i++ {
next := p.b[i]
if c == '\'' && next != '\'' {
end = i
break
}
c = next
}
p.i = end
b := p.b[start:end]
return b, true
}
func isNum(c byte) bool {
return c >= '0' && c <= '9'
}
+380
View File
@@ -0,0 +1,380 @@
package pgdriver
import (
"context"
"errors"
"strconv"
"sync"
"time"
"github.com/uptrace/bun"
)
const pingChannel = "bun:ping"
var (
errListenerClosed = errors.New("bun: listener is closed")
errPingTimeout = errors.New("bun: ping timeout")
)
// Notify sends a notification on the channel using `NOTIFY` command.
func Notify(ctx context.Context, db *bun.DB, channel, payload string) error {
_, err := db.ExecContext(ctx, "NOTIFY ?, ?", bun.Ident(channel), payload)
return err
}
type Listener struct {
db *bun.DB
driver *Connector
channels []string
mu sync.Mutex
cn *Conn
closed bool
exit chan struct{}
}
func NewListener(db *bun.DB) *Listener {
return &Listener{
db: db,
driver: db.Driver().(Driver).connector,
exit: make(chan struct{}),
}
}
// Close closes the listener, releasing any open resources.
func (ln *Listener) Close() error {
return ln.withLock(func() error {
if ln.closed {
return errListenerClosed
}
ln.closed = true
close(ln.exit)
return ln.closeConn(errListenerClosed)
})
}
func (ln *Listener) withLock(fn func() error) error {
ln.mu.Lock()
defer ln.mu.Unlock()
return fn()
}
func (ln *Listener) conn(ctx context.Context) (*Conn, error) {
if ln.closed {
return nil, errListenerClosed
}
if ln.cn != nil {
return ln.cn, nil
}
cn, err := ln._conn(ctx)
if err != nil {
return nil, err
}
ln.cn = cn
return cn, nil
}
func (ln *Listener) _conn(ctx context.Context) (*Conn, error) {
driverConn, err := ln.driver.Connect(ctx)
if err != nil {
return nil, err
}
cn := driverConn.(*Conn)
if len(ln.channels) > 0 {
err := ln.listen(ctx, cn, ln.channels...)
if err != nil {
_ = cn.Close()
return nil, err
}
}
return cn, nil
}
func (ln *Listener) checkConn(ctx context.Context, cn *Conn, err error, allowTimeout bool) {
_ = ln.withLock(func() error {
if ln.closed || ln.cn != cn {
return nil
}
if isBadConn(err, allowTimeout) {
ln.reconnect(ctx, err)
}
return nil
})
}
func (ln *Listener) reconnect(ctx context.Context, reason error) {
if ln.cn != nil {
Logger.Printf(ctx, "bun: discarding bad listener connection: %s", reason)
_ = ln.closeConn(reason)
}
_, _ = ln.conn(ctx)
}
func (ln *Listener) closeConn(reason error) error {
if ln.cn == nil {
return nil
}
err := ln.cn.Close()
ln.cn = nil
return err
}
// Listen starts listening for notifications on channels.
func (ln *Listener) Listen(ctx context.Context, channels ...string) error {
var cn *Conn
if err := ln.withLock(func() error {
ln.channels = appendIfNotExists(ln.channels, channels...)
var err error
cn, err = ln.conn(ctx)
return err
}); err != nil {
return err
}
if err := ln.listen(ctx, cn, channels...); err != nil {
ln.checkConn(ctx, cn, err, false)
return err
}
return nil
}
func (ln *Listener) listen(ctx context.Context, cn *Conn, channels ...string) error {
for _, channel := range channels {
if err := writeQuery(ctx, cn, "LISTEN "+strconv.Quote(channel)); err != nil {
return err
}
}
return nil
}
// Unlisten stops listening for notifications on channels.
func (ln *Listener) Unlisten(ctx context.Context, channels ...string) error {
var cn *Conn
if err := ln.withLock(func() error {
ln.channels = removeIfExists(ln.channels, channels...)
var err error
cn, err = ln.conn(ctx)
return err
}); err != nil {
return err
}
if err := ln.unlisten(ctx, cn, channels...); err != nil {
ln.checkConn(ctx, cn, err, false)
return err
}
return nil
}
func (ln *Listener) unlisten(ctx context.Context, cn *Conn, channels ...string) error {
for _, channel := range channels {
if err := writeQuery(ctx, cn, "UNLISTEN "+strconv.Quote(channel)); err != nil {
return err
}
}
return nil
}
// Receive indefinitely waits for a notification. This is low-level API
// and in most cases Channel should be used instead.
func (ln *Listener) Receive(ctx context.Context) (channel string, payload string, err error) {
return ln.ReceiveTimeout(ctx, 0)
}
// ReceiveTimeout waits for a notification until timeout is reached.
// This is low-level API and in most cases Channel should be used instead.
func (ln *Listener) ReceiveTimeout(
ctx context.Context, timeout time.Duration,
) (channel, payload string, err error) {
var cn *Conn
if err := ln.withLock(func() error {
var err error
cn, err = ln.conn(ctx)
return err
}); err != nil {
return "", "", err
}
rd := cn.reader(ctx, timeout)
channel, payload, err = readNotification(ctx, rd)
if err != nil {
ln.checkConn(ctx, cn, err, timeout > 0)
return "", "", err
}
return channel, payload, nil
}
// Channel returns a channel for concurrently receiving notifications.
// It periodically sends Ping notification to test connection health.
//
// The channel is closed with Listener. Receive* APIs can not be used
// after channel is created.
func (ln *Listener) Channel(opts ...ChannelOption) <-chan Notification {
return newChannel(ln, opts).ch
}
//------------------------------------------------------------------------------
// Notification received with LISTEN command.
type Notification struct {
Channel string
Payload string
}
type ChannelOption func(c *channel)
func WithChannelSize(size int) ChannelOption {
return func(c *channel) {
c.size = size
}
}
type channel struct {
ctx context.Context
ln *Listener
size int
pingTimeout time.Duration
ch chan Notification
pingCh chan struct{}
}
func newChannel(ln *Listener, opts []ChannelOption) *channel {
c := &channel{
ctx: context.TODO(),
ln: ln,
size: 1000,
pingTimeout: 5 * time.Second,
}
for _, opt := range opts {
opt(c)
}
c.ch = make(chan Notification, c.size)
c.pingCh = make(chan struct{}, 1)
_ = c.ln.Listen(c.ctx, pingChannel)
go c.startReceive()
go c.startPing()
return c
}
func (c *channel) startReceive() {
var errCount int
for {
channel, payload, err := c.ln.Receive(c.ctx)
if err != nil {
if err == errListenerClosed {
close(c.ch)
return
}
if errCount > 0 {
time.Sleep(500 * time.Millisecond)
}
errCount++
continue
}
errCount = 0
// Any notification is as good as a ping.
select {
case c.pingCh <- struct{}{}:
default:
}
switch channel {
case pingChannel:
// ignore
default:
select {
case c.ch <- Notification{channel, payload}:
default:
Logger.Printf(c.ctx, "pgdriver: Listener buffer is full (message is dropped)")
}
}
}
}
func (c *channel) startPing() {
timer := time.NewTimer(time.Minute)
timer.Stop()
healthy := true
for {
timer.Reset(c.pingTimeout)
select {
case <-c.pingCh:
healthy = true
if !timer.Stop() {
<-timer.C
}
case <-timer.C:
pingErr := c.ping(c.ctx)
if healthy {
healthy = false
} else {
if pingErr == nil {
pingErr = errPingTimeout
}
_ = c.ln.withLock(func() error {
c.ln.reconnect(c.ctx, pingErr)
return nil
})
}
case <-c.ln.exit:
return
}
}
}
func (c *channel) ping(ctx context.Context) error {
_, err := c.ln.db.ExecContext(ctx, "NOTIFY "+strconv.Quote(pingChannel))
return err
}
func appendIfNotExists(ss []string, es ...string) []string {
loop:
for _, e := range es {
for _, s := range ss {
if s == e {
continue loop
}
}
ss = append(ss, e)
}
return ss
}
func removeIfExists(ss []string, es ...string) []string {
for _, e := range es {
for i, s := range ss {
if s == e {
last := len(ss) - 1
ss[i] = ss[last]
ss = ss[:last]
break
}
}
}
return ss
}
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
// +build appengine
package internal
func bytesToString(b []byte) string {
return string(b)
}
func stringToBytes(s string) []byte {
return []byte(s)
}
+19
View File
@@ -0,0 +1,19 @@
// +build !appengine
package pgdriver
import "unsafe"
func bytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
//nolint:deadcode,unused
func stringToBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(
&struct {
string
Cap int
}{s, len(s)},
))
}
+112
View File
@@ -0,0 +1,112 @@
package pgdriver
import (
"encoding/binary"
"io"
"sync"
)
var wbPool = sync.Pool{
New: func() interface{} {
return newWriteBuffer()
},
}
func getWriteBuffer() *writeBuffer {
wb := wbPool.Get().(*writeBuffer)
return wb
}
func putWriteBuffer(wb *writeBuffer) {
wb.Reset()
wbPool.Put(wb)
}
type writeBuffer struct {
Bytes []byte
msgStart int
paramStart int
}
func newWriteBuffer() *writeBuffer {
return &writeBuffer{
Bytes: make([]byte, 0, 1024),
}
}
func (b *writeBuffer) Reset() {
b.Bytes = b.Bytes[:0]
}
func (b *writeBuffer) StartMessage(c byte) {
if c == 0 {
b.msgStart = len(b.Bytes)
b.Bytes = append(b.Bytes, 0, 0, 0, 0)
} else {
b.msgStart = len(b.Bytes) + 1
b.Bytes = append(b.Bytes, c, 0, 0, 0, 0)
}
}
func (b *writeBuffer) FinishMessage() {
binary.BigEndian.PutUint32(
b.Bytes[b.msgStart:], uint32(len(b.Bytes)-b.msgStart))
}
func (b *writeBuffer) Query() []byte {
return b.Bytes[b.msgStart+4 : len(b.Bytes)-1]
}
func (b *writeBuffer) StartParam() {
b.paramStart = len(b.Bytes)
b.Bytes = append(b.Bytes, 0, 0, 0, 0)
}
func (b *writeBuffer) FinishParam() {
binary.BigEndian.PutUint32(
b.Bytes[b.paramStart:], uint32(len(b.Bytes)-b.paramStart-4))
}
var nullParamLength = int32(-1)
func (b *writeBuffer) FinishNullParam() {
binary.BigEndian.PutUint32(
b.Bytes[b.paramStart:], uint32(nullParamLength))
}
func (b *writeBuffer) Write(data []byte) (int, error) {
b.Bytes = append(b.Bytes, data...)
return len(data), nil
}
func (b *writeBuffer) WriteInt16(num int16) {
b.Bytes = append(b.Bytes, 0, 0)
binary.BigEndian.PutUint16(b.Bytes[len(b.Bytes)-2:], uint16(num))
}
func (b *writeBuffer) WriteInt32(num int32) {
b.Bytes = append(b.Bytes, 0, 0, 0, 0)
binary.BigEndian.PutUint32(b.Bytes[len(b.Bytes)-4:], uint32(num))
}
func (b *writeBuffer) WriteString(s string) {
b.Bytes = append(b.Bytes, s...)
b.Bytes = append(b.Bytes, 0)
}
func (b *writeBuffer) WriteBytes(data []byte) {
b.Bytes = append(b.Bytes, data...)
b.Bytes = append(b.Bytes, 0)
}
func (b *writeBuffer) WriteByte(c byte) error {
b.Bytes = append(b.Bytes, c)
return nil
}
func (b *writeBuffer) ReadFrom(r io.Reader) (int64, error) {
n, err := r.Read(b.Bytes[len(b.Bytes):cap(b.Bytes)])
b.Bytes = b.Bytes[:len(b.Bytes)+n]
return int64(n), err
}
+27
View File
@@ -0,0 +1,27 @@
package bunjson
import (
"encoding/json"
"io"
)
var _ Provider = (*StdProvider)(nil)
// StdProvider implements the JSON Provider interface using the standard encoding/json package.
type StdProvider struct{}
func (StdProvider) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (StdProvider) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (StdProvider) NewEncoder(w io.Writer) Encoder {
return json.NewEncoder(w)
}
func (StdProvider) NewDecoder(r io.Reader) Decoder {
return json.NewDecoder(r)
}
+43
View File
@@ -0,0 +1,43 @@
package bunjson
import (
"io"
)
var provider Provider = StdProvider{}
func SetProvider(p Provider) {
provider = p
}
type Provider interface {
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
NewEncoder(w io.Writer) Encoder
NewDecoder(r io.Reader) Decoder
}
type Decoder interface {
Decode(v any) error
UseNumber()
}
type Encoder interface {
Encode(v any) error
}
func Marshal(v any) ([]byte, error) {
return provider.Marshal(v)
}
func Unmarshal(data []byte, v any) error {
return provider.Unmarshal(data, v)
}
func NewEncoder(w io.Writer) Encoder {
return provider.NewEncoder(w)
}
func NewDecoder(r io.Reader) Decoder {
return provider.NewDecoder(r)
}
+115
View File
@@ -0,0 +1,115 @@
package bun
import (
"context"
"database/sql"
"strings"
"sync/atomic"
"time"
"unicode"
)
// QueryEvent captures information about a query execution for hooks.
type QueryEvent struct {
DB *DB
IQuery Query
Query string
QueryTemplate string
QueryArgs []any
Model Model
StartTime time.Time
Result sql.Result
Err error
Stash map[any]any
}
// Operation returns the SQL operation name such as SELECT or UPDATE.
func (e *QueryEvent) Operation() string {
if e.IQuery != nil {
return e.IQuery.Operation()
}
return queryOperation(e.Query)
}
func queryOperation(query string) string {
queryOp := strings.TrimLeftFunc(query, unicode.IsSpace)
if idx := strings.IndexByte(queryOp, ' '); idx > 0 {
queryOp = queryOp[:idx]
}
if len(queryOp) > 16 {
queryOp = queryOp[:16]
}
return queryOp
}
// QueryHook allows observing queries before and after execution.
type QueryHook interface {
BeforeQuery(context.Context, *QueryEvent) context.Context
AfterQuery(context.Context, *QueryEvent)
}
func (db *DB) beforeQuery(
ctx context.Context,
iquery Query,
queryTemplate string,
queryArgs []any,
query string,
model Model,
) (context.Context, *QueryEvent) {
atomic.AddUint32(&db.stats.Queries, 1)
if len(db.queryHooks) == 0 {
return ctx, nil
}
event := &QueryEvent{
DB: db,
Model: model,
IQuery: iquery,
Query: query,
QueryTemplate: queryTemplate,
QueryArgs: queryArgs,
StartTime: time.Now(),
}
for _, hook := range db.queryHooks {
ctx = hook.BeforeQuery(ctx, event)
}
return ctx, event
}
func (db *DB) afterQuery(
ctx context.Context,
event *QueryEvent,
res sql.Result,
err error,
) {
switch err {
case nil, sql.ErrNoRows:
// nothing
default:
atomic.AddUint32(&db.stats.Errors, 1)
}
if event == nil {
return
}
event.Result = res
event.Err = err
db.afterQueryFromIndex(ctx, event, len(db.queryHooks)-1)
}
func (db *DB) afterQueryFromIndex(ctx context.Context, event *QueryEvent, hookIndex int) {
for ; hookIndex >= 0; hookIndex-- {
db.queryHooks[hookIndex].AfterQuery(ctx, event)
}
}
+16
View File
@@ -0,0 +1,16 @@
package internal
type Flag uint64
func (flag Flag) Has(other Flag) bool {
return flag&other != 0
}
func (flag Flag) Set(other Flag) Flag {
return flag | other
}
func (flag Flag) Remove(other Flag) Flag {
flag &= ^other
return flag
}
+43
View File
@@ -0,0 +1,43 @@
package internal
import (
fasthex "github.com/tmthrgd/go-hex"
)
type HexEncoder struct {
b []byte
written bool
}
func NewHexEncoder(b []byte) *HexEncoder {
return &HexEncoder{
b: b,
}
}
func (enc *HexEncoder) Bytes() []byte {
return enc.b
}
func (enc *HexEncoder) Write(b []byte) (int, error) {
if !enc.written {
enc.b = append(enc.b, '\'')
enc.b = append(enc.b, `\x`...)
enc.written = true
}
i := len(enc.b)
enc.b = append(enc.b, make([]byte, fasthex.EncodedLen(len(b)))...)
fasthex.Encode(enc.b[i:], b)
return len(b), nil
}
func (enc *HexEncoder) Close() error {
if enc.written {
enc.b = append(enc.b, '\'')
} else {
enc.b = append(enc.b, "NULL"...)
}
return nil
}
+54
View File
@@ -0,0 +1,54 @@
package internal
import (
"fmt"
"log"
"os"
)
type Logging interface {
Printf(format string, v ...any)
}
var defaultLogger = log.New(os.Stderr, "", log.LstdFlags)
var Logger Logging = &logger{
log: defaultLogger,
}
var Warn = &wrapper{
prefix: "WARN: bun: ",
logger: Logger,
}
var Deprecated = &wrapper{
prefix: "DEPRECATED: bun: ",
logger: Logger,
}
type logger struct {
log *log.Logger
}
func (l *logger) Printf(format string, v ...any) {
_ = l.log.Output(2, fmt.Sprintf(format, v...))
}
type wrapper struct {
prefix string
logger Logging
}
func (w *wrapper) Printf(format string, v ...any) {
w.logger.Printf(w.prefix+format, v...)
}
func SetLogger(newLogger Logging) {
if newLogger == nil {
Logger = &logger{log: defaultLogger}
} else {
Logger = newLogger
}
Warn.logger = Logger
Deprecated.logger = Logger
}
+67
View File
@@ -0,0 +1,67 @@
package internal
import "reflect"
var ifaceType = reflect.TypeFor[any]()
type MapKey struct {
iface any
}
func NewMapKey(is []any) MapKey {
return MapKey{
iface: newMapKey(is),
}
}
func newMapKey(is []any) any {
switch len(is) {
case 1:
ptr := new([1]any)
copy((*ptr)[:], is)
return *ptr
case 2:
ptr := new([2]any)
copy((*ptr)[:], is)
return *ptr
case 3:
ptr := new([3]any)
copy((*ptr)[:], is)
return *ptr
case 4:
ptr := new([4]any)
copy((*ptr)[:], is)
return *ptr
case 5:
ptr := new([5]any)
copy((*ptr)[:], is)
return *ptr
case 6:
ptr := new([6]any)
copy((*ptr)[:], is)
return *ptr
case 7:
ptr := new([7]any)
copy((*ptr)[:], is)
return *ptr
case 8:
ptr := new([8]any)
copy((*ptr)[:], is)
return *ptr
case 9:
ptr := new([9]any)
copy((*ptr)[:], is)
return *ptr
case 10:
ptr := new([10]any)
copy((*ptr)[:], is)
return *ptr
default:
}
at := reflect.New(reflect.ArrayOf(len(is), ifaceType)).Elem()
for i, v := range is {
*(at.Index(i).Addr().Interface().(*any)) = v
}
return at.Interface()
}
+125
View File
@@ -0,0 +1,125 @@
package ordered
// Pair represents a key-value pair in the ordered map.
type Pair[K comparable, V any] struct {
Key K
Value V
next, prev *Pair[K, V] // Pointers to the next and previous pairs in the linked list.
}
// Map represents an ordered map.
type Map[K comparable, V any] struct {
root *Pair[K, V] // Sentinel node for the circular doubly linked list.
zero V // Zero value for the value type.
pairs map[K]*Pair[K, V] // Map from keys to pairs.
}
// NewMap creates a new ordered map with optional initial data.
func NewMap[K comparable, V any](initialData ...Pair[K, V]) *Map[K, V] {
m := &Map[K, V]{}
m.Clear()
for _, pair := range initialData {
m.Store(pair.Key, pair.Value)
}
return m
}
// Clear removes all pairs from the map.
func (m *Map[K, V]) Clear() {
if m.root != nil {
m.root.next, m.root.prev = nil, nil // avoid memory leaks
}
for _, pair := range m.pairs {
pair.next, pair.prev = nil, nil // avoid memory leaks
}
m.root = &Pair[K, V]{}
m.root.next, m.root.prev = m.root, m.root
m.pairs = make(map[K]*Pair[K, V])
}
// Len returns the number of pairs in the map.
func (m *Map[K, V]) Len() int {
return len(m.pairs)
}
// Load returns the value associated with the key, and a boolean indicating if the key was found.
func (m *Map[K, V]) Load(key K) (V, bool) {
if pair, present := m.pairs[key]; present {
return pair.Value, true
}
return m.zero, false
}
// Value returns the value associated with the key, or the zero value if the key is not found.
func (m *Map[K, V]) Value(key K) V {
if pair, present := m.pairs[key]; present {
return pair.Value
}
return m.zero
}
// Store adds or updates a key-value pair in the map.
func (m *Map[K, V]) Store(key K, value V) {
if pair, present := m.pairs[key]; present {
pair.Value = value
return
}
pair := &Pair[K, V]{Key: key, Value: value}
pair.prev = m.root.prev
m.root.prev.next = pair
m.root.prev = pair
pair.next = m.root
m.pairs[key] = pair
}
// Delete removes a key-value pair from the map.
func (m *Map[K, V]) Delete(key K) {
if pair, present := m.pairs[key]; present {
pair.prev.next = pair.next
pair.next.prev = pair.prev
pair.next, pair.prev = nil, nil // avoid memory leaks
delete(m.pairs, key)
}
}
// Range calls the given function for each key-value pair in the map in order.
func (m *Map[K, V]) Range(yield func(key K, value V) bool) {
for pair := m.root.next; pair != m.root; pair = pair.next {
if !yield(pair.Key, pair.Value) {
break
}
}
}
// Keys returns a slice of all keys in the map in order.
func (m *Map[K, V]) Keys() []K {
keys := make([]K, 0, len(m.pairs))
m.Range(func(key K, _ V) bool {
keys = append(keys, key)
return true
})
return keys
}
// Values returns a slice of all values in the map in order.
func (m *Map[K, V]) Values() []V {
values := make([]V, 0, len(m.pairs))
m.Range(func(_ K, value V) bool {
values = append(values, value)
return true
})
return values
}
// Pairs returns a slice of all key-value pairs in the map in order.
func (m *Map[K, V]) Pairs() []Pair[K, V] {
pairs := make([]Pair[K, V], 0, len(m.pairs))
m.Range(func(key K, value V) bool {
pairs = append(pairs, Pair[K, V]{Key: key, Value: value})
return true
})
return pairs
}
+194
View File
@@ -0,0 +1,194 @@
package parser
import (
"bytes"
"fmt"
"io"
"strconv"
"github.com/uptrace/bun/internal"
)
type Parser struct {
b []byte
i int
}
func New(b []byte) *Parser {
return &Parser{
b: b,
}
}
func NewString(s string) *Parser {
return New(internal.Bytes(s))
}
func (p *Parser) Reset(b []byte) {
p.b = b
p.i = 0
}
func (p *Parser) Valid() bool {
return p.i < len(p.b)
}
func (p *Parser) Remaining() []byte {
return p.b[p.i:]
}
func (p *Parser) ReadByte() (byte, error) {
if p.Valid() {
ch := p.b[p.i]
p.Advance()
return ch, nil
}
return 0, io.ErrUnexpectedEOF
}
func (p *Parser) Read() byte {
if p.Valid() {
ch := p.b[p.i]
p.Advance()
return ch
}
return 0
}
func (p *Parser) Unread() {
if p.i > 0 {
p.i--
}
}
func (p *Parser) Peek() byte {
if p.Valid() {
return p.b[p.i]
}
return 0
}
func (p *Parser) Advance() {
p.i++
}
func (p *Parser) Skip(skip byte) error {
ch := p.Peek()
if ch == skip {
p.Advance()
return nil
}
return fmt.Errorf("got %q, wanted %q", ch, skip)
}
func (p *Parser) SkipPrefix(skip []byte) error {
if !bytes.HasPrefix(p.b[p.i:], skip) {
return fmt.Errorf("got %q, wanted prefix %q", p.b, skip)
}
p.i += len(skip)
return nil
}
func (p *Parser) CutPrefix(skip []byte) bool {
if !bytes.HasPrefix(p.b[p.i:], skip) {
return false
}
p.i += len(skip)
return true
}
func (p *Parser) ReadSep(sep byte) ([]byte, bool) {
ind := bytes.IndexByte(p.b[p.i:], sep)
if ind == -1 {
b := p.b[p.i:]
p.i = len(p.b)
return b, false
}
b := p.b[p.i : p.i+ind]
p.i += ind + 1
return b, true
}
func (p *Parser) ReadIdentifier() (string, bool) {
if p.i < len(p.b) && p.b[p.i] == '(' {
s := p.i + 1
if ind := bytes.IndexByte(p.b[s:], ')'); ind != -1 {
b := p.b[s : s+ind]
if isIdent(b) {
p.i = s + ind + 1
return internal.String(b), false
}
}
}
ind := len(p.b) - p.i
var alpha bool
for i, c := range p.b[p.i:] {
if isNum(c) {
continue
}
if isAlpha(c) || (i > 0 && alpha && c == '_') {
alpha = true
continue
}
ind = i
break
}
if ind == 0 {
return "", false
}
b := p.b[p.i : p.i+ind]
p.i += ind
return internal.String(b), !alpha
}
func (p *Parser) ReadNumber() int {
ind := len(p.b) - p.i
for i, c := range p.b[p.i:] {
if !isNum(c) {
ind = i
break
}
}
if ind == 0 {
return 0
}
n, err := strconv.Atoi(string(p.b[p.i : p.i+ind]))
if err != nil {
panic(err)
}
p.i += ind
return n
}
func isNum(c byte) bool {
return c >= '0' && c <= '9'
}
func isAlpha(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// isIdent reports whether b is a valid identifier consisting of
// letters, digits, and underscores, with at least one letter.
func isIdent(b []byte) bool {
if len(b) == 0 {
return false
}
hasAlpha := false
for i, c := range b {
if isAlpha(c) {
hasAlpha = true
continue
}
if isNum(c) {
continue
}
if c == '_' && i > 0 {
continue
}
return false
}
return hasAlpha
}
+11
View File
@@ -0,0 +1,11 @@
// +build appengine
package internal
func String(b []byte) string {
return string(b)
}
func Bytes(s string) []byte {
return []byte(s)
}
+184
View File
@@ -0,0 +1,184 @@
package tagparser
import (
"strings"
)
type Tag struct {
Name string
Options map[string][]string
}
func (t Tag) IsZero() bool {
return t.Name == "" && t.Options == nil
}
func (t Tag) HasOption(name string) bool {
_, ok := t.Options[name]
return ok
}
func (t Tag) Option(name string) (string, bool) {
if vs, ok := t.Options[name]; ok {
return vs[len(vs)-1], true
}
return "", false
}
func Parse(s string) Tag {
if s == "" {
return Tag{}
}
p := parser{
s: s,
}
p.parse()
return p.tag
}
type parser struct {
s string
i int
tag Tag
seenName bool // for empty names
}
func (p *parser) setName(name string) {
if p.seenName {
p.addOption(name, "")
} else {
p.seenName = true
p.tag.Name = name
}
}
func (p *parser) addOption(key, value string) {
p.seenName = true
if key == "" {
return
}
if p.tag.Options == nil {
p.tag.Options = make(map[string][]string)
}
if vs, ok := p.tag.Options[key]; ok {
p.tag.Options[key] = append(vs, value)
} else {
p.tag.Options[key] = []string{value}
}
}
func (p *parser) parse() {
for p.valid() {
p.parseKeyValue()
if p.peek() == ',' {
p.i++
}
}
}
func (p *parser) parseKeyValue() {
start := p.i
for p.valid() {
switch c := p.read(); c {
case ',':
key := p.s[start : p.i-1]
p.setName(key)
return
case ':':
key := p.s[start : p.i-1]
value := p.parseValue()
p.addOption(key, value)
return
case '"':
key := p.parseQuotedValue()
p.setName(key)
return
}
}
key := p.s[start:p.i]
p.setName(key)
}
func (p *parser) parseValue() string {
start := p.i
for p.valid() {
switch c := p.read(); c {
case '"':
return p.parseQuotedValue()
case ',':
return p.s[start : p.i-1]
case '(':
p.skipPairs('(', ')')
}
}
if p.i == start {
return ""
}
return p.s[start:p.i]
}
func (p *parser) parseQuotedValue() string {
if i := strings.IndexByte(p.s[p.i:], '"'); i >= 0 && p.s[p.i+i-1] != '\\' {
s := p.s[p.i : p.i+i]
p.i += i + 1
return s
}
b := make([]byte, 0, 16)
for p.valid() {
switch c := p.read(); c {
case '\\':
b = append(b, p.read())
case '"':
return string(b)
default:
b = append(b, c)
}
}
return ""
}
func (p *parser) skipPairs(start, end byte) {
var lvl int
for p.valid() {
switch c := p.read(); c {
case '"':
_ = p.parseQuotedValue()
case start:
lvl++
case end:
if lvl == 0 {
return
}
lvl--
}
}
}
func (p *parser) valid() bool {
return p.i < len(p.s)
}
func (p *parser) read() byte {
if !p.valid() {
return 0
}
c := p.s[p.i]
p.i++
return c
}
func (p *parser) peek() byte {
if !p.valid() {
return 0
}
c := p.s[p.i]
return c
}
+61
View File
@@ -0,0 +1,61 @@
package internal
import (
"fmt"
"time"
)
const (
dateFormat = "2006-01-02"
timeFormat = "15:04:05.999999999"
timetzFormat1 = "15:04:05.999999999-07:00:00"
timetzFormat2 = "15:04:05.999999999-07:00"
timetzFormat3 = "15:04:05.999999999-07"
timestampFormat = "2006-01-02 15:04:05.999999999"
timestamptzFormat1 = "2006-01-02 15:04:05.999999999-07:00:00"
timestamptzFormat2 = "2006-01-02 15:04:05.999999999-07:00"
timestamptzFormat3 = "2006-01-02 15:04:05.999999999-07"
)
func ParseTime(s string) (time.Time, error) {
l := len(s)
if l >= len("2006-01-02 15:04:05") {
switch s[10] {
case ' ':
if c := s[l-6]; c == '+' || c == '-' {
return time.Parse(timestamptzFormat2, s)
}
if c := s[l-3]; c == '+' || c == '-' {
return time.Parse(timestamptzFormat3, s)
}
if c := s[l-9]; c == '+' || c == '-' {
return time.Parse(timestamptzFormat1, s)
}
return time.ParseInLocation(timestampFormat, s, time.UTC)
case 'T':
return time.Parse(time.RFC3339Nano, s)
}
}
if l >= len("15:04:05-07") {
if c := s[l-6]; c == '+' || c == '-' {
return time.Parse(timetzFormat2, s)
}
if c := s[l-3]; c == '+' || c == '-' {
return time.Parse(timetzFormat3, s)
}
if c := s[l-9]; c == '+' || c == '-' {
return time.Parse(timetzFormat1, s)
}
}
if l < len("15:04:05") {
return time.Time{}, fmt.Errorf("bun: can't parse time=%q", s)
}
if s[2] == ':' {
return time.ParseInLocation(timeFormat, s, time.UTC)
}
return time.ParseInLocation(dateFormat, s, time.UTC)
}
+67
View File
@@ -0,0 +1,67 @@
package internal
func IsUpper(c byte) bool {
return c >= 'A' && c <= 'Z'
}
func IsLower(c byte) bool {
return c >= 'a' && c <= 'z'
}
func ToUpper(c byte) byte {
return c - 32
}
func ToLower(c byte) byte {
return c + 32
}
// Underscore converts "CamelCasedString" to "camel_cased_string".
func Underscore(s string) string {
r := make([]byte, 0, len(s)+5)
for i := 0; i < len(s); i++ {
c := s[i]
if IsUpper(c) {
if i > 0 && i+1 < len(s) && (IsLower(s[i-1]) || IsLower(s[i+1])) {
r = append(r, '_', ToLower(c))
} else {
r = append(r, ToLower(c))
}
} else {
r = append(r, c)
}
}
return string(r)
}
func CamelCased(s string) string {
r := make([]byte, 0, len(s))
upperNext := true
for i := 0; i < len(s); i++ {
c := s[i]
if c == '_' {
upperNext = true
continue
}
if upperNext {
if IsLower(c) {
c = ToUpper(c)
}
upperNext = false
}
r = append(r, c)
}
return string(r)
}
func ToExported(s string) string {
if len(s) == 0 {
return s
}
if c := s[0]; IsLower(c) {
b := []byte(s)
b[0] = ToUpper(c)
return string(b)
}
return s
}
+22
View File
@@ -0,0 +1,22 @@
//go:build !appengine
// +build !appengine
package internal
import "unsafe"
// String converts byte slice to string.
func String(b []byte) string {
if len(b) == 0 {
return ""
}
return unsafe.String(&b[0], len(b))
}
// Bytes converts string to byte slice.
func Bytes(s string) []byte {
if s == "" {
return []byte{}
}
return unsafe.Slice(unsafe.StringData(s), len(s))
}
+87
View File
@@ -0,0 +1,87 @@
package internal
import (
"reflect"
)
func MakeSliceNextElemFunc(v reflect.Value) func() reflect.Value {
if v.Kind() == reflect.Array {
var pos int
return func() reflect.Value {
v := v.Index(pos)
pos++
return v
}
}
elemType := v.Type().Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
return func() reflect.Value {
if v.Len() < v.Cap() {
v.Set(v.Slice(0, v.Len()+1))
elem := v.Index(v.Len() - 1)
if elem.IsNil() {
elem.Set(reflect.New(elemType))
}
return elem
}
elem := reflect.New(elemType)
v.Set(reflect.Append(v, elem))
return elem
}
}
zero := reflect.Zero(elemType)
return func() reflect.Value {
if v.Len() < v.Cap() {
v.Set(v.Slice(0, v.Len()+1))
return v.Index(v.Len() - 1)
}
v.Set(reflect.Append(v, zero))
return v.Index(v.Len() - 1)
}
}
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
func FieldByIndexAlloc(v reflect.Value, index []int) reflect.Value {
if len(index) == 1 {
return v.Field(index[0])
}
for i, idx := range index {
if i > 0 {
v = indirectNil(v)
}
v = v.Field(idx)
}
return v
}
func indirectNil(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Ptr {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = v.Elem()
}
return v
}
// MakeQueryBytes returns zero-length byte slice with capacity of 4096.
func MakeQueryBytes() []byte {
// TODO: make this configurable?
return make([]byte, 0, 4096)
}
+475
View File
@@ -0,0 +1,475 @@
package migrate
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/uptrace/bun"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/migrate/sqlschema"
"github.com/uptrace/bun/schema"
)
// AutoMigratorOption configures an AutoMigrator.
type AutoMigratorOption func(m *AutoMigrator)
// WithModel adds a bun.Model to the migration scope.
func WithModel(models ...any) AutoMigratorOption {
return func(m *AutoMigrator) {
m.includeModels = append(m.includeModels, models...)
}
}
// WithExcludeTable tells AutoMigrator to exclude database tables from the migration scope.
// This prevents AutoMigrator from dropping tables which may exist in the schema
// but which are not used by the application.
//
// Expressions may make use of the wildcards supported by the SQL LIKE operator:
// - % as a wildcard
// - _ as a single character
//
// Do not exclude tables included via WithModel, as BunModelInspector ignores this setting.
func WithExcludeTable(tables ...string) AutoMigratorOption {
return func(m *AutoMigrator) {
m.excludeTables = append(m.excludeTables, tables...)
}
}
// WithExcludeForeignKeys tells AutoMigrator to exclude a foreign key constaint
// from the migration scope. This prevents AutoMigrator from dropping foreign keys
// that are defined manually via CreateTableQuery.ForeignKey().
func WithExcludeForeignKeys(fks ...sqlschema.ForeignKey) AutoMigratorOption {
return func(m *AutoMigrator) {
m.excludeForeignKeys = append(m.excludeForeignKeys, fks...)
}
}
// WithSchemaName sets the database schema to migrate objects in.
// By default, dialects' default schema is used.
func WithSchemaName(schemaName string) AutoMigratorOption {
return func(m *AutoMigrator) {
m.schemaName = schemaName
}
}
// WithTableNameAuto overrides default migrations table name.
func WithTableNameAuto(table string) AutoMigratorOption {
return func(m *AutoMigrator) {
m.table = table
m.migratorOpts = append(m.migratorOpts, WithTableName(table))
}
}
// WithLocksTableNameAuto overrides default migration locks table name.
func WithLocksTableNameAuto(table string) AutoMigratorOption {
return func(m *AutoMigrator) {
m.locksTable = table
m.migratorOpts = append(m.migratorOpts, WithLocksTableName(table))
}
}
// WithMarkAppliedOnSuccessAuto sets the migrator to only mark migrations as applied/unapplied
// when their up/down is successful.
func WithMarkAppliedOnSuccessAuto(enabled bool) AutoMigratorOption {
return func(m *AutoMigrator) {
m.migratorOpts = append(m.migratorOpts, WithMarkAppliedOnSuccess(enabled))
}
}
// WithMigrationsDirectoryAuto overrides the default directory for migration files.
func WithMigrationsDirectoryAuto(directory string) AutoMigratorOption {
return func(m *AutoMigrator) {
m.migrationsOpts = append(m.migrationsOpts, WithMigrationsDirectory(directory))
}
}
// AutoMigrator performs automated schema migrations.
//
// It is designed to be a drop-in replacement for some Migrator functionality and supports all existing
// configuration options.
// Similarly to Migrator, it has methods to create SQL migrations, write them to a file, and apply them.
// Unlike Migrator, it detects the differences between the state defined by bun models and the current
// database schema automatically.
//
// Usage:
// 1. Generate migrations and apply them at once with AutoMigrator.Migrate().
// 2. Create up- and down-SQL migration files and apply migrations using Migrator.Migrate().
//
// While both methods produce complete, reversible migrations (with entries in the database
// and SQL migration files), prefer creating migrations and applying them separately for
// any non-trivial cases to ensure AutoMigrator detects expected changes correctly.
//
// Limitations:
// - AutoMigrator only supports a subset of the possible ALTER TABLE modifications.
// - Some changes are not automatically reversible. For example, you would need to manually
// add a CREATE TABLE query to the .down migration file to revert a DROP TABLE migration.
// - Does not validate most dialect-specific constraints. For example, when changing column
// data type, make sure the data con be auto-casted to the new type.
// - Due to how the schema-state diff is calculated, it is not possible to rename a table and
// modify any of its columns' _data type_ in a single run. This will cause the AutoMigrator
// to drop and re-create the table under a different name; it is better to apply this change in 2 steps.
// Renaming a table and renaming its columns at the same time is possible.
// - Renaming table/column to an existing name, i.e. like this [A->B] [B->C], is not possible due to how
// AutoMigrator distinguishes "rename" and "unchanged" columns.
//
// Dialect must implement both sqlschema.Inspector and sqlschema.Migrator to be used with AutoMigrator.
type AutoMigrator struct {
db *bun.DB
// dbInspector creates the current state for the target database.
dbInspector sqlschema.Inspector
// modelInspector creates the desired state based on the model definitions.
modelInspector sqlschema.Inspector
// dbMigrator executes ALTER TABLE queries.
dbMigrator sqlschema.Migrator
table string // Migrations table (excluded from database inspection)
locksTable string // Migration locks table (excluded from database inspection)
// schemaName is the database schema considered for migration.
schemaName string
// includeModels define the migration scope.
includeModels []any
excludeTables []string // excludeTables are excluded from database inspection.
excludeForeignKeys []sqlschema.ForeignKey // excludeForeignKeys are excluded from database inspection.
// diffOpts are passed to detector constructor.
diffOpts []diffOption
// migratorOpts are passed to Migrator constructor.
migratorOpts []MigratorOption
// migrationsOpts are passed to Migrations constructor.
migrationsOpts []MigrationsOption
}
// NewAutoMigrator creates an AutoMigrator that detects schema differences and generates migrations.
func NewAutoMigrator(db *bun.DB, opts ...AutoMigratorOption) (*AutoMigrator, error) {
am := &AutoMigrator{
db: db,
table: defaultTable,
locksTable: defaultLocksTable,
schemaName: db.Dialect().DefaultSchema(),
}
for _, opt := range opts {
opt(am)
}
am.excludeTables = append(am.excludeTables, am.table, am.locksTable)
dbInspector, err := sqlschema.NewInspector(db,
sqlschema.WithSchemaName(am.schemaName),
sqlschema.WithExcludeTables(am.excludeTables...),
sqlschema.WithExcludeForeignKeys(am.excludeForeignKeys...),
)
if err != nil {
return nil, err
}
am.dbInspector = dbInspector
am.diffOpts = append(am.diffOpts, withCompareTypeFunc(db.Dialect().(sqlschema.InspectorDialect).CompareType))
dbMigrator, err := sqlschema.NewMigrator(db, am.schemaName)
if err != nil {
return nil, err
}
am.dbMigrator = dbMigrator
tables := schema.NewTables(db.Dialect())
tables.Register(am.includeModels...)
am.modelInspector = sqlschema.NewBunModelInspector(tables, sqlschema.WithSchemaName(am.schemaName))
return am, nil
}
func (am *AutoMigrator) plan(ctx context.Context) (*changeset, error) {
var err error
got, err := am.dbInspector.Inspect(ctx)
if err != nil {
return nil, err
}
want, err := am.modelInspector.Inspect(ctx)
if err != nil {
return nil, err
}
changes := diff(got, want, am.diffOpts...)
if err := changes.ResolveDependencies(); err != nil {
return nil, fmt.Errorf("plan migrations: %w", err)
}
return changes, nil
}
// Migrate writes required changes to a new migration file and runs the migration.
// This will create an entry in the migrations table, making it possible to revert
// the changes with Migrator.Rollback(). MigrationOptions are passed on to Migrator.Migrate().
func (am *AutoMigrator) Migrate(ctx context.Context, opts ...MigrationOption) (*MigrationGroup, error) {
migrations, _, err := am.createSQLMigrations(ctx, false)
if err != nil {
if err == errNothingToMigrate {
return new(MigrationGroup), nil
}
return nil, fmt.Errorf("auto migrate: %w", err)
}
migrator := NewMigrator(am.db, migrations, am.migratorOpts...)
if err := migrator.Init(ctx); err != nil {
return nil, fmt.Errorf("auto migrate: %w", err)
}
group, err := migrator.Migrate(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("auto migrate: %w", err)
}
return group, nil
}
// CreateSQLMigration writes required changes to a new migration file.
// Use migrate.Migrator to apply the generated migrations.
func (am *AutoMigrator) CreateSQLMigrations(ctx context.Context) ([]*MigrationFile, error) {
_, files, err := am.createSQLMigrations(ctx, false)
if err == errNothingToMigrate {
return files, nil
}
return files, err
}
// CreateTxSQLMigration writes required changes to a new migration file making sure they will be executed
// in a transaction when applied. Use migrate.Migrator to apply the generated migrations.
func (am *AutoMigrator) CreateTxSQLMigrations(ctx context.Context) ([]*MigrationFile, error) {
_, files, err := am.createSQLMigrations(ctx, true)
if err == errNothingToMigrate {
return files, nil
}
return files, err
}
// errNothingToMigrate is a sentinel error which means the database is already in a desired state.
// Should not be returned to the user -- return a nil-error instead.
var errNothingToMigrate = errors.New("nothing to migrate")
func (am *AutoMigrator) createSQLMigrations(ctx context.Context, transactional bool) (*Migrations, []*MigrationFile, error) {
changes, err := am.plan(ctx)
if err != nil {
return nil, nil, fmt.Errorf("create sql migrations: %w", err)
}
if changes.Len() == 0 {
return nil, nil, errNothingToMigrate
}
name, _ := genMigrationName(am.schemaName + "_auto")
migrations := NewMigrations(am.migrationsOpts...)
migrations.Add(Migration{
Name: name,
Up: wrapGoMigrationFunc(changes.Up(am.dbMigrator)),
Down: wrapGoMigrationFunc(changes.Down(am.dbMigrator)),
Comment: "Changes detected by bun.AutoMigrator",
})
// Append .tx.up.sql or .up.sql to migration name, depending if it should be transactional.
fname := func(direction string) string {
return name + map[bool]string{true: ".tx.", false: "."}[transactional] + direction + ".sql"
}
up, err := am.createSQL(ctx, migrations, fname("up"), changes, transactional)
if err != nil {
return nil, nil, fmt.Errorf("create sql migration up: %w", err)
}
down, err := am.createSQL(ctx, migrations, fname("down"), changes.GetReverse(), transactional)
if err != nil {
return nil, nil, fmt.Errorf("create sql migration down: %w", err)
}
return migrations, []*MigrationFile{up, down}, nil
}
func (am *AutoMigrator) createSQL(_ context.Context, migrations *Migrations, fname string, changes *changeset, transactional bool) (*MigrationFile, error) {
var buf bytes.Buffer
if transactional {
buf.WriteString("SET statement_timeout = 0;")
}
if err := changes.WriteTo(&buf, am.dbMigrator); err != nil {
return nil, err
}
content := buf.Bytes()
fpath := filepath.Join(migrations.getDirectory(), fname)
if err := os.WriteFile(fpath, content, 0o644); err != nil {
return nil, err
}
mf := &MigrationFile{
Name: fname,
Path: fpath,
Content: string(content),
}
return mf, nil
}
func (c *changeset) Len() int {
return len(c.operations)
}
// Func creates a MigrationFunc that applies all operations all the changeset.
func (c *changeset) Func(m sqlschema.Migrator) MigrationFunc {
return func(ctx context.Context, db *bun.DB) error {
return c.apply(ctx, db, m)
}
}
// GetReverse returns a new changeset with each operation in it "reversed" and in reverse order.
func (c *changeset) GetReverse() *changeset {
var reverse changeset
for i := len(c.operations) - 1; i >= 0; i-- {
reverse.Add(c.operations[i].GetReverse())
}
return &reverse
}
// Up is syntactic sugar.
func (c *changeset) Up(m sqlschema.Migrator) MigrationFunc {
return c.Func(m)
}
// Down is syntactic sugar.
func (c *changeset) Down(m sqlschema.Migrator) MigrationFunc {
return c.GetReverse().Func(m)
}
// apply generates SQL for each operation and executes it.
func (c *changeset) apply(ctx context.Context, db *bun.DB, m sqlschema.Migrator) error {
if len(c.operations) == 0 {
return nil
}
for _, op := range c.operations {
if _, skip := op.(*Unimplemented); skip {
continue
}
b := internal.MakeQueryBytes()
b, err := m.AppendSQL(b, op)
if err != nil {
return fmt.Errorf("apply changes: %w", err)
}
query := internal.String(b)
if _, err = db.ExecContext(ctx, query); err != nil {
return fmt.Errorf("apply changes: %w", err)
}
}
return nil
}
func (c *changeset) WriteTo(w io.Writer, m sqlschema.Migrator) error {
var err error
b := internal.MakeQueryBytes()
for _, op := range c.operations {
if comment, isComment := op.(*Unimplemented); isComment {
b = append(b, "/*\n"...)
b = append(b, *comment...)
b = append(b, "\n*/"...)
continue
}
// Append each query separately, merge later.
// Dialects assume that the []byte only holds
// the contents of a single query and may be misled.
queryBytes := internal.MakeQueryBytes()
queryBytes, err = m.AppendSQL(queryBytes, op)
if err != nil {
return fmt.Errorf("write changeset: %w", err)
}
b = append(b, queryBytes...)
b = append(b, ";\n"...)
}
if _, err := w.Write(b); err != nil {
return fmt.Errorf("write changeset: %w", err)
}
return nil
}
func (c *changeset) ResolveDependencies() error {
if len(c.operations) <= 1 {
return nil
}
const (
unvisited = iota
current
visited
)
status := make(map[Operation]int, len(c.operations))
for _, op := range c.operations {
status[op] = unvisited
}
var resolved []Operation
var nextOp Operation
var visit func(op Operation) error
next := func() bool {
for op, s := range status {
if s == unvisited {
nextOp = op
return true
}
}
return false
}
// visit iterates over c.operations until it finds all operations that depend on the current one
// or runs into circular dependency, in which case it will return an error.
visit = func(op Operation) error {
switch status[op] {
case visited:
return nil
case current:
// TODO: add details (circle) to the error message
return errors.New("detected circular dependency")
}
status[op] = current
for _, another := range c.operations {
if dop, hasDeps := another.(interface {
DependsOn(Operation) bool
}); another == op || !hasDeps || !dop.DependsOn(op) {
continue
}
if err := visit(another); err != nil {
return err
}
}
status[op] = visited
// Any dependent nodes would've already been added to the list by now, so we prepend.
resolved = append([]Operation{op}, resolved...)
return nil
}
for next() {
if err := visit(nextOp); err != nil {
return err
}
}
c.operations = resolved
return nil
}
+426
View File
@@ -0,0 +1,426 @@
package migrate
import (
"github.com/uptrace/bun/internal/ordered"
"github.com/uptrace/bun/migrate/sqlschema"
)
// changeset is a set of changes to the database schema definition.
type changeset struct {
operations []Operation
}
// Add new operations to the changeset.
func (c *changeset) Add(op ...Operation) {
c.operations = append(c.operations, op...)
}
// diff calculates the diff between the current database schema and the target state.
// The changeset is not sorted -- the caller should resolve dependencies before applying the changes.
func diff(got, want sqlschema.Database, opts ...diffOption) *changeset {
d := newDetector(got, want, opts...)
return d.detectChanges()
}
func (d *detector) detectChanges() *changeset {
currentTables := toOrderedMap(d.current.GetTables())
targetTables := toOrderedMap(d.target.GetTables())
RenameCreate:
for _, wantPair := range targetTables.Pairs() {
wantName, wantTable := wantPair.Key, wantPair.Value
// A table with this name exists in the database. We assume that schema objects won't
// be renamed to an already existing name, nor do we support such cases.
// Simply check if the table definition has changed.
if haveTable, ok := currentTables.Load(wantName); ok {
d.detectColumnChanges(haveTable, wantTable, true)
d.detectConstraintChanges(haveTable, wantTable)
continue
}
// Find all renamed tables. We assume that renamed tables have the same signature.
for _, havePair := range currentTables.Pairs() {
haveName, haveTable := havePair.Key, havePair.Value
if _, exists := targetTables.Load(haveName); !exists && d.canRename(haveTable, wantTable) {
d.changes.Add(&RenameTableOp{
TableName: haveTable.GetName(),
NewName: wantName,
})
d.refMap.RenameTable(haveTable.GetName(), wantName)
// Find renamed columns, if any, and check if constraints (PK, UNIQUE) have been updated.
// We need not check wantTable any further.
d.detectColumnChanges(haveTable, wantTable, false)
d.detectConstraintChanges(haveTable, wantTable)
currentTables.Delete(haveName)
continue RenameCreate
}
}
// If wantTable does not exist in the database and was not renamed
// then we need to create this table in the database.
additional := wantTable.(*sqlschema.BunTable)
d.changes.Add(&CreateTableOp{
TableName: wantTable.GetName(),
Model: additional.Model,
})
}
// Drop any remaining "current" tables which do not have a model.
for _, tPair := range currentTables.Pairs() {
name, table := tPair.Key, tPair.Value
if _, keep := targetTables.Load(name); !keep {
d.changes.Add(&DropTableOp{
TableName: table.GetName(),
})
}
}
targetFKs := d.target.GetForeignKeys()
currentFKs := d.refMap.Deref()
for fk := range targetFKs {
if _, ok := currentFKs[fk]; !ok {
d.changes.Add(&AddForeignKeyOp{
ForeignKey: fk,
ConstraintName: "", // leave empty to let each dialect apply their convention
})
}
}
for fk, name := range currentFKs {
if _, ok := targetFKs[fk]; !ok {
d.changes.Add(&DropForeignKeyOp{
ConstraintName: name,
ForeignKey: fk,
})
}
}
return &d.changes
}
// toOrderedMap transforms a slice of objects to an ordered map, using return of GetName() as key.
func toOrderedMap[V interface{ GetName() string }](named []V) *ordered.Map[string, V] {
m := ordered.NewMap[string, V]()
for _, v := range named {
m.Store(v.GetName(), v)
}
return m
}
// detectColumnChanges finds renamed columns and, if checkType == true, columns with changed type.
func (d *detector) detectColumnChanges(current, target sqlschema.Table, checkType bool) {
currentColumns := toOrderedMap(current.GetColumns())
targetColumns := toOrderedMap(target.GetColumns())
ChangeRename:
for _, tPair := range targetColumns.Pairs() {
tName, tCol := tPair.Key, tPair.Value
// This column exists in the database, so it hasn't been renamed, dropped, or added.
// Still, we should not delete(columns, thisColumn), because later we will need to
// check that we do not try to rename a column to an already a name that already exists.
if cCol, ok := currentColumns.Load(tName); ok {
if checkType && !d.equalColumns(cCol, tCol) {
d.changes.Add(&ChangeColumnTypeOp{
TableName: target.GetName(),
Column: tName,
From: cCol,
To: d.makeTargetColDef(cCol, tCol),
})
}
continue
}
// Column tName does not exist in the database -- it's been either renamed or added.
// Find renamed columns first.
for _, cPair := range currentColumns.Pairs() {
cName, cCol := cPair.Key, cPair.Value
// Cannot rename if a column with this name already exists or the types differ.
if _, exists := targetColumns.Load(cName); exists || !d.equalColumns(tCol, cCol) {
continue
}
d.changes.Add(&RenameColumnOp{
TableName: target.GetName(),
OldName: cName,
NewName: tName,
})
d.refMap.RenameColumn(target.GetName(), cName, tName)
currentColumns.Delete(cName) // no need to check this column again
// Update primary key definition to avoid superficially recreating the constraint.
current.GetPrimaryKey().Columns.Replace(cName, tName)
continue ChangeRename
}
d.changes.Add(&AddColumnOp{
TableName: target.GetName(),
ColumnName: tName,
Column: tCol,
})
}
// Drop columns which do not exist in the target schema and were not renamed.
for _, cPair := range currentColumns.Pairs() {
cName, cCol := cPair.Key, cPair.Value
if _, keep := targetColumns.Load(cName); !keep {
d.changes.Add(&DropColumnOp{
TableName: target.GetName(),
ColumnName: cName,
Column: cCol,
})
}
}
}
func (d *detector) detectConstraintChanges(current, target sqlschema.Table) {
Add:
for _, want := range target.GetUniqueConstraints() {
for _, got := range current.GetUniqueConstraints() {
if got.Equals(want) {
continue Add
}
}
d.changes.Add(&AddUniqueConstraintOp{
TableName: target.GetName(),
Unique: want,
})
}
Drop:
for _, got := range current.GetUniqueConstraints() {
for _, want := range target.GetUniqueConstraints() {
if got.Equals(want) {
continue Drop
}
}
d.changes.Add(&DropUniqueConstraintOp{
TableName: target.GetName(),
Unique: got,
})
}
targetPK := target.GetPrimaryKey()
currentPK := current.GetPrimaryKey()
// Detect primary key changes
if targetPK == nil && currentPK == nil {
return
}
switch {
case targetPK == nil && currentPK != nil:
d.changes.Add(&DropPrimaryKeyOp{
TableName: target.GetName(),
PrimaryKey: *currentPK,
})
case currentPK == nil && targetPK != nil:
d.changes.Add(&AddPrimaryKeyOp{
TableName: target.GetName(),
PrimaryKey: *targetPK,
})
case targetPK.Columns != currentPK.Columns:
d.changes.Add(&ChangePrimaryKeyOp{
TableName: target.GetName(),
Old: *currentPK,
New: *targetPK,
})
}
}
func newDetector(got, want sqlschema.Database, opts ...diffOption) *detector {
cfg := &detectorConfig{
cmpType: func(c1, c2 sqlschema.Column) bool {
return c1.GetSQLType() == c2.GetSQLType() && c1.GetVarcharLen() == c2.GetVarcharLen()
},
}
for _, opt := range opts {
opt(cfg)
}
return &detector{
current: got,
target: want,
refMap: newRefMap(got.GetForeignKeys()),
cmpType: cfg.cmpType,
}
}
type diffOption func(*detectorConfig)
func withCompareTypeFunc(f CompareTypeFunc) diffOption {
return func(cfg *detectorConfig) {
cfg.cmpType = f
}
}
// detectorConfig controls how differences in the model states are resolved.
type detectorConfig struct {
cmpType CompareTypeFunc
}
// detector may modify the passed database schemas, so it isn't safe to re-use them.
type detector struct {
// current state represents the existing database schema.
current sqlschema.Database
// target state represents the database schema defined in bun models.
target sqlschema.Database
changes changeset
refMap refMap
// cmpType determines column type equivalence.
// Default is direct comparison with '==' operator, which is inaccurate
// due to the existence of dialect-specific type aliases. The caller
// should pass a concrete InspectorDialect.EquivalentType for robust comparison.
cmpType CompareTypeFunc
}
// canRename checks if t1 can be renamed to t2.
func (d detector) canRename(t1, t2 sqlschema.Table) bool {
return t1.GetSchema() == t2.GetSchema() && equalSignatures(t1, t2, d.equalColumns)
}
func (d detector) equalColumns(col1, col2 sqlschema.Column) bool {
return d.cmpType(col1, col2) &&
col1.GetDefaultValue() == col2.GetDefaultValue() &&
col1.GetIsNullable() == col2.GetIsNullable() &&
col1.GetIsAutoIncrement() == col2.GetIsAutoIncrement() &&
col1.GetIsIdentity() == col2.GetIsIdentity()
}
func (d detector) makeTargetColDef(current, target sqlschema.Column) sqlschema.Column {
// Avoid unnecessary type-change migrations if the types are equivalent.
if d.cmpType(current, target) {
target = &sqlschema.BaseColumn{
Name: target.GetName(),
DefaultValue: target.GetDefaultValue(),
IsNullable: target.GetIsNullable(),
IsAutoIncrement: target.GetIsAutoIncrement(),
IsIdentity: target.GetIsIdentity(),
SQLType: current.GetSQLType(),
VarcharLen: current.GetVarcharLen(),
}
}
return target
}
// CompareTypeFunc compares two column definitions and reports whether they have the same SQL type.
type CompareTypeFunc func(sqlschema.Column, sqlschema.Column) bool
// equalSignatures determines if two tables have the same "signature".
func equalSignatures(t1, t2 sqlschema.Table, eq CompareTypeFunc) bool {
sig1 := newSignature(t1, eq)
sig2 := newSignature(t2, eq)
return sig1.Equals(sig2)
}
// signature is a set of column definitions, which allows "relation/name-agnostic" comparison between them;
// meaning that two columns are considered equal if their types are the same.
type signature struct {
// underlying stores the number of occurrences for each unique column type.
// It helps to account for the fact that a table might have multiple columns that have the same type.
underlying map[sqlschema.BaseColumn]int
eq CompareTypeFunc
}
func newSignature(t sqlschema.Table, eq CompareTypeFunc) signature {
s := signature{
underlying: make(map[sqlschema.BaseColumn]int),
eq: eq,
}
s.scan(t)
return s
}
// scan iterates over table's field and counts occurrences of each unique column definition.
func (s *signature) scan(t sqlschema.Table) {
for _, icol := range t.GetColumns() {
scanCol := icol.(*sqlschema.BaseColumn)
// This is slightly more expensive than if the columns could be compared directly
// and we always did s.underlying[col]++, but we get type-equivalence in return.
col, count := s.getCount(*scanCol)
if count == 0 {
s.underlying[*scanCol] = 1
} else {
s.underlying[col]++
}
}
}
// getCount uses CompareTypeFunc to find a column with the same (equivalent) SQL type
// and returns its count. Count 0 means there are no columns with of this type.
func (s *signature) getCount(keyCol sqlschema.BaseColumn) (key sqlschema.BaseColumn, count int) {
for col, cnt := range s.underlying {
if s.eq(&col, &keyCol) {
return col, cnt
}
}
return keyCol, 0
}
// Equals returns true if 2 signatures share an identical set of columns.
func (s *signature) Equals(other signature) bool {
if len(s.underlying) != len(other.underlying) {
return false
}
for col, count := range s.underlying {
if _, countOther := other.getCount(col); countOther != count {
return false
}
}
return true
}
// refMap is a utility for tracking superficial changes in foreign keys,
// which do not require any modification in the database.
// Modern SQL dialects automatically updated foreign key constraints whenever
// a column or a table is renamed. Detector can use refMap to ignore any
// differences in foreign keys which were caused by renamed column/table.
type refMap map[*sqlschema.ForeignKey]string
func newRefMap(fks map[sqlschema.ForeignKey]string) refMap {
rm := make(map[*sqlschema.ForeignKey]string)
for fk, name := range fks {
rm[&fk] = name
}
return rm
}
// RenameT updates table name in all foreign key definions which depend on it.
func (rm refMap) RenameTable(tableName string, newName string) {
for fk := range rm {
switch tableName {
case fk.From.TableName:
fk.From.TableName = newName
case fk.To.TableName:
fk.To.TableName = newName
}
}
}
// RenameColumn updates column name in all foreign key definions which depend on it.
func (rm refMap) RenameColumn(tableName string, column, newName string) {
for fk := range rm {
if tableName == fk.From.TableName {
fk.From.Column.Replace(column, newName)
}
if tableName == fk.To.TableName {
fk.To.Column.Replace(column, newName)
}
}
}
// Deref returns copies of ForeignKey values to a map.
func (rm refMap) Deref() map[sqlschema.ForeignKey]string {
out := make(map[sqlschema.ForeignKey]string)
for fk, name := range rm {
out[*fk] = name
}
return out
}
+357
View File
@@ -0,0 +1,357 @@
package migrate
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"io/fs"
"slices"
"strings"
"text/template"
"time"
"github.com/uptrace/bun"
)
// Migration represents a single database migration with up and down functions.
type Migration struct {
bun.BaseModel
ID int64 `bun:",pk,autoincrement"`
Name string
Comment string `bun:"-"`
GroupID int64
MigratedAt time.Time `bun:",notnull,nullzero,default:current_timestamp"`
Up internalMigrationFunc `bun:"-"`
Down internalMigrationFunc `bun:"-"`
}
// String returns the migration name and comment.
func (m Migration) String() string {
return fmt.Sprintf("%s_%s", m.Name, m.Comment)
}
// IsApplied reports whether the migration has been applied.
func (m Migration) IsApplied() bool {
return m.ID > 0
}
// MigrationFunc is a function that executes a migration against a database.
type MigrationFunc func(ctx context.Context, db *bun.DB) error
type internalMigrationFunc func(ctx context.Context, migrator *Migrator, migration *Migration) error
func wrapGoMigrationFunc(fn MigrationFunc) internalMigrationFunc {
return func(ctx context.Context, migrator *Migrator, migration *Migration) error {
if migrator.beforeMigrationHook != nil {
if err := migrator.beforeMigrationHook(ctx, migrator.db, migration); err != nil {
return err
}
}
if err := fn(ctx, migrator.db); err != nil {
return err
}
if migrator.afterMigrationHook != nil {
if err := migrator.afterMigrationHook(ctx, migrator.db, migration); err != nil {
return err
}
}
return nil
}
}
func newSQLMigrationFunc(fsys fs.FS, name string) internalMigrationFunc {
return func(ctx context.Context, migrator *Migrator, migration *Migration) error {
sqlFile, err := fsys.Open(name)
if err != nil {
return err
}
contents, err := io.ReadAll(sqlFile)
if err != nil {
return err
}
var reader io.Reader = bytes.NewReader(contents)
if migrator.templateData != nil {
buf, err := renderTemplate(contents, migrator.templateData)
if err != nil {
return err
}
reader = buf
}
scanner := bufio.NewScanner(reader)
var queries []string
var query []byte
for scanner.Scan() {
b := scanner.Bytes()
const prefix = "--bun:"
if bytes.HasPrefix(b, []byte(prefix)) {
b = b[len(prefix):]
if bytes.Equal(b, []byte("split")) {
queries = append(queries, string(query))
query = query[:0]
continue
}
return fmt.Errorf("bun: unknown directive: %q", b)
}
query = append(query, b...)
query = append(query, '\n')
}
if len(query) > 0 {
queries = append(queries, string(query))
}
if err := scanner.Err(); err != nil {
return err
}
var idb bun.IConn
isTx := strings.HasSuffix(name, ".tx.up.sql") || strings.HasSuffix(name, ".tx.down.sql")
if isTx {
tx, err := migrator.db.BeginTx(ctx, nil)
if err != nil {
return err
}
idb = tx
} else {
conn, err := migrator.db.Conn(ctx)
if err != nil {
return err
}
idb = conn
}
var retErr error
var execErr error
defer func() {
if tx, ok := idb.(bun.Tx); ok {
if execErr != nil {
retErr = tx.Rollback()
} else {
retErr = tx.Commit()
}
return
}
if conn, ok := idb.(bun.Conn); ok {
retErr = conn.Close()
return
}
panic("not reached")
}()
execErr = migrator.exec(ctx, idb, migration, queries)
if execErr != nil {
return execErr
}
return retErr
}
}
func renderTemplate(contents []byte, templateData any) (*bytes.Buffer, error) {
tmpl, err := template.New("migration").Parse(string(contents))
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
}
var rendered bytes.Buffer
if err := tmpl.Execute(&rendered, templateData); err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}
return &rendered, nil
}
const goTemplate = `package %s
import (
"context"
"fmt"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
fmt.Print(" [up migration] ")
return nil
}, func(ctx context.Context, db *bun.DB) error {
fmt.Print(" [down migration] ")
return nil
})
}
`
const sqlTemplate = `SET statement_timeout = 0;
--bun:split
SELECT 1
--bun:split
SELECT 2
`
const transactionalSQLTemplate = `SET statement_timeout = 0;
SELECT 1;
`
//------------------------------------------------------------------------------
// MigrationSlice is a slice of migrations that provides helper methods for filtering and grouping.
type MigrationSlice []Migration
func (ms MigrationSlice) String() string {
if len(ms) == 0 {
return "empty"
}
if len(ms) > 5 {
return fmt.Sprintf("%d migrations (%s ... %s)", len(ms), ms[0].Name, ms[len(ms)-1].Name)
}
var sb strings.Builder
for i := range ms {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(ms[i].String())
}
return sb.String()
}
// Applied returns applied migrations in descending order
// (the order is important and is used in Rollback).
func (ms MigrationSlice) Applied() MigrationSlice {
var applied MigrationSlice
for i := range ms {
if ms[i].IsApplied() {
applied = append(applied, ms[i])
}
}
sortDesc(applied)
return applied
}
// Unapplied returns unapplied migrations in ascending order
// (the order is important and is used in Migrate).
func (ms MigrationSlice) Unapplied() MigrationSlice {
var unapplied MigrationSlice
for i := range ms {
if !ms[i].IsApplied() {
unapplied = append(unapplied, ms[i])
}
}
sortAsc(unapplied)
return unapplied
}
// LastGroupID returns the last applied migration group id.
// The id is 0 when there are no migration groups.
func (ms MigrationSlice) LastGroupID() int64 {
var lastGroupID int64
for i := range ms {
groupID := ms[i].GroupID
if groupID > lastGroupID {
lastGroupID = groupID
}
}
return lastGroupID
}
// LastGroup returns the last applied migration group.
func (ms MigrationSlice) LastGroup() *MigrationGroup {
group := &MigrationGroup{
ID: ms.LastGroupID(),
}
if group.ID == 0 {
return group
}
for i := range ms {
if ms[i].GroupID == group.ID {
group.Migrations = append(group.Migrations, ms[i])
}
}
return group
}
// MigrationGroup is a group of migrations that were applied together in a single Migrate call.
type MigrationGroup struct {
ID int64
Migrations MigrationSlice
}
// IsZero reports whether the group is empty.
func (g MigrationGroup) IsZero() bool {
return g.ID == 0 && len(g.Migrations) == 0
}
func (g MigrationGroup) String() string {
if g.IsZero() {
return "nil"
}
return fmt.Sprintf("group #%d (%s)", g.ID, g.Migrations)
}
// MigrationFile represents a generated migration file on disk.
type MigrationFile struct {
Name string
Path string
Content string
}
//------------------------------------------------------------------------------
type migrationConfig struct {
nop bool
}
func newMigrationConfig(opts []MigrationOption) *migrationConfig {
cfg := new(migrationConfig)
for _, opt := range opts {
opt(cfg)
}
return cfg
}
// MigrationOption configures how a migration is executed.
type MigrationOption func(cfg *migrationConfig)
// WithNopMigration creates a no-op migration that marks itself as applied without running.
func WithNopMigration() MigrationOption {
return func(cfg *migrationConfig) {
cfg.nop = true
}
}
//------------------------------------------------------------------------------
func sortAsc(ms MigrationSlice) {
slices.SortFunc(ms, func(a, b Migration) int {
return strings.Compare(a.Name, b.Name)
})
}
func sortDesc(ms MigrationSlice) {
slices.SortFunc(ms, func(a, b Migration) int {
return strings.Compare(b.Name, a.Name)
})
}
+177
View File
@@ -0,0 +1,177 @@
package migrate
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
)
// MigrationsOption configures a Migrations instance.
type MigrationsOption func(m *Migrations)
// WithMigrationsDirectory sets the directory where migration files are stored.
func WithMigrationsDirectory(directory string) MigrationsOption {
return func(m *Migrations) {
m.explicitDirectory = directory
}
}
// Migrations is a collection of registered migrations.
type Migrations struct {
ms MigrationSlice
explicitDirectory string
implicitDirectory string
}
// NewMigrations creates a new collection of migrations.
func NewMigrations(opts ...MigrationsOption) *Migrations {
m := new(Migrations)
for _, opt := range opts {
opt(m)
}
m.implicitDirectory = filepath.Dir(migrationFile())
return m
}
// Sorted returns a copy of the migrations sorted by name in ascending order.
func (m *Migrations) Sorted() MigrationSlice {
migrations := make(MigrationSlice, len(m.ms))
copy(migrations, m.ms)
sortAsc(migrations)
return migrations
}
// MustRegister is like Register but panics on error.
func (m *Migrations) MustRegister(up, down MigrationFunc) {
if err := m.Register(up, down); err != nil {
panic(err)
}
}
// Register registers up and down migration functions derived from the caller's file name.
func (m *Migrations) Register(up, down MigrationFunc) error {
fpath := migrationFile()
name, comment, err := extractMigrationName(fpath)
if err != nil {
return err
}
m.Add(Migration{
Name: name,
Comment: comment,
Up: wrapGoMigrationFunc(up),
Down: wrapGoMigrationFunc(down),
})
return nil
}
// Add appends a migration to the collection.
func (m *Migrations) Add(migration Migration) {
if migration.Name == "" {
panic("migration name is required")
}
m.ms = append(m.ms, migration)
}
// DiscoverCaller discovers SQL migration files in the caller's directory.
func (m *Migrations) DiscoverCaller() error {
dir := filepath.Dir(migrationFile())
return m.Discover(os.DirFS(dir))
}
// Discover discovers SQL migration files in the given filesystem.
func (m *Migrations) Discover(fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".up.sql") && !strings.HasSuffix(path, ".down.sql") {
return nil
}
name, comment, err := extractMigrationName(path)
if err != nil {
return err
}
migration := m.getOrCreateMigration(name)
migration.Comment = comment
migrationFunc := newSQLMigrationFunc(fsys, path)
if strings.HasSuffix(path, ".up.sql") {
migration.Up = migrationFunc
return nil
}
if strings.HasSuffix(path, ".down.sql") {
migration.Down = migrationFunc
return nil
}
return errors.New("migrate: not reached")
})
}
func (m *Migrations) getOrCreateMigration(name string) *Migration {
for i := range m.ms {
mig := &m.ms[i]
if mig.Name == name {
return mig
}
}
m.ms = append(m.ms, Migration{Name: name})
return &m.ms[len(m.ms)-1]
}
func (m *Migrations) getDirectory() string {
if m.explicitDirectory != "" {
return m.explicitDirectory
}
if m.implicitDirectory != "" {
return m.implicitDirectory
}
return filepath.Dir(migrationFile())
}
func migrationFile() string {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(1, pcs[:])
frames := runtime.CallersFrames(pcs[:n])
for {
f, ok := frames.Next()
if !ok {
break
}
if !strings.Contains(f.Function, "/bun/migrate.") {
return f.File
}
}
return ""
}
var fnameRE = regexp.MustCompile(`^(\d{1,14})_([0-9a-z_\-]+)\.`)
func extractMigrationName(fpath string) (string, string, error) {
fname := filepath.Base(fpath)
matches := fnameRE.FindStringSubmatch(fname)
if matches == nil {
return "", "", fmt.Errorf("migrate: unsupported migration name format: %q", fname)
}
return matches[1], matches[2], nil
}
+656
View File
@@ -0,0 +1,656 @@
package migrate
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/feature"
)
const (
defaultTable = "bun_migrations"
defaultLocksTable = "bun_migration_locks"
)
// MigratorOption configures a Migrator.
type MigratorOption func(m *Migrator)
// WithTableName overrides default migrations table name.
func WithTableName(table string) MigratorOption {
return func(m *Migrator) {
m.table = table
}
}
// WithLocksTableName overrides default migration locks table name.
func WithLocksTableName(table string) MigratorOption {
return func(m *Migrator) {
m.locksTable = table
}
}
// WithMarkAppliedOnSuccess sets the migrator to only mark migrations as applied/unapplied
// when their up/down is successful.
func WithMarkAppliedOnSuccess(enabled bool) MigratorOption {
return func(m *Migrator) {
m.markAppliedOnSuccess = enabled
}
}
// WithUpsert enables upsert (ON CONFLICT / ON DUPLICATE KEY / MERGE) in MarkApplied.
// This is required when re-running already-applied migrations via RunMigration.
// Init automatically creates a unique index on the name column.
func WithUpsert(enabled bool) MigratorOption {
return func(m *Migrator) {
m.useUpsert = enabled
}
}
// WithTemplateData sets data passed to SQL migration templates during rendering.
func WithTemplateData(data any) MigratorOption {
return func(m *Migrator) {
m.templateData = data
}
}
// MigrationHook is a callback invoked before or after each migration runs.
type MigrationHook func(ctx context.Context, db bun.IConn, migration *Migration) error
// BeforeMigration registers a hook that runs before each migration.
func BeforeMigration(hook MigrationHook) MigratorOption {
return func(m *Migrator) {
m.beforeMigrationHook = hook
}
}
// AfterMigration registers a hook that runs after each migration.
func AfterMigration(hook MigrationHook) MigratorOption {
return func(m *Migrator) {
m.afterMigrationHook = hook
}
}
// Migrator manages the lifecycle of database migrations.
type Migrator struct {
db *bun.DB
migrations *Migrations
table string
locksTable string
markAppliedOnSuccess bool
useUpsert bool
templateData any
beforeMigrationHook MigrationHook
afterMigrationHook MigrationHook
}
// NewMigrator creates a new Migrator for the given database and migrations.
func NewMigrator(db *bun.DB, migrations *Migrations, opts ...MigratorOption) *Migrator {
m := &Migrator{
db: db,
migrations: migrations,
table: defaultTable,
locksTable: defaultLocksTable,
}
for _, opt := range opts {
opt(m)
}
return m
}
// DB returns the underlying bun.DB.
func (m *Migrator) DB() *bun.DB {
return m.db
}
// MigrationsWithStatus returns migrations with status in ascending order.
func (m *Migrator) MigrationsWithStatus(ctx context.Context) (MigrationSlice, error) {
sorted, _, err := m.migrationsWithStatus(ctx)
return sorted, err
}
func (m *Migrator) migrationsWithStatus(ctx context.Context) (MigrationSlice, int64, error) {
sorted := m.migrations.Sorted()
applied, err := m.AppliedMigrations(ctx)
if err != nil {
return nil, 0, err
}
appliedMap := migrationMap(applied)
for i := range sorted {
m1 := &sorted[i]
if m2, ok := appliedMap[m1.Name]; ok {
m1.ID = m2.ID
m1.GroupID = m2.GroupID
m1.MigratedAt = m2.MigratedAt
}
}
return sorted, applied.LastGroupID(), nil
}
// Init creates the migration tables if they do not already exist.
func (m *Migrator) Init(ctx context.Context) error {
if _, err := m.db.NewCreateTable().
Model((*Migration)(nil)).
ModelTableExpr(m.table).
IfNotExists().
Exec(ctx); err != nil {
return err
}
if m.useUpsert {
if _, err := m.db.NewCreateIndex().
Unique().
TableExpr(m.table).
Index(m.table + "_name_unique").
Column("name").
IfNotExists().
Exec(ctx); err != nil && !isIndexAlreadyExistsError(err) {
return err
}
}
if _, err := m.db.NewCreateTable().
Model((*migrationLock)(nil)).
ModelTableExpr(m.locksTable).
IfNotExists().
Exec(ctx); err != nil {
return err
}
return nil
}
// Reset drops and re-creates the migration tables.
func (m *Migrator) Reset(ctx context.Context) error {
if _, err := m.db.NewDropTable().
Model((*Migration)(nil)).
ModelTableExpr(m.table).
IfExists().
Exec(ctx); err != nil {
return err
}
if _, err := m.db.NewDropTable().
Model((*migrationLock)(nil)).
ModelTableExpr(m.locksTable).
IfExists().
Exec(ctx); err != nil {
return err
}
return m.Init(ctx)
}
// Migrate runs unapplied migrations. If a migration fails, migrate immediately exits.
func (m *Migrator) Migrate(ctx context.Context, opts ...MigrationOption) (*MigrationGroup, error) {
cfg := newMigrationConfig(opts)
group := new(MigrationGroup)
if err := m.validate(); err != nil {
return group, err
}
migrations, lastGroupID, err := m.migrationsWithStatus(ctx)
if err != nil {
return group, err
}
migrations = migrations.Unapplied()
if len(migrations) == 0 {
return group, nil
}
group.ID = lastGroupID + 1
for i := range migrations {
migration := &migrations[i]
migration.GroupID = group.ID
if !m.markAppliedOnSuccess {
if err := m.MarkApplied(ctx, migration); err != nil {
return group, err
}
}
group.Migrations = migrations[:i+1]
if !cfg.nop && migration.Up != nil {
if err := migration.Up(ctx, m, migration); err != nil {
return group, fmt.Errorf("%s: up: %w", migration.Name, err)
}
}
if m.markAppliedOnSuccess {
if err := m.MarkApplied(ctx, migration); err != nil {
return group, err
}
}
}
return group, nil
}
// RunMigration runs the up migration with the given name and marks it as applied.
// It runs the migration even if it is already marked as applied.
// The migration is added as a new applied record, creating a separate migration group.
func (m *Migrator) RunMigration(
ctx context.Context, migrationName string, opts ...MigrationOption,
) error {
cfg := newMigrationConfig(opts)
if err := m.validate(); err != nil {
return err
}
if migrationName == "" {
return errors.New("migrate: migration name cannot be empty")
}
if !m.useUpsert {
return errors.New("migrate: RunMigration requires WithUpsert(true)")
}
migrations, lastGroupID, err := m.migrationsWithStatus(ctx)
if err != nil {
return err
}
var migration *Migration
for i := range migrations {
if migrations[i].Name == migrationName {
migration = &migrations[i]
break
}
}
if migration == nil {
return fmt.Errorf("migrate: migration with name %q not found", migrationName)
}
if migration.Up == nil {
return fmt.Errorf("migrate: migration %s does not have up migration", migration.Name)
}
if cfg.nop {
return nil
}
migration.GroupID = lastGroupID + 1
if !m.markAppliedOnSuccess {
if err := m.MarkApplied(ctx, migration); err != nil {
return err
}
}
if err := migration.Up(ctx, m, migration); err != nil {
return fmt.Errorf("%s: up: %w", migration.Name, err)
}
if m.markAppliedOnSuccess {
if err := m.MarkApplied(ctx, migration); err != nil {
return err
}
}
return nil
}
// Rollback rolls back the last migration group.
func (m *Migrator) Rollback(ctx context.Context, opts ...MigrationOption) (*MigrationGroup, error) {
cfg := newMigrationConfig(opts)
lastGroup := new(MigrationGroup)
if err := m.validate(); err != nil {
return lastGroup, err
}
migrations, err := m.MigrationsWithStatus(ctx)
if err != nil {
return lastGroup, err
}
lastGroup = migrations.LastGroup()
for i := len(lastGroup.Migrations) - 1; i >= 0; i-- {
migration := &lastGroup.Migrations[i]
if !m.markAppliedOnSuccess {
if err := m.MarkUnapplied(ctx, migration); err != nil {
return lastGroup, err
}
}
if !cfg.nop && migration.Down != nil {
if err := migration.Down(ctx, m, migration); err != nil {
return lastGroup, fmt.Errorf("%s: down: %w", migration.Name, err)
}
}
if m.markAppliedOnSuccess {
if err := m.MarkUnapplied(ctx, migration); err != nil {
return lastGroup, err
}
}
}
return lastGroup, nil
}
type goMigrationConfig struct {
packageName string
goTemplate string
}
// GoMigrationOption configures Go migration file generation.
type GoMigrationOption func(cfg *goMigrationConfig)
// WithPackageName sets the Go package name used in generated migration files.
func WithPackageName(name string) GoMigrationOption {
return func(cfg *goMigrationConfig) {
cfg.packageName = name
}
}
// WithGoTemplate sets the Go template string used for generated migration files.
func WithGoTemplate(template string) GoMigrationOption {
return func(cfg *goMigrationConfig) {
cfg.goTemplate = template
}
}
// CreateGoMigration creates a Go migration file.
func (m *Migrator) CreateGoMigration(
ctx context.Context, name string, opts ...GoMigrationOption,
) (*MigrationFile, error) {
cfg := &goMigrationConfig{
packageName: "migrations",
goTemplate: goTemplate,
}
for _, opt := range opts {
opt(cfg)
}
name, err := genMigrationName(name)
if err != nil {
return nil, err
}
fname := name + ".go"
fpath := filepath.Join(m.migrations.getDirectory(), fname)
content := fmt.Sprintf(cfg.goTemplate, cfg.packageName)
if err := os.WriteFile(fpath, []byte(content), 0o644); err != nil {
return nil, err
}
mf := &MigrationFile{
Name: fname,
Path: fpath,
Content: content,
}
return mf, nil
}
// CreateTxSQLMigration creates transactional up and down SQL migration files.
func (m *Migrator) CreateTxSQLMigrations(ctx context.Context, name string) ([]*MigrationFile, error) {
name, err := genMigrationName(name)
if err != nil {
return nil, err
}
up, err := m.createSQL(ctx, name+".tx.up.sql", true)
if err != nil {
return nil, err
}
down, err := m.createSQL(ctx, name+".tx.down.sql", true)
if err != nil {
return nil, err
}
return []*MigrationFile{up, down}, nil
}
// CreateSQLMigrations creates up and down SQL migration files.
func (m *Migrator) CreateSQLMigrations(ctx context.Context, name string) ([]*MigrationFile, error) {
name, err := genMigrationName(name)
if err != nil {
return nil, err
}
up, err := m.createSQL(ctx, name+".up.sql", false)
if err != nil {
return nil, err
}
down, err := m.createSQL(ctx, name+".down.sql", false)
if err != nil {
return nil, err
}
return []*MigrationFile{up, down}, nil
}
func (m *Migrator) createSQL(_ context.Context, fname string, transactional bool) (*MigrationFile, error) {
fpath := filepath.Join(m.migrations.getDirectory(), fname)
template := sqlTemplate
if transactional {
template = transactionalSQLTemplate
}
if err := os.WriteFile(fpath, []byte(template), 0o644); err != nil {
return nil, err
}
mf := &MigrationFile{
Name: fname,
Path: fpath,
Content: goTemplate,
}
return mf, nil
}
var nameRE = regexp.MustCompile(`^[0-9a-z_\-]+$`)
func genMigrationName(name string) (string, error) {
const timeFormat = "20060102150405"
if name == "" {
return "", errors.New("migrate: migration name can't be empty")
}
if !nameRE.MatchString(name) {
return "", fmt.Errorf("migrate: invalid migration name: %q", name)
}
version := time.Now().UTC().Format(timeFormat)
return fmt.Sprintf("%s_%s", version, name), nil
}
// MarkApplied marks the migration as applied (completed).
func (m *Migrator) MarkApplied(ctx context.Context, migration *Migration) error {
q := m.db.NewInsert().Model(migration).
ModelTableExpr(m.table)
if m.useUpsert {
switch {
case m.db.HasFeature(feature.InsertOnConflict):
q = q.On("CONFLICT (name) DO UPDATE").
Set("group_id = EXCLUDED.group_id").
Set("migrated_at = EXCLUDED.migrated_at")
case m.db.HasFeature(feature.InsertOnDuplicateKey):
q = q.On("DUPLICATE KEY UPDATE").
Set("group_id = VALUES(group_id)").
Set("migrated_at = VALUES(migrated_at)")
case m.db.HasFeature(feature.Merge):
source := MigrationSlice{*migration}
_, err := m.db.NewMerge().
Model(migration).
ModelTableExpr("? AS migration", bun.Name(m.table)).
With("_data", m.db.NewValues(&source)).
Using("_data").
On("migration.name = _data.name").
WhenUpdate("MATCHED", func(q *bun.UpdateQuery) *bun.UpdateQuery {
return q.
Set("group_id = _data.group_id").
Set("migrated_at = _data.migrated_at")
}).
WhenInsert("NOT MATCHED", func(q *bun.InsertQuery) *bun.InsertQuery {
return q.
Value("name", "_data.name").
Value("group_id", "_data.group_id").
Value("migrated_at", "_data.migrated_at")
}).
Exec(ctx)
return err
default:
return errors.New("migrate: dialect does not support upsert or merge")
}
}
_, err := q.Exec(ctx)
return err
}
// MarkUnapplied marks the migration as unapplied (new).
func (m *Migrator) MarkUnapplied(ctx context.Context, migration *Migration) error {
_, err := m.db.NewDelete().
Model(migration).
ModelTableExpr(m.table).
Where("id = ?", migration.ID).
Exec(ctx)
return err
}
// TruncateTable removes all rows from the migrations table.
func (m *Migrator) TruncateTable(ctx context.Context) error {
_, err := m.db.NewTruncateTable().
Model((*Migration)(nil)).
ModelTableExpr(m.table).
Exec(ctx)
return err
}
// MissingMigrations returns applied migrations that can no longer be found.
func (m *Migrator) MissingMigrations(ctx context.Context) (MigrationSlice, error) {
applied, err := m.AppliedMigrations(ctx)
if err != nil {
return nil, err
}
existing := migrationMap(m.migrations.ms)
for i := len(applied) - 1; i >= 0; i-- {
m := &applied[i]
if _, ok := existing[m.Name]; ok {
applied = append(applied[:i], applied[i+1:]...)
}
}
return applied, nil
}
// AppliedMigrations returns applied (applied) migrations in descending order.
func (m *Migrator) AppliedMigrations(ctx context.Context) (MigrationSlice, error) {
var ms MigrationSlice
if err := m.db.NewSelect().
ColumnExpr("*").
Model(&ms).
ModelTableExpr(m.table).
Scan(ctx); err != nil {
return nil, err
}
return ms, nil
}
func (m *Migrator) formattedTableName(db *bun.DB) string {
return db.QueryGen().FormatQuery(m.table)
}
func (m *Migrator) validate() error {
if len(m.migrations.ms) == 0 {
return errors.New("migrate: there are no migrations")
}
return nil
}
func (m *Migrator) exec(
ctx context.Context, db bun.IConn, migration *Migration, queries []string,
) error {
if m.beforeMigrationHook != nil {
if err := m.beforeMigrationHook(ctx, db, migration); err != nil {
return err
}
}
for _, query := range queries {
if strings.TrimSpace(query) == "" {
continue
}
if _, err := db.ExecContext(ctx, query); err != nil {
return err
}
}
if m.afterMigrationHook != nil {
if err := m.afterMigrationHook(ctx, db, migration); err != nil {
return err
}
}
return nil
}
//------------------------------------------------------------------------------
type migrationLock struct {
ID int64 `bun:",pk,autoincrement"`
TableName string `bun:",unique"`
}
// Lock acquires an advisory lock on the migration table to prevent concurrent migrations.
func (m *Migrator) Lock(ctx context.Context) error {
lock := &migrationLock{
TableName: m.formattedTableName(m.db),
}
if _, err := m.db.NewInsert().
Model(lock).
ModelTableExpr(m.locksTable).
Exec(ctx); err != nil {
return fmt.Errorf("migrate: migrations table is already locked (%w)", err)
}
return nil
}
// Unlock releases the advisory lock on the migration table.
func (m *Migrator) Unlock(ctx context.Context) error {
tableName := m.formattedTableName(m.db)
_, err := m.db.NewDelete().
Model((*migrationLock)(nil)).
ModelTableExpr(m.locksTable).
Where("? = ?", bun.Ident("table_name"), tableName).
Exec(ctx)
return err
}
// isIndexAlreadyExistsError checks whether err indicates the index already exists.
// This is needed for dialects that do not support CREATE INDEX IF NOT EXISTS
// (e.g. MySQL, MSSQL), where a duplicate-index error is expected on repeated Init calls.
func isIndexAlreadyExistsError(err error) bool {
s := strings.ToLower(err.Error())
// MySQL: Error 1061: Duplicate key name '...'
// MSSQL: The index '...' already exists on table '...'
// Oracle: ORA-00955: name is already used by an existing object
return strings.Contains(s, "duplicate key name") || strings.Contains(s, "already exist")
}
func migrationMap(ms MigrationSlice) map[string]*Migration {
mp := make(map[string]*Migration)
for i := range ms {
m := &ms[i]
mp[m.Name] = m
}
return mp
}
+339
View File
@@ -0,0 +1,339 @@
package migrate
import (
"fmt"
"github.com/uptrace/bun/migrate/sqlschema"
)
// Operation encapsulates the request to change a database definition
// and knowns which operation can revert it.
//
// It is useful to define "monolith" Operations whenever possible,
// even though they a dialect may require several distinct steps to apply them.
// For example, changing a primary key involves first dropping the old constraint
// before generating the new one. Yet, this is only an implementation detail and
// passing a higher-level ChangePrimaryKeyOp will give the dialect more information
// about the applied change.
//
// Some operations might be irreversible due to technical limitations. Returning
// a *comment from GetReverse() will add an explanatory note to the generate migration file.
//
// To declare dependency on another Operation, operations should implement
// { DependsOn(Operation) bool } interface, which Changeset will use to resolve dependencies.
type Operation interface {
GetReverse() Operation
}
// CreateTableOp creates a new table in the schema.
//
// It does not report dependency on any other migration and may be executed first.
// Make sure the dialect does not include FOREIGN KEY constraints in the CREATE TABLE
// statement, as those may potentially reference not-yet-existing columns/tables.
type CreateTableOp struct {
TableName string
Model any
}
var _ Operation = (*CreateTableOp)(nil)
func (op *CreateTableOp) GetReverse() Operation {
return &DropTableOp{TableName: op.TableName}
}
// DropTableOp drops a database table. This operation is not reversible.
type DropTableOp struct {
TableName string
}
var _ Operation = (*DropTableOp)(nil)
func (op *DropTableOp) DependsOn(another Operation) bool {
drop, ok := another.(*DropForeignKeyOp)
return ok && drop.ForeignKey.DependsOnTable(op.TableName)
}
// GetReverse for a DropTable returns a no-op migration. Logically, CreateTable is the reverse,
// but DropTable does not have the table's definition to create one.
func (op *DropTableOp) GetReverse() Operation {
c := Unimplemented(fmt.Sprintf("WARNING: \"DROP TABLE %s\" cannot be reversed automatically because table definition is not available", op.TableName))
return &c
}
// RenameTableOp renames the table. Changing the "schema" part of the table's FQN (moving tables between schemas) is not allowed.
type RenameTableOp struct {
TableName string
NewName string
}
var _ Operation = (*RenameTableOp)(nil)
func (op *RenameTableOp) GetReverse() Operation {
return &RenameTableOp{
TableName: op.NewName,
NewName: op.TableName,
}
}
// RenameColumnOp renames a column in the table. If the changeset includes a rename operation
// for the column's table, it should be executed first.
type RenameColumnOp struct {
TableName string
OldName string
NewName string
}
var _ Operation = (*RenameColumnOp)(nil)
func (op *RenameColumnOp) GetReverse() Operation {
return &RenameColumnOp{
TableName: op.TableName,
OldName: op.NewName,
NewName: op.OldName,
}
}
func (op *RenameColumnOp) DependsOn(another Operation) bool {
rename, ok := another.(*RenameTableOp)
return ok && op.TableName == rename.NewName
}
// AddColumnOp adds a new column to the table.
type AddColumnOp struct {
TableName string
ColumnName string
Column sqlschema.Column
}
var _ Operation = (*AddColumnOp)(nil)
func (op *AddColumnOp) GetReverse() Operation {
return &DropColumnOp{
TableName: op.TableName,
ColumnName: op.ColumnName,
Column: op.Column,
}
}
// DropColumnOp drop a column from the table.
//
// While some dialects allow DROP CASCADE to drop dependent constraints,
// explicit handling on constraints is preferred for transparency and debugging.
// DropColumnOp depends on DropForeignKeyOp, DropPrimaryKeyOp, and ChangePrimaryKeyOp
// if any of the constraints is defined on this table.
type DropColumnOp struct {
TableName string
ColumnName string
Column sqlschema.Column
}
var _ Operation = (*DropColumnOp)(nil)
func (op *DropColumnOp) GetReverse() Operation {
return &AddColumnOp{
TableName: op.TableName,
ColumnName: op.ColumnName,
Column: op.Column,
}
}
func (op *DropColumnOp) DependsOn(another Operation) bool {
switch drop := another.(type) {
case *DropForeignKeyOp:
return drop.ForeignKey.DependsOnColumn(op.TableName, op.ColumnName)
case *DropPrimaryKeyOp:
return op.TableName == drop.TableName && drop.PrimaryKey.Columns.Contains(op.ColumnName)
case *ChangePrimaryKeyOp:
return op.TableName == drop.TableName && drop.Old.Columns.Contains(op.ColumnName)
}
return false
}
// AddForeignKey adds a new FOREIGN KEY constraint.
type AddForeignKeyOp struct {
ForeignKey sqlschema.ForeignKey
ConstraintName string
}
var _ Operation = (*AddForeignKeyOp)(nil)
func (op *AddForeignKeyOp) TableName() string {
return op.ForeignKey.From.TableName
}
func (op *AddForeignKeyOp) DependsOn(another Operation) bool {
switch another := another.(type) {
case *RenameTableOp:
return op.ForeignKey.DependsOnTable(another.TableName) || op.ForeignKey.DependsOnTable(another.NewName)
case *CreateTableOp:
return op.ForeignKey.DependsOnTable(another.TableName)
}
return false
}
func (op *AddForeignKeyOp) GetReverse() Operation {
return &DropForeignKeyOp{
ForeignKey: op.ForeignKey,
ConstraintName: op.ConstraintName,
}
}
// DropForeignKeyOp drops a FOREIGN KEY constraint.
type DropForeignKeyOp struct {
ForeignKey sqlschema.ForeignKey
ConstraintName string
}
var _ Operation = (*DropForeignKeyOp)(nil)
func (op *DropForeignKeyOp) TableName() string {
return op.ForeignKey.From.TableName
}
func (op *DropForeignKeyOp) GetReverse() Operation {
return &AddForeignKeyOp{
ForeignKey: op.ForeignKey,
ConstraintName: op.ConstraintName,
}
}
// AddUniqueConstraintOp adds new UNIQUE constraint to the table.
type AddUniqueConstraintOp struct {
TableName string
Unique sqlschema.Unique
}
var _ Operation = (*AddUniqueConstraintOp)(nil)
func (op *AddUniqueConstraintOp) GetReverse() Operation {
return &DropUniqueConstraintOp{
TableName: op.TableName,
Unique: op.Unique,
}
}
func (op *AddUniqueConstraintOp) DependsOn(another Operation) bool {
switch another := another.(type) {
case *AddColumnOp:
return op.TableName == another.TableName && op.Unique.Columns.Contains(another.ColumnName)
case *RenameTableOp:
return op.TableName == another.NewName
case *DropUniqueConstraintOp:
// We want to drop the constraint with the same name before adding this one.
return op.TableName == another.TableName && op.Unique.Name == another.Unique.Name
default:
return false
}
}
// DropUniqueConstraintOp drops a UNIQUE constraint.
type DropUniqueConstraintOp struct {
TableName string
Unique sqlschema.Unique
}
var _ Operation = (*DropUniqueConstraintOp)(nil)
func (op *DropUniqueConstraintOp) DependsOn(another Operation) bool {
if rename, ok := another.(*RenameTableOp); ok {
return op.TableName == rename.NewName
}
return false
}
func (op *DropUniqueConstraintOp) GetReverse() Operation {
return &AddUniqueConstraintOp{
TableName: op.TableName,
Unique: op.Unique,
}
}
// ChangeColumnTypeOp set a new data type for the column.
// The two types should be such that the data can be auto-casted from one to another.
// E.g. reducing VARCHAR lenght is not possible in most dialects.
// AutoMigrator does not enforce or validate these rules.
type ChangeColumnTypeOp struct {
TableName string
Column string
From sqlschema.Column
To sqlschema.Column
}
var _ Operation = (*ChangeColumnTypeOp)(nil)
func (op *ChangeColumnTypeOp) GetReverse() Operation {
return &ChangeColumnTypeOp{
TableName: op.TableName,
Column: op.Column,
From: op.To,
To: op.From,
}
}
// DropPrimaryKeyOp drops the table's PRIMARY KEY.
type DropPrimaryKeyOp struct {
TableName string
PrimaryKey sqlschema.PrimaryKey
}
var _ Operation = (*DropPrimaryKeyOp)(nil)
func (op *DropPrimaryKeyOp) GetReverse() Operation {
return &AddPrimaryKeyOp{
TableName: op.TableName,
PrimaryKey: op.PrimaryKey,
}
}
// AddPrimaryKeyOp adds a new PRIMARY KEY to the table.
type AddPrimaryKeyOp struct {
TableName string
PrimaryKey sqlschema.PrimaryKey
}
var _ Operation = (*AddPrimaryKeyOp)(nil)
func (op *AddPrimaryKeyOp) GetReverse() Operation {
return &DropPrimaryKeyOp{
TableName: op.TableName,
PrimaryKey: op.PrimaryKey,
}
}
func (op *AddPrimaryKeyOp) DependsOn(another Operation) bool {
switch another := another.(type) {
case *AddColumnOp:
return op.TableName == another.TableName && op.PrimaryKey.Columns.Contains(another.ColumnName)
}
return false
}
// ChangePrimaryKeyOp changes the PRIMARY KEY of the table.
type ChangePrimaryKeyOp struct {
TableName string
Old sqlschema.PrimaryKey
New sqlschema.PrimaryKey
}
var _ Operation = (*AddPrimaryKeyOp)(nil)
func (op *ChangePrimaryKeyOp) GetReverse() Operation {
return &ChangePrimaryKeyOp{
TableName: op.TableName,
Old: op.New,
New: op.Old,
}
}
// Unimplemented denotes an Operation that cannot be executed.
//
// Operations, which cannot be reversed due to current technical limitations,
// may have their GetReverse() return &Unimplemented with a helpful message.
//
// When applying operations, changelog should skip it or output as a log message,
// and write it as an SQL Unimplemented when creating migration files.
type Unimplemented string
var _ Operation = (*Unimplemented)(nil)
func (reason *Unimplemented) GetReverse() Operation { return reason }
+75
View File
@@ -0,0 +1,75 @@
package sqlschema
import (
"fmt"
"github.com/uptrace/bun/schema"
)
type Column interface {
GetName() string
GetSQLType() string
GetVarcharLen() int
GetDefaultValue() string
GetIsNullable() bool
GetIsAutoIncrement() bool
GetIsIdentity() bool
AppendQuery(schema.QueryGen, []byte) ([]byte, error)
}
var _ Column = (*BaseColumn)(nil)
// BaseColumn is a base column definition that stores various attributes of a column.
//
// Dialects and only dialects can use it to implement the Column interface.
// Other packages must use the Column interface.
type BaseColumn struct {
Name string
SQLType string
VarcharLen int
DefaultValue string
IsNullable bool
IsAutoIncrement bool
IsIdentity bool
// TODO: add Precision and Cardinality for timestamps/bit-strings/floats and arrays respectively.
}
func (cd BaseColumn) GetName() string {
return cd.Name
}
func (cd BaseColumn) GetSQLType() string {
return cd.SQLType
}
func (cd BaseColumn) GetVarcharLen() int {
return cd.VarcharLen
}
func (cd BaseColumn) GetDefaultValue() string {
return cd.DefaultValue
}
func (cd BaseColumn) GetIsNullable() bool {
return cd.IsNullable
}
func (cd BaseColumn) GetIsAutoIncrement() bool {
return cd.IsAutoIncrement
}
func (cd BaseColumn) GetIsIdentity() bool {
return cd.IsIdentity
}
// AppendQuery appends full SQL data type.
func (c *BaseColumn) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
b = append(b, c.SQLType...)
if c.VarcharLen == 0 {
return b, nil
}
b = append(b, "("...)
b = append(b, fmt.Sprint(c.VarcharLen)...)
b = append(b, ")"...)
return b, nil
}
+128
View File
@@ -0,0 +1,128 @@
package sqlschema
import (
"slices"
"strings"
"github.com/uptrace/bun/schema"
)
type Database interface {
GetTables() []Table
GetForeignKeys() map[ForeignKey]string
}
var _ Database = (*BaseDatabase)(nil)
// BaseDatabase is a base database definition.
//
// Dialects and only dialects can use it to implement the Database interface.
// Other packages must use the Database interface.
type BaseDatabase struct {
Tables []Table
ForeignKeys map[ForeignKey]string
}
func (ds BaseDatabase) GetTables() []Table {
return ds.Tables
}
func (ds BaseDatabase) GetForeignKeys() map[ForeignKey]string {
return ds.ForeignKeys
}
// ForeignKey represents a foreign key constraint between two tables.
type ForeignKey struct {
From ColumnReference
To ColumnReference
}
func NewColumnReference(tableName string, columns ...string) ColumnReference {
return ColumnReference{
TableName: tableName,
Column: NewColumns(columns...),
}
}
func (fk ForeignKey) DependsOnTable(tableName string) bool {
return fk.From.TableName == tableName || fk.To.TableName == tableName
}
func (fk ForeignKey) DependsOnColumn(tableName string, column string) bool {
return fk.DependsOnTable(tableName) &&
(fk.From.Column.Contains(column) || fk.To.Column.Contains(column))
}
// Columns is a hashable representation of []string used to define schema constraints that depend on multiple columns.
// Although having duplicated column references in these constraints is illegal, Columns neither validates nor enforces this constraint on the caller.
type Columns string
// NewColumns creates a composite column from a slice of column names.
func NewColumns(columns ...string) Columns {
slices.Sort(columns)
return Columns(strings.Join(columns, ","))
}
func (c *Columns) String() string {
return string(*c)
}
func (c *Columns) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error) {
return schema.Safe(*c).AppendQuery(gen, b)
}
// Split returns a slice of column names that make up the composite.
func (c *Columns) Split() []string {
return strings.Split(c.String(), ",")
}
// ContainsColumns checks that columns in "other" are a subset of current colums.
func (c *Columns) ContainsColumns(other Columns) bool {
columns := c.Split()
Outer:
for _, check := range other.Split() {
for _, column := range columns {
if check == column {
continue Outer
}
}
return false
}
return true
}
// Contains checks that a composite column contains the current column.
func (c *Columns) Contains(other string) bool {
return c.ContainsColumns(Columns(other))
}
// Replace renames a column if it is part of the composite.
// If a composite consists of multiple columns, only one column will be renamed.
func (c *Columns) Replace(oldColumn, newColumn string) bool {
columns := c.Split()
for i, column := range columns {
if column == oldColumn {
columns[i] = newColumn
*c = NewColumns(columns...)
return true
}
}
return false
}
// Unique represents a unique constraint defined on 1 or more columns.
type Unique struct {
Name string
Columns Columns
}
// Equals checks that two unique constraint are the same, assuming both are defined for the same table.
func (u Unique) Equals(other Unique) bool {
return u.Columns == other.Columns
}
// ColumnReference identifies a column or set of columns in a table.
type ColumnReference struct {
TableName string
Column Columns
}
+274
View File
@@ -0,0 +1,274 @@
package sqlschema
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/uptrace/bun"
"github.com/uptrace/bun/schema"
)
type InspectorDialect interface {
schema.Dialect
// Inspector returns a new instance of Inspector for the dialect.
// Dialects MAY set their default InspectorConfig values in constructor
// but MUST apply InspectorOptions to ensure they can be overriden.
//
// Use ApplyInspectorOptions to reduce boilerplate.
NewInspector(db *bun.DB, options ...InspectorOption) Inspector
// CompareType returns true if col1 and co2 SQL types are equivalent,
// i.e. they might use dialect-specifc type aliases (SERIAL ~ SMALLINT)
// or specify the same VARCHAR length differently (VARCHAR(255) ~ VARCHAR).
CompareType(Column, Column) bool
}
// InspectorConfig controls the scope of migration by limiting the objects Inspector should return.
// Inspectors SHOULD use the configuration directly instead of copying it, or MAY choose to embed it,
// to make sure options are always applied correctly.
//
// ExcludeTables and ExcludeForeignKeys are intended for database inspectors,
// to compensate for the fact that model structs may not wholly reflect the
// state of the database schema.
// Database inspectors MUST respect these exclusions to prevent relations
// from being dropped unintentionally.
type InspectorConfig struct {
// SchemaName limits inspection to tables in a particular schema.
SchemaName string
// ExcludeTables from inspection. Patterns MAY make use of wildcards
// like % and _ and dialects MUST acknowledge that by using them
// with the SQL LIKE operator.
ExcludeTables []string
// ExcludeForeignKeys from inspection.
ExcludeForeignKeys map[ForeignKey]string
}
// Inspector reads schema state.
type Inspector interface {
Inspect(ctx context.Context) (Database, error)
}
func WithSchemaName(schemaName string) InspectorOption {
return func(cfg *InspectorConfig) {
cfg.SchemaName = schemaName
}
}
// WithExcludeTables forces inspector to exclude tables from the reported schema state.
// It works in append-only mode, i.e. tables cannot be re-included.
//
// Patterns MAY make use of % and _ wildcards, as if writing a LIKE clause in SQL.
func WithExcludeTables(tables ...string) InspectorOption {
return func(cfg *InspectorConfig) {
cfg.ExcludeTables = append(cfg.ExcludeTables, tables...)
}
}
// WithExcludeForeignKeys forces inspector to exclude foreign keys
// from the reported schema state.
func WithExcludeForeignKeys(fks ...ForeignKey) InspectorOption {
return func(cfg *InspectorConfig) {
for _, fk := range fks {
cfg.ExcludeForeignKeys[fk] = ""
}
}
}
// NewInspector creates a new database inspector, if the dialect supports it.
func NewInspector(db *bun.DB, options ...InspectorOption) (Inspector, error) {
dialect, ok := (db.Dialect()).(InspectorDialect)
if !ok {
return nil, fmt.Errorf("%s does not implement sqlschema.Inspector", db.Dialect().Name())
}
return &inspector{
Inspector: dialect.NewInspector(db, options...),
}, nil
}
func NewBunModelInspector(tables *schema.Tables, options ...InspectorOption) *BunModelInspector {
bmi := &BunModelInspector{
tables: tables,
}
ApplyInspectorOptions(&bmi.InspectorConfig, options...)
return bmi
}
// InspectorOption configures an Inspector.
type InspectorOption func(*InspectorConfig)
// ApplyInspectorOptions applies the given options to an InspectorConfig.
func ApplyInspectorOptions(cfg *InspectorConfig, options ...InspectorOption) {
if cfg.ExcludeForeignKeys == nil {
cfg.ExcludeForeignKeys = make(map[ForeignKey]string)
}
for _, opt := range options {
opt(cfg)
}
}
// inspector is opaque pointer to a database inspector.
type inspector struct {
Inspector
}
// BunModelInspector creates the current project state from the passed bun.Models.
// Do not recycle BunModelInspector for different sets of models, as older models will not be de-registerred before the next run.
//
// BunModelInspector does not know which the database's dialect, so it does not
// assume any default schema name. Always specify the target schema name via
// WithSchemaName option to receive meaningful results.
type BunModelInspector struct {
InspectorConfig
tables *schema.Tables
}
var _ Inspector = (*BunModelInspector)(nil)
func (bmi *BunModelInspector) Inspect(ctx context.Context) (Database, error) {
state := BunModelSchema{
BaseDatabase: BaseDatabase{
ForeignKeys: make(map[ForeignKey]string),
},
}
for _, t := range bmi.tables.All() {
if t.Schema != bmi.SchemaName {
continue
}
var columns []Column
for _, f := range t.Fields {
sqlType, length, err := parseLen(f.CreateTableSQLType)
if err != nil {
return nil, fmt.Errorf("parse length in %q: %w", f.CreateTableSQLType, err)
}
columns = append(columns, &BaseColumn{
Name: f.Name,
SQLType: strings.ToLower(sqlType), // TODO(dyma): maybe this is not necessary after Column.Eq()
VarcharLen: length,
DefaultValue: exprOrLiteral(f.SQLDefault),
IsNullable: !f.NotNull,
IsAutoIncrement: f.AutoIncrement,
IsIdentity: f.Identity,
})
}
var unique []Unique
for name, group := range t.Unique {
// Create a separate unique index for single-column unique constraints
// let each dialect apply the default naming convention.
if name == "" {
for _, f := range group {
unique = append(unique, Unique{Columns: NewColumns(f.Name)})
}
continue
}
// Set the name if it is a "unique group", in which case the user has provided the name.
var columns []string
for _, f := range group {
columns = append(columns, f.Name)
}
unique = append(unique, Unique{Name: name, Columns: NewColumns(columns...)})
}
var pk *PrimaryKey
if len(t.PKs) > 0 {
var columns []string
for _, f := range t.PKs {
columns = append(columns, f.Name)
}
pk = &PrimaryKey{Columns: NewColumns(columns...)}
}
// In cases where a table is defined in a non-default schema in the `bun:table` tag,
// schema.Table only extracts the name of the schema, but passes the entire tag value to t.Name
// for backwads-compatibility. For example, a bun model like this:
// type Model struct { bun.BaseModel `bun:"table:favourite.books` }
// produces
// schema.Table{ Schema: "favourite", Name: "favourite.books" }
tableName := strings.TrimPrefix(t.Name, t.Schema+".")
state.Tables = append(state.Tables, &BunTable{
BaseTable: BaseTable{
Schema: t.Schema,
Name: tableName,
Columns: columns,
UniqueConstraints: unique,
PrimaryKey: pk,
},
Model: t.ZeroIface,
})
for _, rel := range t.Relations {
// These relations are nominal and do not need a foreign key to be declared in the current table.
// They will be either expressed as N:1 relations in an m2m mapping table, or will be referenced by the other table if it's a 1:N.
if rel.Type == schema.ManyToManyRelation ||
rel.Type == schema.HasManyRelation {
continue
}
var fromCols, toCols []string
for _, f := range rel.BasePKs {
fromCols = append(fromCols, f.Name)
}
for _, f := range rel.JoinPKs {
toCols = append(toCols, f.Name)
}
target := rel.JoinTable
state.ForeignKeys[ForeignKey{
From: NewColumnReference(t.Name, fromCols...),
To: NewColumnReference(target.Name, toCols...),
}] = ""
}
}
return state, nil
}
func parseLen(typ string) (string, int, error) {
paren := strings.Index(typ, "(")
if paren == -1 {
return typ, 0, nil
}
length, err := strconv.Atoi(typ[paren+1 : len(typ)-1])
if err != nil {
return typ, 0, err
}
return typ[:paren], length, nil
}
// exprOrLiteral converts string to lowercase, if it does not contain a string literal 'lit'
// and trims the surrounding ” otherwise.
// Use it to ensure that user-defined default values in the models are always comparable
// to those returned by the database inspector, regardless of the case convention in individual drivers.
func exprOrLiteral(s string) string {
if strings.HasPrefix(s, "'") && strings.HasSuffix(s, "'") {
return strings.Trim(s, "'")
}
return strings.ToLower(s)
}
// BunModelSchema is the schema state derived from bun table models.
type BunModelSchema struct {
BaseDatabase
Tables []Table
}
func (ms BunModelSchema) GetTables() []Table {
return ms.Tables
}
// BunTable provides additional table metadata that is only accessible from scanning bun models.
type BunTable struct {
BaseTable
// Model stores the zero interface to the underlying Go struct.
Model any
}
+51
View File
@@ -0,0 +1,51 @@
package sqlschema
import (
"fmt"
"github.com/uptrace/bun"
"github.com/uptrace/bun/schema"
)
// MigratorDialect is a Dialect that can create a Migrator for executing schema changes.
type MigratorDialect interface {
schema.Dialect
NewMigrator(db *bun.DB, schemaName string) Migrator
}
// Migrator renders schema-change operations as SQL.
type Migrator interface {
AppendSQL(b []byte, operation any) ([]byte, error)
}
// migrator is a dialect-agnostic wrapper for sqlschema.MigratorDialect.
type migrator struct {
Migrator
}
func NewMigrator(db *bun.DB, schemaName string) (Migrator, error) {
md, ok := db.Dialect().(MigratorDialect)
if !ok {
return nil, fmt.Errorf("%q dialect does not implement sqlschema.Migrator", db.Dialect().Name())
}
return &migrator{
Migrator: md.NewMigrator(db, schemaName),
}, nil
}
// BaseMigrator can be embeded by dialect's Migrator implementations to re-use some of the existing bun queries.
type BaseMigrator struct {
db *bun.DB
}
func NewBaseMigrator(db *bun.DB) *BaseMigrator {
return &BaseMigrator{db: db}
}
func (m *BaseMigrator) AppendCreateTable(b []byte, model any) ([]byte, error) {
return m.db.NewCreateTable().Model(model).AppendQuery(m.db.QueryGen(), b)
}
func (m *BaseMigrator) AppendDropTable(b []byte, schemaName, tableName string) ([]byte, error) {
return m.db.NewDropTable().TableExpr("?.?", bun.Ident(schemaName), bun.Ident(tableName)).AppendQuery(m.db.QueryGen(), b)
}
+56
View File
@@ -0,0 +1,56 @@
package sqlschema
type Table interface {
GetSchema() string
GetName() string
GetColumns() []Column
GetPrimaryKey() *PrimaryKey
GetUniqueConstraints() []Unique
}
var _ Table = (*BaseTable)(nil)
// BaseTable is a base table definition.
//
// Dialects and only dialects can use it to implement the Table interface.
// Other packages must use the Table interface.
type BaseTable struct {
Schema string
Name string
// ColumnDefinitions map each column name to the column definition.
Columns []Column
// PrimaryKey holds the primary key definition.
// A nil value means that no primary key is defined for the table.
PrimaryKey *PrimaryKey
// UniqueConstraints defined on the table.
UniqueConstraints []Unique
}
// PrimaryKey represents a primary key constraint defined on 1 or more columns.
type PrimaryKey struct {
Name string
Columns Columns
}
func (td *BaseTable) GetSchema() string {
return td.Schema
}
func (td *BaseTable) GetName() string {
return td.Name
}
func (td *BaseTable) GetColumns() []Column {
return td.Columns
}
func (td *BaseTable) GetPrimaryKey() *PrimaryKey {
return td.PrimaryKey
}
func (td *BaseTable) GetUniqueConstraints() []Unique {
return td.UniqueConstraints
}
+210
View File
@@ -0,0 +1,210 @@
package bun
import (
"context"
"database/sql"
"errors"
"fmt"
"reflect"
"time"
"github.com/uptrace/bun/schema"
)
var errNilModel = errors.New("bun: Model(nil)")
var (
timeType = reflect.TypeFor[time.Time]()
bytesType = reflect.TypeFor[[]byte]()
)
// Model is implemented by all Bun models.
type Model = schema.Model
type rowScanner interface {
ScanRow(ctx context.Context, rows *sql.Rows) error
}
// TableModel describes models that map to database tables and support query lifecycle hooks.
type TableModel interface {
Model
schema.BeforeAppendModelHook
schema.BeforeScanRowHook
schema.AfterScanRowHook
ScanColumn(column string, src any) error
Table() *schema.Table
Relation() *schema.Relation
join(string) *relationJoin
getJoin(string) *relationJoin
getJoins() []relationJoin
addJoin(relationJoin) *relationJoin
clone() TableModel
rootValue() reflect.Value
parentIndex() []int
mount(reflect.Value)
updateSoftDeleteField(time.Time) error
}
func newModel(db *DB, dest []any) (Model, error) {
if len(dest) == 1 {
return _newModel(db, dest[0], true)
}
values := make([]reflect.Value, len(dest))
for i, el := range dest {
v := reflect.ValueOf(el)
if v.Kind() != reflect.Ptr {
return nil, fmt.Errorf("bun: Scan(non-pointer %T)", dest)
}
v = v.Elem()
if v.Kind() != reflect.Slice {
return newScanModel(db, dest), nil
}
values[i] = v
}
return newSliceModel(db, dest, values), nil
}
func newSingleModel(db *DB, dest any) (Model, error) {
return _newModel(db, dest, false)
}
func _newModel(db *DB, dest any, scan bool) (Model, error) {
switch dest := dest.(type) {
case nil:
return nil, errNilModel
case Model:
return dest, nil
case sql.Scanner:
if !scan {
return nil, fmt.Errorf("bun: Model(unsupported %T)", dest)
}
return newScanModel(db, []any{dest}), nil
}
v := reflect.ValueOf(dest)
if !v.IsValid() {
return nil, errNilModel
}
if v.Kind() != reflect.Ptr {
return nil, fmt.Errorf("bun: Model(non-pointer %T)", dest)
}
if v.IsNil() {
typ := v.Type().Elem()
if typ.Kind() == reflect.Struct {
return newStructTableModel(db, dest, db.Table(typ)), nil
}
return nil, fmt.Errorf("bun: Model(nil %s %T)", typ.Kind(), dest)
}
v = v.Elem()
typ := v.Type()
switch typ {
case timeType, bytesType:
return newScanModel(db, []any{dest}), nil
}
switch v.Kind() {
case reflect.Map:
if err := validMap(typ); err != nil {
return nil, err
}
mapPtr := v.Addr().Interface().(*map[string]any)
return newMapModel(db, mapPtr), nil
case reflect.Struct:
return newStructTableModelValue(db, dest, v), nil
case reflect.Slice:
switch elemType := sliceElemType(v); elemType.Kind() {
case reflect.Struct:
if elemType != timeType {
return newSliceTableModel(db, dest, v, elemType), nil
}
case reflect.Map:
if err := validMap(elemType); err != nil {
return nil, err
}
slicePtr := v.Addr().Interface().(*[]map[string]any)
return newMapSliceModel(db, slicePtr), nil
}
return newSliceModel(db, []any{dest}, []reflect.Value{v}), nil
}
if scan {
return newScanModel(db, []any{dest}), nil
}
return nil, fmt.Errorf("bun: Model(unsupported %T)", dest)
}
func newTableModelIndex(
db *DB,
table *schema.Table,
root reflect.Value,
index []int,
rel *schema.Relation,
) (TableModel, error) {
typ := typeByIndex(table.Type, index)
if typ.Kind() == reflect.Struct {
return &structTableModel{
db: db,
table: table.Dialect().Tables().Get(typ),
rel: rel,
root: root,
index: index,
}, nil
}
if typ.Kind() == reflect.Slice {
structType := indirectType(typ.Elem())
if structType.Kind() == reflect.Struct {
m := sliceTableModel{
structTableModel: structTableModel{
db: db,
table: table.Dialect().Tables().Get(structType),
rel: rel,
root: root,
index: index,
},
}
m.init(typ)
return &m, nil
}
}
return nil, fmt.Errorf("bun: NewModel(%s)", typ)
}
func validMap(typ reflect.Type) error {
if typ.Key().Kind() != reflect.String || typ.Elem().Kind() != reflect.Interface {
return fmt.Errorf("bun: Model(unsupported %s) (expected *map[string]any)",
typ)
}
return nil
}
//------------------------------------------------------------------------------
func isSingleRowModel(m Model) bool {
switch m.(type) {
case *mapModel,
*structTableModel,
*scanModel:
return true
default:
return false
}
}
+186
View File
@@ -0,0 +1,186 @@
package bun
import (
"bytes"
"context"
"database/sql"
"reflect"
"slices"
"github.com/uptrace/bun/schema"
)
type mapModel struct {
db *DB
dest *map[string]any
m map[string]any
rows *sql.Rows
columns []string
_columnTypes []*sql.ColumnType
scanIndex int
}
var _ Model = (*mapModel)(nil)
func newMapModel(db *DB, dest *map[string]any) *mapModel {
m := &mapModel{
db: db,
dest: dest,
}
if dest != nil {
m.m = *dest
}
return m
}
func (m *mapModel) Value() any {
return m.dest
}
func (m *mapModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
if !rows.Next() {
return 0, rows.Err()
}
columns, err := rows.Columns()
if err != nil {
return 0, err
}
m.rows = rows
m.columns = columns
dest := makeDest(m, len(columns))
if m.m == nil {
m.m = make(map[string]any, len(m.columns))
}
m.scanIndex = 0
if err := rows.Scan(dest...); err != nil {
return 0, err
}
*m.dest = m.m
return 1, nil
}
func (m *mapModel) Scan(src any) error {
if _, ok := src.([]byte); !ok {
return m.scanRaw(src)
}
columnTypes, err := m.columnTypes()
if err != nil {
return err
}
scanType := columnTypes[m.scanIndex].ScanType()
switch scanType.Kind() {
case reflect.Interface:
return m.scanRaw(src)
case reflect.Slice:
if scanType.Elem().Kind() == reflect.Uint8 {
// Reference types such as []byte are only valid until the next call to Scan.
src := bytes.Clone(src.([]byte))
return m.scanRaw(src)
}
}
dest := reflect.New(scanType).Elem()
if err := schema.Scanner(scanType)(dest, src); err != nil {
return err
}
return m.scanRaw(dest.Interface())
}
func (m *mapModel) columnTypes() ([]*sql.ColumnType, error) {
if m._columnTypes == nil {
columnTypes, err := m.rows.ColumnTypes()
if err != nil {
return nil, err
}
m._columnTypes = columnTypes
}
return m._columnTypes, nil
}
func (m *mapModel) scanRaw(src any) error {
columnName := m.columns[m.scanIndex]
m.scanIndex++
m.m[columnName] = src
return nil
}
func (m *mapModel) appendColumnsValues(gen schema.QueryGen, b []byte) []byte {
keys := make([]string, 0, len(m.m))
for k := range m.m {
keys = append(keys, k)
}
slices.Sort(keys)
b = append(b, " ("...)
for i, k := range keys {
if i > 0 {
b = append(b, ", "...)
}
b = gen.AppendIdent(b, k)
}
b = append(b, ") VALUES ("...)
isTemplate := gen.IsNop()
for i, k := range keys {
if i > 0 {
b = append(b, ", "...)
}
if isTemplate {
b = append(b, '?')
} else {
b = gen.Append(b, m.m[k])
}
}
b = append(b, ")"...)
return b
}
func (m *mapModel) appendSet(gen schema.QueryGen, b []byte) []byte {
keys := make([]string, 0, len(m.m))
for k := range m.m {
keys = append(keys, k)
}
slices.Sort(keys)
isTemplate := gen.IsNop()
for i, k := range keys {
if i > 0 {
b = append(b, ", "...)
}
b = gen.AppendIdent(b, k)
b = append(b, " = "...)
if isTemplate {
b = append(b, '?')
} else {
b = gen.Append(b, m.m[k])
}
}
return b
}
func makeDest(v any, n int) []any {
dest := make([]any, n)
for i := range dest {
dest[i] = v
}
return dest
}
+153
View File
@@ -0,0 +1,153 @@
package bun
import (
"context"
"database/sql"
"errors"
"slices"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/schema"
)
type mapSliceModel struct {
mapModel
dest *[]map[string]any
keys []string
}
var _ Model = (*mapSliceModel)(nil)
func newMapSliceModel(db *DB, dest *[]map[string]any) *mapSliceModel {
return &mapSliceModel{
mapModel: mapModel{
db: db,
},
dest: dest,
}
}
func (m *mapSliceModel) Value() any {
return m.dest
}
func (m *mapSliceModel) SetCap(cap int) {
if cap > 100 {
cap = 100
}
if slice := *m.dest; len(slice) < cap {
*m.dest = make([]map[string]any, 0, cap)
}
}
func (m *mapSliceModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
columns, err := rows.Columns()
if err != nil {
return 0, err
}
m.rows = rows
m.columns = columns
dest := makeDest(m, len(columns))
slice := *m.dest
if len(slice) > 0 {
slice = slice[:0]
}
var n int
for rows.Next() {
m.m = make(map[string]any, len(m.columns))
m.scanIndex = 0
if err := rows.Scan(dest...); err != nil {
return 0, err
}
slice = append(slice, m.m)
n++
}
if err := rows.Err(); err != nil {
return 0, err
}
*m.dest = slice
return n, nil
}
func (m *mapSliceModel) appendColumns(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if err := m.initKeys(); err != nil {
return nil, err
}
for i, k := range m.keys {
if i > 0 {
b = append(b, ", "...)
}
b = gen.AppendIdent(b, k)
}
return b, nil
}
func (m *mapSliceModel) appendValues(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if err := m.initKeys(); err != nil {
return nil, err
}
slice := *m.dest
if gen.IsNop() {
for i := range m.keys {
if i > 0 {
b = append(b, ", "...)
}
b = append(b, '?')
}
return b, nil
}
for i, el := range slice {
if i > 0 {
b = append(b, "), "...)
if m.db.HasFeature(feature.ValuesRow) {
b = append(b, "ROW("...)
} else {
b = append(b, '(')
}
}
for j, key := range m.keys {
if j > 0 {
b = append(b, ", "...)
}
b = gen.Append(b, el[key])
}
}
return b, nil
}
func (m *mapSliceModel) initKeys() error {
if m.keys != nil {
return nil
}
slice := *m.dest
if len(slice) == 0 {
return errors.New("bun: map slice is empty")
}
first := slice[0]
keys := make([]string, 0, len(first))
for k := range first {
keys = append(keys, k)
}
slices.Sort(keys)
m.keys = keys
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package bun
import (
"context"
"database/sql"
"reflect"
"github.com/uptrace/bun/schema"
)
type scanModel struct {
db *DB
dest []any
scanIndex int
}
var _ Model = (*scanModel)(nil)
func newScanModel(db *DB, dest []any) *scanModel {
return &scanModel{
db: db,
dest: dest,
}
}
func (m *scanModel) Value() any {
return m.dest
}
func (m *scanModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
if !rows.Next() {
return 0, rows.Err()
}
dest := makeDest(m, len(m.dest))
m.scanIndex = 0
if err := rows.Scan(dest...); err != nil {
return 0, err
}
return 1, nil
}
func (m *scanModel) ScanRow(ctx context.Context, rows *sql.Rows) error {
return rows.Scan(m.dest...)
}
func (m *scanModel) Scan(src any) error {
dest := reflect.ValueOf(m.dest[m.scanIndex])
m.scanIndex++
scanner := schema.Scanner(dest.Type())
return scanner(dest, src)
}
+82
View File
@@ -0,0 +1,82 @@
package bun
import (
"context"
"database/sql"
"reflect"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
type sliceInfo struct {
nextElem func() reflect.Value
scan schema.ScannerFunc
}
type sliceModel struct {
dest []any
values []reflect.Value
scanIndex int
info []sliceInfo
}
var _ Model = (*sliceModel)(nil)
func newSliceModel(db *DB, dest []any, values []reflect.Value) *sliceModel {
return &sliceModel{
dest: dest,
values: values,
}
}
func (m *sliceModel) Value() any {
return m.dest
}
func (m *sliceModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
columns, err := rows.Columns()
if err != nil {
return 0, err
}
m.info = make([]sliceInfo, len(m.values))
for i, v := range m.values {
if v.IsValid() && v.Len() > 0 {
v.Set(v.Slice(0, 0))
}
m.info[i] = sliceInfo{
nextElem: internal.MakeSliceNextElemFunc(v),
scan: schema.Scanner(v.Type().Elem()),
}
}
if len(columns) == 0 {
return 0, nil
}
dest := makeDest(m, len(columns))
var n int
for rows.Next() {
m.scanIndex = 0
if err := rows.Scan(dest...); err != nil {
return 0, err
}
n++
}
if err := rows.Err(); err != nil {
return 0, err
}
return n, nil
}
func (m *sliceModel) Scan(src any) error {
info := m.info[m.scanIndex]
m.scanIndex++
dest := info.nextElem()
return info.scan(dest, src)
}
+185
View File
@@ -0,0 +1,185 @@
package bun
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"reflect"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
type hasManyModel struct {
*sliceTableModel
baseTable *schema.Table
rel *schema.Relation
baseValues map[internal.MapKey][]reflect.Value
structKey []any
}
var _ TableModel = (*hasManyModel)(nil)
func newHasManyModel(j *relationJoin) *hasManyModel {
baseTable := j.BaseModel.Table()
joinModel := j.JoinModel.(*sliceTableModel)
baseValues := baseValues(joinModel, j.Relation.BasePKs)
if len(baseValues) == 0 {
return nil
}
m := hasManyModel{
sliceTableModel: joinModel,
baseTable: baseTable,
rel: j.Relation,
baseValues: baseValues,
}
if !m.sliceOfPtr {
m.strct = reflect.New(m.table.Type).Elem()
}
return &m
}
func (m *hasManyModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
columns, err := rows.Columns()
if err != nil {
return 0, err
}
m.columns = columns
dest := makeDest(m, len(columns))
var n int
m.structKey = make([]any, len(m.rel.JoinPKs))
for rows.Next() {
if m.sliceOfPtr {
m.strct = reflect.New(m.table.Type).Elem()
} else {
m.strct.Set(m.table.ZeroValue)
}
m.structInited = false
m.scanIndex = 0
if err := rows.Scan(dest...); err != nil {
return 0, err
}
if err := m.parkStruct(); err != nil {
return 0, err
}
n++
}
if err := rows.Err(); err != nil {
return 0, err
}
return n, nil
}
func (m *hasManyModel) Scan(src any) error {
column := m.columns[m.scanIndex]
m.scanIndex++
field := m.table.LookupField(column)
if field == nil {
return fmt.Errorf("bun: %s does not have column %q", m.table.TypeName, column)
}
if err := field.ScanValue(m.strct, src); err != nil {
return err
}
for i, f := range m.rel.JoinPKs {
if f.Name == column {
m.structKey[i] = indirectAsKey(field.Value(m.strct))
break
}
}
return nil
}
func (m *hasManyModel) parkStruct() error {
baseValues, ok := m.baseValues[internal.NewMapKey(m.structKey)]
if !ok {
return fmt.Errorf(
"bun: has-many relation=%s does not have base %s with id=%q (check join conditions)",
m.rel.Field.GoName, m.baseTable, m.structKey)
}
for i, v := range baseValues {
if !m.sliceOfPtr {
v.Set(reflect.Append(v, m.strct))
continue
}
if i == 0 {
v.Set(reflect.Append(v, m.strct.Addr()))
continue
}
clone := reflect.New(m.strct.Type()).Elem()
clone.Set(m.strct)
v.Set(reflect.Append(v, clone.Addr()))
}
return nil
}
func (m *hasManyModel) clone() TableModel {
return &hasManyModel{
sliceTableModel: m.sliceTableModel.clone().(*sliceTableModel),
baseTable: m.baseTable,
rel: m.rel,
baseValues: m.baseValues,
structKey: m.structKey,
}
}
func baseValues(model TableModel, fields []*schema.Field) map[internal.MapKey][]reflect.Value {
fieldIndex := model.Relation().Field.Index
m := make(map[internal.MapKey][]reflect.Value)
key := make([]any, 0, len(fields))
walk(model.rootValue(), model.parentIndex(), func(v reflect.Value) {
key = modelKey(key[:0], v, fields)
mapKey := internal.NewMapKey(key)
m[mapKey] = append(m[mapKey], v.FieldByIndex(fieldIndex))
})
return m
}
func modelKey(key []any, strct reflect.Value, fields []*schema.Field) []any {
for _, f := range fields {
key = append(key, indirectAsKey(f.Value(strct)))
}
return key
}
// indirectAsKey return the field value dereferencing the pointer if necessary.
// The value is then used as a map key.
func indirectAsKey(field reflect.Value) any {
if field.Kind() == reflect.Pointer && field.IsNil() {
return nil
}
i := field.Interface()
if valuer, ok := i.(driver.Valuer); ok {
if v, err := valuer.Value(); err == nil && v != nil {
switch reflect.TypeOf(v).Kind() {
case reflect.Array, reflect.Chan, reflect.Func,
reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer:
// NOTE #1107, these types cannot be used as map key,
// let us use original logic.
return i
default:
return v
}
}
}
return reflect.Indirect(field).Interface()
}
+142
View File
@@ -0,0 +1,142 @@
package bun
import (
"context"
"database/sql"
"fmt"
"reflect"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
type m2mModel struct {
*sliceTableModel
baseTable *schema.Table
rel *schema.Relation
baseValues map[internal.MapKey][]reflect.Value
structKey []any
}
var _ TableModel = (*m2mModel)(nil)
func newM2MModel(j *relationJoin) *m2mModel {
baseTable := j.BaseModel.Table()
joinModel := j.JoinModel.(*sliceTableModel)
baseValues := baseValues(joinModel, j.Relation.BasePKs)
if len(baseValues) == 0 {
return nil
}
m := &m2mModel{
sliceTableModel: joinModel,
baseTable: baseTable,
rel: j.Relation,
baseValues: baseValues,
}
if !m.sliceOfPtr {
m.strct = reflect.New(m.table.Type).Elem()
}
return m
}
func (m *m2mModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
columns, err := rows.Columns()
if err != nil {
return 0, err
}
m.columns = columns
dest := makeDest(m, len(columns))
var n int
for rows.Next() {
if m.sliceOfPtr {
m.strct = reflect.New(m.table.Type).Elem()
} else {
m.strct.Set(m.table.ZeroValue)
}
m.structInited = false
m.scanIndex = 0
m.structKey = m.structKey[:0]
if err := rows.Scan(dest...); err != nil {
return 0, err
}
if err := m.parkStruct(); err != nil {
return 0, err
}
n++
}
if err := rows.Err(); err != nil {
return 0, err
}
return n, nil
}
func (m *m2mModel) Scan(src any) error {
column := m.columns[m.scanIndex]
m.scanIndex++
// Base pks must come first.
if m.scanIndex <= len(m.rel.M2MBasePKs) {
return m.scanM2MColumn(column, src)
}
if field, ok := m.table.FieldMap[column]; ok {
return field.ScanValue(m.strct, src)
}
_, err := m.scanColumn(column, src)
return err
}
func (m *m2mModel) scanM2MColumn(column string, src any) error {
for _, field := range m.rel.M2MBasePKs {
if field.Name == column {
dest := reflect.New(field.IndirectType).Elem()
if err := field.Scan(dest, src); err != nil {
return err
}
m.structKey = append(m.structKey, indirectAsKey(dest))
break
}
}
_, err := m.scanColumn(column, src)
return err
}
func (m *m2mModel) parkStruct() error {
baseValues, ok := m.baseValues[internal.NewMapKey(m.structKey)]
if !ok {
return fmt.Errorf(
"bun: m2m relation=%s does not have base %s with key=%q (check join conditions)",
m.rel.Field.GoName, m.baseTable, m.structKey)
}
for _, v := range baseValues {
if m.sliceOfPtr {
v.Set(reflect.Append(v, m.strct.Addr()))
} else {
v.Set(reflect.Append(v, m.strct))
}
}
return nil
}
func (m *m2mModel) clone() TableModel {
return &m2mModel{
sliceTableModel: m.sliceTableModel.clone().(*sliceTableModel),
baseTable: m.baseTable,
rel: m.rel,
baseValues: m.baseValues,
structKey: m.structKey,
}
}
+136
View File
@@ -0,0 +1,136 @@
package bun
import (
"context"
"database/sql"
"reflect"
"time"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
type sliceTableModel struct {
structTableModel
slice reflect.Value
sliceLen int
sliceOfPtr bool
nextElem func() reflect.Value
}
var _ TableModel = (*sliceTableModel)(nil)
func newSliceTableModel(
db *DB, dest any, slice reflect.Value, elemType reflect.Type,
) *sliceTableModel {
m := &sliceTableModel{
structTableModel: structTableModel{
db: db,
table: db.Table(elemType),
dest: dest,
root: slice,
},
slice: slice,
sliceLen: slice.Len(),
nextElem: internal.MakeSliceNextElemFunc(slice),
}
m.init(slice.Type())
return m
}
func (m *sliceTableModel) init(sliceType reflect.Type) {
switch sliceType.Elem().Kind() {
case reflect.Ptr, reflect.Interface:
m.sliceOfPtr = true
}
}
func (m *sliceTableModel) join(name string) *relationJoin {
return m._join(m.slice, name)
}
func (m *sliceTableModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
columns, err := rows.Columns()
if err != nil {
return 0, err
}
m.columns = columns
dest := makeDest(m, len(columns))
if m.slice.IsValid() && m.slice.Len() > 0 {
m.slice.Set(m.slice.Slice(0, 0))
}
var n int
for rows.Next() {
m.strct = m.nextElem()
if m.sliceOfPtr {
m.strct = m.strct.Elem()
}
m.structInited = false
if err := m.scanRow(ctx, rows, dest); err != nil {
return 0, err
}
n++
}
if err := rows.Err(); err != nil {
return 0, err
}
return n, nil
}
var _ schema.BeforeAppendModelHook = (*sliceTableModel)(nil)
func (m *sliceTableModel) BeforeAppendModel(ctx context.Context, query Query) error {
if !m.table.HasBeforeAppendModelHook() || !m.slice.IsValid() {
return nil
}
sliceLen := m.slice.Len()
for i := 0; i < sliceLen; i++ {
strct := m.slice.Index(i)
if !m.sliceOfPtr {
strct = strct.Addr()
}
err := strct.Interface().(schema.BeforeAppendModelHook).BeforeAppendModel(ctx, query)
if err != nil {
return err
}
}
return nil
}
// Inherit these hooks from structTableModel.
var (
_ schema.BeforeScanRowHook = (*sliceTableModel)(nil)
_ schema.AfterScanRowHook = (*sliceTableModel)(nil)
)
func (m *sliceTableModel) updateSoftDeleteField(tm time.Time) error {
sliceLen := m.slice.Len()
for i := 0; i < sliceLen; i++ {
strct := indirect(m.slice.Index(i))
fv := m.table.SoftDeleteField.Value(strct)
if err := m.table.UpdateSoftDeleteField(fv, tm); err != nil {
return err
}
}
return nil
}
func (m *sliceTableModel) clone() TableModel {
return &sliceTableModel{
structTableModel: *m.structTableModel.clone().(*structTableModel),
slice: m.slice,
sliceLen: m.sliceLen,
sliceOfPtr: m.sliceOfPtr,
nextElem: m.nextElem,
}
}
+373
View File
@@ -0,0 +1,373 @@
package bun
import (
"context"
"database/sql"
"fmt"
"reflect"
"strings"
"time"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
type structTableModel struct {
db *DB
table *schema.Table
rel *schema.Relation
joins []relationJoin
dest any
root reflect.Value
index []int
strct reflect.Value
structInited bool
structInitErr error
columns []string
scanIndex int
}
var _ TableModel = (*structTableModel)(nil)
func newStructTableModel(db *DB, dest any, table *schema.Table) *structTableModel {
return &structTableModel{
db: db,
table: table,
dest: dest,
}
}
func newStructTableModelValue(db *DB, dest any, v reflect.Value) *structTableModel {
return &structTableModel{
db: db,
table: db.Table(v.Type()),
dest: dest,
root: v,
strct: v,
}
}
func (m *structTableModel) Value() any {
return m.dest
}
func (m *structTableModel) Table() *schema.Table {
return m.table
}
func (m *structTableModel) Relation() *schema.Relation {
return m.rel
}
func (m *structTableModel) initStruct() error {
if m.structInited {
return m.structInitErr
}
m.structInited = true
switch m.strct.Kind() {
case reflect.Invalid:
m.structInitErr = errNilModel
return m.structInitErr
case reflect.Interface:
m.strct = m.strct.Elem()
}
if m.strct.Kind() == reflect.Ptr {
if m.strct.IsNil() {
m.strct.Set(reflect.New(m.strct.Type().Elem()))
m.strct = m.strct.Elem()
} else {
m.strct = m.strct.Elem()
}
}
m.mountJoins()
return nil
}
func (m *structTableModel) mountJoins() {
for i := range m.joins {
j := &m.joins[i]
switch j.Relation.Type {
case schema.HasOneRelation, schema.BelongsToRelation:
j.JoinModel.mount(m.strct)
}
}
}
var _ schema.BeforeAppendModelHook = (*structTableModel)(nil)
func (m *structTableModel) BeforeAppendModel(ctx context.Context, query Query) error {
if !m.table.HasBeforeAppendModelHook() || !m.strct.IsValid() {
return nil
}
return m.strct.Addr().Interface().(schema.BeforeAppendModelHook).BeforeAppendModel(ctx, query)
}
var _ schema.BeforeScanRowHook = (*structTableModel)(nil)
func (m *structTableModel) BeforeScanRow(ctx context.Context) error {
if m.table.HasBeforeScanRowHook() {
return m.strct.Addr().Interface().(schema.BeforeScanRowHook).BeforeScanRow(ctx)
}
return nil
}
var _ schema.AfterScanRowHook = (*structTableModel)(nil)
func (m *structTableModel) AfterScanRow(ctx context.Context) error {
if !m.structInited {
return nil
}
if m.table.HasAfterScanRowHook() {
firstErr := m.strct.Addr().Interface().(schema.AfterScanRowHook).AfterScanRow(ctx)
for _, j := range m.joins {
switch j.Relation.Type {
case schema.HasOneRelation, schema.BelongsToRelation:
if err := j.JoinModel.AfterScanRow(ctx); err != nil && firstErr == nil {
firstErr = err
}
}
}
return firstErr
}
return nil
}
func (m *structTableModel) getJoin(name string) *relationJoin {
for i := range m.joins {
j := &m.joins[i]
if j.Relation.Field.Name == name || j.Relation.Field.GoName == name {
return j
}
}
return nil
}
func (m *structTableModel) getJoins() []relationJoin {
return m.joins
}
func (m *structTableModel) addJoin(j relationJoin) *relationJoin {
m.joins = append(m.joins, j)
return &m.joins[len(m.joins)-1]
}
func (m *structTableModel) join(name string) *relationJoin {
return m._join(m.strct, name)
}
func (m *structTableModel) _join(bind reflect.Value, name string) *relationJoin {
path := strings.Split(name, ".")
index := make([]int, 0, len(path))
currJoin := relationJoin{
BaseModel: m,
JoinModel: m,
}
var lastJoin *relationJoin
for _, name := range path {
relation, ok := currJoin.JoinModel.Table().Relations[name]
if !ok {
return nil
}
currJoin.Relation = relation
index = append(index, relation.Field.Index...)
if j := currJoin.JoinModel.getJoin(name); j != nil {
currJoin.BaseModel = j.BaseModel
currJoin.JoinModel = j.JoinModel
lastJoin = j
} else {
model, err := newTableModelIndex(m.db, m.table, bind, index, relation)
if err != nil {
return nil
}
currJoin.Parent = lastJoin
currJoin.BaseModel = currJoin.JoinModel
currJoin.JoinModel = model
lastJoin = currJoin.BaseModel.addJoin(currJoin)
}
}
return lastJoin
}
func (m *structTableModel) rootValue() reflect.Value {
return m.root
}
func (m *structTableModel) parentIndex() []int {
return m.index[:len(m.index)-len(m.rel.Field.Index)]
}
func (m *structTableModel) mount(host reflect.Value) {
m.strct = internal.FieldByIndexAlloc(host, m.rel.Field.Index)
m.structInited = false
}
func (m *structTableModel) updateSoftDeleteField(tm time.Time) error {
if !m.strct.IsValid() {
return nil
}
fv := m.table.SoftDeleteField.Value(m.strct)
return m.table.UpdateSoftDeleteField(fv, tm)
}
func (m *structTableModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
if !rows.Next() {
return 0, rows.Err()
}
var n int
if err := m.ScanRow(ctx, rows); err != nil {
return 0, err
}
n++
// And discard the rest. This is especially important for SQLite3, which can return
// a row like it was inserted successfully and then return an actual error for the next row.
// See issues/100.
for rows.Next() {
n++
}
if err := rows.Err(); err != nil {
return 0, err
}
return n, nil
}
func (m *structTableModel) ScanRow(ctx context.Context, rows *sql.Rows) error {
columns, err := rows.Columns()
if err != nil {
return err
}
m.columns = columns
dest := makeDest(m, len(columns))
return m.scanRow(ctx, rows, dest)
}
func (m *structTableModel) scanRow(ctx context.Context, rows *sql.Rows, dest []any) error {
if err := m.BeforeScanRow(ctx); err != nil {
return err
}
m.scanIndex = 0
if err := rows.Scan(dest...); err != nil {
return err
}
if err := m.AfterScanRow(ctx); err != nil {
return err
}
return nil
}
func (m *structTableModel) Scan(src any) error {
column := m.columns[m.scanIndex]
m.scanIndex++
return m.ScanColumn(unquote(column), src)
}
func (m *structTableModel) ScanColumn(column string, src any) error {
if ok, err := m.scanColumn(column, src); ok {
return err
}
if column == "" || column[0] == '_' || m.db.flags.Has(discardUnknownColumns) {
return nil
}
return fmt.Errorf("bun: %s does not have column %q", m.table.TypeName, column)
}
func (m *structTableModel) scanColumn(column string, src any) (bool, error) {
if src != nil {
if err := m.initStruct(); err != nil {
return true, err
}
}
if field := m.table.LookupField(column); field != nil {
if src == nil && m.isNil() {
return true, nil
}
return true, field.ScanValue(m.strct, src)
}
if joinName, column := splitColumn(column); joinName != "" {
if join := m.getJoin(joinName); join != nil {
return true, join.JoinModel.ScanColumn(column, src)
}
if m.table.ModelName == joinName {
return true, m.ScanColumn(column, src)
}
}
return false, nil
}
func (m *structTableModel) isNil() bool {
return m.strct.Kind() == reflect.Ptr && m.strct.IsNil()
}
func (m *structTableModel) AppendNamedArg(
gen schema.QueryGen, b []byte, name string,
) ([]byte, bool) {
return m.table.AppendNamedArg(gen, b, name, m.strct)
}
func (m *structTableModel) clone() TableModel {
return &structTableModel{
db: m.db,
table: m.table,
rel: m.rel,
joins: append([]relationJoin{}, m.joins...),
dest: m.dest,
root: m.root,
index: append([]int{}, m.index...),
strct: m.strct,
structInited: m.structInited,
structInitErr: m.structInitErr,
columns: append([]string{}, m.columns...),
scanIndex: m.scanIndex,
}
}
// sqlite3 sometimes does not unquote columns.
func unquote(s string) string {
if s == "" {
return s
}
if s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
func splitColumn(s string) (string, string) {
if i := strings.Index(s, "__"); i >= 0 {
return s[:i], s[i+2:]
}
return "", s
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "gobun",
"version": "1.2.18",
"main": "index.js",
"repository": "git@github.com:uptrace/bun.git",
"author": "Vladimir Mihailenco <vladimir.webdev@gmail.com>",
"license": "BSD-2-clause"
}
+1591
View File
File diff suppressed because it is too large Load Diff
+152
View File
@@ -0,0 +1,152 @@
package bun
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// AddColumnQuery builds ALTER TABLE ... ADD COLUMN statements.
type AddColumnQuery struct {
baseQuery
ifNotExists bool
comment string
}
var _ Query = (*AddColumnQuery)(nil)
// NewAddColumnQuery creates an AddColumnQuery bound to the provided DB.
func NewAddColumnQuery(db *DB) *AddColumnQuery {
q := &AddColumnQuery{
baseQuery: baseQuery{
db: db,
},
}
return q
}
func (q *AddColumnQuery) Conn(db IConn) *AddColumnQuery {
q.setConn(db)
return q
}
func (q *AddColumnQuery) Model(model any) *AddColumnQuery {
q.setModel(model)
return q
}
func (q *AddColumnQuery) Err(err error) *AddColumnQuery {
q.setErr(err)
return q
}
// Apply calls each function in fns, passing the AddColumnQuery as an argument.
func (q *AddColumnQuery) Apply(fns ...func(*AddColumnQuery) *AddColumnQuery) *AddColumnQuery {
for _, fn := range fns {
if fn != nil {
q = fn(q)
}
}
return q
}
//------------------------------------------------------------------------------
func (q *AddColumnQuery) Table(tables ...string) *AddColumnQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *AddColumnQuery) TableExpr(query string, args ...any) *AddColumnQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *AddColumnQuery) ModelTableExpr(query string, args ...any) *AddColumnQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *AddColumnQuery) ColumnExpr(query string, args ...any) *AddColumnQuery {
q.addColumn(schema.SafeQuery(query, args))
return q
}
func (q *AddColumnQuery) IfNotExists() *AddColumnQuery {
q.ifNotExists = true
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *AddColumnQuery) Comment(comment string) *AddColumnQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *AddColumnQuery) Operation() string {
return "ADD COLUMN"
}
func (q *AddColumnQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
if len(q.columns) != 1 {
return nil, fmt.Errorf("bun: AddColumnQuery requires exactly one column")
}
b = append(b, "ALTER TABLE "...)
b, err = q.appendFirstTable(gen, b)
if err != nil {
return nil, err
}
b = append(b, " ADD "...)
if q.ifNotExists {
b = append(b, "IF NOT EXISTS "...)
}
b, err = q.columns[0].AppendQuery(gen, b)
if err != nil {
return nil, err
}
return b, nil
}
//------------------------------------------------------------------------------
func (q *AddColumnQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
if q.ifNotExists && !q.hasFeature(feature.AlterColumnExists) {
return nil, feature.NewNotSupportError(feature.AlterColumnExists)
}
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
query := internal.String(queryBytes)
return q.exec(ctx, q, query)
}
+150
View File
@@ -0,0 +1,150 @@
package bun
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// DropColumnQuery builds ALTER TABLE ... DROP COLUMN statements.
type DropColumnQuery struct {
baseQuery
comment string
}
var _ Query = (*DropColumnQuery)(nil)
// NewDropColumnQuery creates a DropColumnQuery bound to the provided DB.
func NewDropColumnQuery(db *DB) *DropColumnQuery {
q := &DropColumnQuery{
baseQuery: baseQuery{
db: db,
},
}
return q
}
func (q *DropColumnQuery) Conn(db IConn) *DropColumnQuery {
q.setConn(db)
return q
}
func (q *DropColumnQuery) Model(model any) *DropColumnQuery {
q.setModel(model)
return q
}
func (q *DropColumnQuery) Err(err error) *DropColumnQuery {
q.setErr(err)
return q
}
// Apply calls each function in fns, passing the DropColumnQuery as an argument.
func (q *DropColumnQuery) Apply(fns ...func(*DropColumnQuery) *DropColumnQuery) *DropColumnQuery {
for _, fn := range fns {
if fn != nil {
q = fn(q)
}
}
return q
}
//------------------------------------------------------------------------------
func (q *DropColumnQuery) Table(tables ...string) *DropColumnQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *DropColumnQuery) TableExpr(query string, args ...any) *DropColumnQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *DropColumnQuery) ModelTableExpr(query string, args ...any) *DropColumnQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *DropColumnQuery) Column(columns ...string) *DropColumnQuery {
for _, column := range columns {
q.addColumn(schema.UnsafeIdent(column))
}
return q
}
func (q *DropColumnQuery) ColumnExpr(query string, args ...any) *DropColumnQuery {
q.addColumn(schema.SafeQuery(query, args))
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *DropColumnQuery) Comment(comment string) *DropColumnQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *DropColumnQuery) Operation() string {
return "DROP COLUMN"
}
func (q *DropColumnQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
if len(q.columns) != 1 {
return nil, fmt.Errorf("bun: DropColumnQuery requires exactly one column")
}
b = append(b, "ALTER TABLE "...)
b, err = q.appendFirstTable(gen, b)
if err != nil {
return nil, err
}
b = append(b, " DROP COLUMN "...)
b, err = q.columns[0].AppendQuery(gen, b)
if err != nil {
return nil, err
}
return b, nil
}
//------------------------------------------------------------------------------
func (q *DropColumnQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
query := internal.String(queryBytes)
res, err := q.exec(ctx, q, query)
if err != nil {
return nil, err
}
return res, nil
}
+454
View File
@@ -0,0 +1,454 @@
package bun
import (
"context"
"database/sql"
"errors"
"time"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// DeleteQuery builds SQL DELETE statements.
type DeleteQuery struct {
whereBaseQuery
orderLimitOffsetQuery
returningQuery
comment string
}
var _ Query = (*DeleteQuery)(nil)
// NewDeleteQuery returns a DeleteQuery associated with the provided DB.
func NewDeleteQuery(db *DB) *DeleteQuery {
q := &DeleteQuery{
whereBaseQuery: whereBaseQuery{
baseQuery: baseQuery{
db: db,
},
},
}
return q
}
func (q *DeleteQuery) Conn(db IConn) *DeleteQuery {
q.setConn(db)
return q
}
func (q *DeleteQuery) Model(model any) *DeleteQuery {
q.setModel(model)
return q
}
func (q *DeleteQuery) Err(err error) *DeleteQuery {
q.setErr(err)
return q
}
// Apply calls each function in fns, passing the DeleteQuery as an argument.
func (q *DeleteQuery) Apply(fns ...func(*DeleteQuery) *DeleteQuery) *DeleteQuery {
for _, fn := range fns {
if fn != nil {
q = fn(q)
}
}
return q
}
func (q *DeleteQuery) With(name string, query Query) *DeleteQuery {
q.addWith(NewWithQuery(name, query))
return q
}
func (q *DeleteQuery) WithRecursive(name string, query Query) *DeleteQuery {
q.addWith(NewWithQuery(name, query).Recursive())
return q
}
func (q *DeleteQuery) WithQuery(query *WithQuery) *DeleteQuery {
q.addWith(query)
return q
}
func (q *DeleteQuery) Table(tables ...string) *DeleteQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *DeleteQuery) TableExpr(query string, args ...any) *DeleteQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *DeleteQuery) ModelTableExpr(query string, args ...any) *DeleteQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *DeleteQuery) WherePK(cols ...string) *DeleteQuery {
q.addWhereCols(cols)
return q
}
func (q *DeleteQuery) Where(query string, args ...any) *DeleteQuery {
q.addWhere(schema.SafeQueryWithSep(query, args, " AND "))
return q
}
func (q *DeleteQuery) WhereOr(query string, args ...any) *DeleteQuery {
q.addWhere(schema.SafeQueryWithSep(query, args, " OR "))
return q
}
func (q *DeleteQuery) WhereGroup(sep string, fn func(*DeleteQuery) *DeleteQuery) *DeleteQuery {
saved := q.where
q.where = nil
q = fn(q)
where := q.where
q.where = saved
q.addWhereGroup(sep, where)
return q
}
func (q *DeleteQuery) WhereDeleted() *DeleteQuery {
q.whereDeleted()
return q
}
func (q *DeleteQuery) WhereAllWithDeleted() *DeleteQuery {
q.whereAllWithDeleted()
return q
}
func (q *DeleteQuery) Order(orders ...string) *DeleteQuery {
if !q.hasFeature(feature.DeleteOrderLimit) {
q.setErr(feature.NewNotSupportError(feature.DeleteOrderLimit))
return q
}
q.addOrder(orders...)
return q
}
func (q *DeleteQuery) OrderExpr(query string, args ...any) *DeleteQuery {
if !q.hasFeature(feature.DeleteOrderLimit) {
q.setErr(feature.NewNotSupportError(feature.DeleteOrderLimit))
return q
}
q.addOrderExpr(query, args...)
return q
}
func (q *DeleteQuery) ForceDelete() *DeleteQuery {
q.flags = q.flags.Set(forceDeleteFlag)
return q
}
// ------------------------------------------------------------------------------
func (q *DeleteQuery) Limit(n int) *DeleteQuery {
if !q.hasFeature(feature.DeleteOrderLimit) {
q.setErr(feature.NewNotSupportError(feature.DeleteOrderLimit))
return q
}
q.setLimit(n)
return q
}
//------------------------------------------------------------------------------
// Returning adds a RETURNING clause to the query.
//
// To suppress the auto-generated RETURNING clause, use `Returning("NULL")`.
func (q *DeleteQuery) Returning(query string, args ...any) *DeleteQuery {
if !q.hasFeature(feature.DeleteReturning) {
q.setErr(feature.NewNotSupportError(feature.DeleteOrderLimit))
return q
}
q.addReturning(schema.SafeQuery(query, args))
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *DeleteQuery) Comment(comment string) *DeleteQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *DeleteQuery) Operation() string {
return "DELETE"
}
func (q *DeleteQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
gen = formatterWithModel(gen, q)
if q.isSoftDelete() {
now := time.Now()
if err := q.tableModel.updateSoftDeleteField(now); err != nil {
return nil, err
}
upd := &UpdateQuery{
whereBaseQuery: q.whereBaseQuery,
returningQuery: q.returningQuery,
}
upd.Set(q.softDeleteSet(gen, now))
return upd.AppendQuery(gen, b)
}
withAlias := q.db.HasFeature(feature.DeleteTableAlias)
b, err = q.appendWith(gen, b)
if err != nil {
return nil, err
}
b = append(b, "DELETE FROM "...)
if withAlias {
b, err = q.appendFirstTableWithAlias(gen, b)
} else {
b, err = q.appendFirstTable(gen, b)
}
if err != nil {
return nil, err
}
if q.hasMultiTables() {
b = append(b, " USING "...)
b, err = q.appendOtherTables(gen, b)
if err != nil {
return nil, err
}
}
if q.hasFeature(feature.Output) && q.hasReturning() {
b = append(b, " OUTPUT "...)
b, err = q.appendOutput(gen, b)
if err != nil {
return nil, err
}
}
b, err = q.mustAppendWhere(gen, b, withAlias)
if err != nil {
return nil, err
}
if q.hasMultiTables() && (len(q.order) > 0 || q.limit > 0) {
return nil, errors.New("bun: can't use ORDER or LIMIT with multiple tables")
}
b, err = q.appendOrder(gen, b)
if err != nil {
return nil, err
}
b, err = q.appendLimitOffset(gen, b)
if err != nil {
return nil, err
}
if q.hasFeature(feature.DeleteReturning) && q.hasReturning() {
b = append(b, " RETURNING "...)
b, err = q.appendReturning(gen, b)
if err != nil {
return nil, err
}
}
return b, nil
}
func (q *DeleteQuery) isSoftDelete() bool {
return q.tableModel != nil && q.table.SoftDeleteField != nil && !q.flags.Has(forceDeleteFlag)
}
func (q *DeleteQuery) softDeleteSet(gen schema.QueryGen, tm time.Time) string {
b := make([]byte, 0, 32)
if gen.HasFeature(feature.UpdateMultiTable) {
b = append(b, q.table.SQLAlias...)
b = append(b, '.')
}
b = append(b, q.table.SoftDeleteField.SQLName...)
b = append(b, " = "...)
b = gen.Append(b, tm)
return internal.String(b)
}
//------------------------------------------------------------------------------
func (q *DeleteQuery) Scan(ctx context.Context, dest ...any) error {
_, err := q.scanOrExec(ctx, dest, true)
return err
}
func (q *DeleteQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
return q.scanOrExec(ctx, dest, len(dest) > 0)
}
func (q *DeleteQuery) scanOrExec(
ctx context.Context, dest []any, hasDest bool,
) (sql.Result, error) {
if q.err != nil {
return nil, q.err
}
if q.table != nil {
if err := q.beforeDeleteHook(ctx); err != nil {
return nil, err
}
}
// Run append model hooks before generating the query.
if err := q.beforeAppendModel(ctx, q); err != nil {
return nil, err
}
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
// Generate the query before checking hasReturning.
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
useScan := hasDest || (q.hasReturning() && q.hasFeature(feature.DeleteReturning|feature.Output))
var model Model
if useScan {
var err error
model, err = q.getModel(dest)
if err != nil {
return nil, err
}
}
query := internal.String(queryBytes)
var res sql.Result
if useScan {
res, err = q.scan(ctx, q, query, model, hasDest)
if err != nil {
return nil, err
}
} else {
res, err = q.exec(ctx, q, query)
if err != nil {
return nil, err
}
}
if q.table != nil {
if err := q.afterDeleteHook(ctx); err != nil {
return nil, err
}
}
return res, nil
}
func (q *DeleteQuery) beforeDeleteHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(BeforeDeleteHook); ok {
if err := hook.BeforeDelete(ctx, q); err != nil {
return err
}
}
return nil
}
func (q *DeleteQuery) afterDeleteHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(AfterDeleteHook); ok {
if err := hook.AfterDelete(ctx, q); err != nil {
return err
}
}
return nil
}
// String returns the generated SQL query string. The DeleteQuery instance must not be
// modified during query generation to ensure multiple calls to String() return identical results.
func (q *DeleteQuery) String() string {
buf, err := q.AppendQuery(q.db.QueryGen(), nil)
if err != nil {
panic(err)
}
return string(buf)
}
//------------------------------------------------------------------------------
func (q *DeleteQuery) QueryBuilder() QueryBuilder {
return &deleteQueryBuilder{q}
}
func (q *DeleteQuery) ApplyQueryBuilder(fn func(QueryBuilder) QueryBuilder) *DeleteQuery {
return fn(q.QueryBuilder()).Unwrap().(*DeleteQuery)
}
type deleteQueryBuilder struct {
*DeleteQuery
}
func (q *deleteQueryBuilder) WhereGroup(
sep string, fn func(QueryBuilder) QueryBuilder,
) QueryBuilder {
q.DeleteQuery = q.DeleteQuery.WhereGroup(sep, func(qs *DeleteQuery) *DeleteQuery {
return fn(q).(*deleteQueryBuilder).DeleteQuery
})
return q
}
func (q *deleteQueryBuilder) Where(query string, args ...any) QueryBuilder {
q.DeleteQuery.Where(query, args...)
return q
}
func (q *deleteQueryBuilder) WhereOr(query string, args ...any) QueryBuilder {
q.DeleteQuery.WhereOr(query, args...)
return q
}
func (q *deleteQueryBuilder) WhereDeleted() QueryBuilder {
q.DeleteQuery.WhereDeleted()
return q
}
func (q *deleteQueryBuilder) WhereAllWithDeleted() QueryBuilder {
q.DeleteQuery.WhereAllWithDeleted()
return q
}
func (q *deleteQueryBuilder) WherePK(cols ...string) QueryBuilder {
q.DeleteQuery.WherePK(cols...)
return q
}
func (q *deleteQueryBuilder) Unwrap() any {
return q.DeleteQuery
}
+270
View File
@@ -0,0 +1,270 @@
package bun
import (
"context"
"database/sql"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// CreateIndexQuery builds CREATE INDEX statements.
type CreateIndexQuery struct {
whereBaseQuery
unique bool
fulltext bool
spatial bool
concurrently bool
ifNotExists bool
index schema.QueryWithArgs
using schema.QueryWithArgs
include []schema.QueryWithArgs
comment string
}
var _ Query = (*CreateIndexQuery)(nil)
// NewCreateIndexQuery returns a CreateIndexQuery tied to the provided DB.
func NewCreateIndexQuery(db *DB) *CreateIndexQuery {
q := &CreateIndexQuery{
whereBaseQuery: whereBaseQuery{
baseQuery: baseQuery{
db: db,
},
},
}
return q
}
func (q *CreateIndexQuery) Conn(db IConn) *CreateIndexQuery {
q.setConn(db)
return q
}
func (q *CreateIndexQuery) Model(model any) *CreateIndexQuery {
q.setModel(model)
return q
}
func (q *CreateIndexQuery) Err(err error) *CreateIndexQuery {
q.setErr(err)
return q
}
func (q *CreateIndexQuery) Unique() *CreateIndexQuery {
q.unique = true
return q
}
func (q *CreateIndexQuery) Concurrently() *CreateIndexQuery {
q.concurrently = true
return q
}
func (q *CreateIndexQuery) IfNotExists() *CreateIndexQuery {
q.ifNotExists = true
return q
}
//------------------------------------------------------------------------------
func (q *CreateIndexQuery) Index(query string) *CreateIndexQuery {
q.index = schema.UnsafeIdent(query)
return q
}
func (q *CreateIndexQuery) IndexExpr(query string, args ...any) *CreateIndexQuery {
q.index = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *CreateIndexQuery) Table(tables ...string) *CreateIndexQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *CreateIndexQuery) TableExpr(query string, args ...any) *CreateIndexQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *CreateIndexQuery) ModelTableExpr(query string, args ...any) *CreateIndexQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
func (q *CreateIndexQuery) Using(query string, args ...any) *CreateIndexQuery {
q.using = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *CreateIndexQuery) Column(columns ...string) *CreateIndexQuery {
for _, column := range columns {
q.addColumn(schema.UnsafeIdent(column))
}
return q
}
func (q *CreateIndexQuery) ColumnExpr(query string, args ...any) *CreateIndexQuery {
q.addColumn(schema.SafeQuery(query, args))
return q
}
func (q *CreateIndexQuery) ExcludeColumn(columns ...string) *CreateIndexQuery {
q.excludeColumn(columns)
return q
}
//------------------------------------------------------------------------------
func (q *CreateIndexQuery) Include(columns ...string) *CreateIndexQuery {
for _, column := range columns {
q.include = append(q.include, schema.UnsafeIdent(column))
}
return q
}
func (q *CreateIndexQuery) IncludeExpr(query string, args ...any) *CreateIndexQuery {
q.include = append(q.include, schema.SafeQuery(query, args))
return q
}
//------------------------------------------------------------------------------
func (q *CreateIndexQuery) Where(query string, args ...any) *CreateIndexQuery {
q.addWhere(schema.SafeQueryWithSep(query, args, " AND "))
return q
}
func (q *CreateIndexQuery) WhereOr(query string, args ...any) *CreateIndexQuery {
q.addWhere(schema.SafeQueryWithSep(query, args, " OR "))
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *CreateIndexQuery) Comment(comment string) *CreateIndexQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *CreateIndexQuery) Operation() string {
return "CREATE INDEX"
}
func (q *CreateIndexQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
b = append(b, "CREATE "...)
if q.unique {
b = append(b, "UNIQUE "...)
}
if q.fulltext {
b = append(b, "FULLTEXT "...)
}
if q.spatial {
b = append(b, "SPATIAL "...)
}
b = append(b, "INDEX "...)
if q.concurrently {
b = append(b, "CONCURRENTLY "...)
}
if q.ifNotExists && gen.HasFeature(feature.CreateIndexIfNotExists) {
b = append(b, "IF NOT EXISTS "...)
}
b, err = q.index.AppendQuery(gen, b)
if err != nil {
return nil, err
}
b = append(b, " ON "...)
b, err = q.appendFirstTable(gen, b)
if err != nil {
return nil, err
}
if !q.using.IsZero() {
b = append(b, " USING "...)
b, err = q.using.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
b = append(b, " ("...)
for i, col := range q.columns {
if i > 0 {
b = append(b, ", "...)
}
b, err = col.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
b = append(b, ')')
if len(q.include) > 0 {
b = append(b, " INCLUDE ("...)
for i, col := range q.include {
if i > 0 {
b = append(b, ", "...)
}
b, err = col.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
b = append(b, ')')
}
if len(q.where) > 0 {
b = append(b, " WHERE "...)
b, err = appendWhere(gen, b, q.where)
if err != nil {
return nil, err
}
}
return b, nil
}
//------------------------------------------------------------------------------
func (q *CreateIndexQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
query := internal.String(queryBytes)
res, err := q.exec(ctx, q, query)
if err != nil {
return nil, err
}
return res, nil
}
+136
View File
@@ -0,0 +1,136 @@
package bun
import (
"context"
"database/sql"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// DropIndexQuery builds DROP INDEX statements.
type DropIndexQuery struct {
baseQuery
cascadeQuery
concurrently bool
ifExists bool
index schema.QueryWithArgs
comment string
}
var _ Query = (*DropIndexQuery)(nil)
// NewDropIndexQuery returns a DropIndexQuery associated with the provided DB.
func NewDropIndexQuery(db *DB) *DropIndexQuery {
q := &DropIndexQuery{
baseQuery: baseQuery{
db: db,
},
}
return q
}
func (q *DropIndexQuery) Conn(db IConn) *DropIndexQuery {
q.setConn(db)
return q
}
func (q *DropIndexQuery) Model(model any) *DropIndexQuery {
q.setModel(model)
return q
}
func (q *DropIndexQuery) Err(err error) *DropIndexQuery {
q.setErr(err)
return q
}
//------------------------------------------------------------------------------
func (q *DropIndexQuery) Concurrently() *DropIndexQuery {
q.concurrently = true
return q
}
func (q *DropIndexQuery) IfExists() *DropIndexQuery {
q.ifExists = true
return q
}
func (q *DropIndexQuery) Cascade() *DropIndexQuery {
q.cascade = true
return q
}
func (q *DropIndexQuery) Restrict() *DropIndexQuery {
q.restrict = true
return q
}
func (q *DropIndexQuery) Index(query string, args ...any) *DropIndexQuery {
q.index = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *DropIndexQuery) Comment(comment string) *DropIndexQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *DropIndexQuery) Operation() string {
return "DROP INDEX"
}
func (q *DropIndexQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
b = append(b, "DROP INDEX "...)
if q.concurrently {
b = append(b, "CONCURRENTLY "...)
}
if q.ifExists {
b = append(b, "IF EXISTS "...)
}
b, err = q.index.AppendQuery(gen, b)
if err != nil {
return nil, err
}
b = q.appendCascade(gen, b)
return b, nil
}
//------------------------------------------------------------------------------
func (q *DropIndexQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
query := internal.String(queryBytes)
res, err := q.exec(ctx, q, query)
if err != nil {
return nil, err
}
return res, nil
}
+708
View File
@@ -0,0 +1,708 @@
package bun
import (
"context"
"database/sql"
"fmt"
"reflect"
"strings"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// InsertQuery builds SQL INSERT statements.
type InsertQuery struct {
whereBaseQuery
returningQuery
customValueQuery
on schema.QueryWithArgs
setQuery
ignore bool
replace bool
comment string
}
var _ Query = (*InsertQuery)(nil)
// NewInsertQuery returns an InsertQuery tied to the provided DB.
func NewInsertQuery(db *DB) *InsertQuery {
q := &InsertQuery{
whereBaseQuery: whereBaseQuery{
baseQuery: baseQuery{
db: db,
},
},
}
return q
}
func (q *InsertQuery) Conn(db IConn) *InsertQuery {
q.setConn(db)
return q
}
func (q *InsertQuery) Model(model any) *InsertQuery {
q.setModel(model)
return q
}
func (q *InsertQuery) Err(err error) *InsertQuery {
q.setErr(err)
return q
}
// Apply calls each function in fns, passing the InsertQuery as an argument.
func (q *InsertQuery) Apply(fns ...func(*InsertQuery) *InsertQuery) *InsertQuery {
for _, fn := range fns {
if fn != nil {
q = fn(q)
}
}
return q
}
func (q *InsertQuery) With(name string, query Query) *InsertQuery {
q.addWith(NewWithQuery(name, query))
return q
}
func (q *InsertQuery) WithRecursive(name string, query Query) *InsertQuery {
q.addWith(NewWithQuery(name, query).Recursive())
return q
}
func (q *InsertQuery) WithQuery(query *WithQuery) *InsertQuery {
q.addWith(query)
return q
}
//------------------------------------------------------------------------------
func (q *InsertQuery) Table(tables ...string) *InsertQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *InsertQuery) TableExpr(query string, args ...any) *InsertQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *InsertQuery) ModelTableExpr(query string, args ...any) *InsertQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *InsertQuery) Column(columns ...string) *InsertQuery {
for _, column := range columns {
q.addColumn(schema.UnsafeIdent(column))
}
return q
}
func (q *InsertQuery) ColumnExpr(query string, args ...any) *InsertQuery {
q.addColumn(schema.SafeQuery(query, args))
return q
}
func (q *InsertQuery) ExcludeColumn(columns ...string) *InsertQuery {
q.excludeColumn(columns)
return q
}
// Value overwrites model value for the column.
func (q *InsertQuery) Value(column string, expr string, args ...any) *InsertQuery {
if q.table == nil {
q.setErr(errNilModel)
return q
}
q.addValue(q.table, column, expr, args)
return q
}
func (q *InsertQuery) Where(query string, args ...any) *InsertQuery {
q.addWhere(schema.SafeQueryWithSep(query, args, " AND "))
return q
}
func (q *InsertQuery) WhereOr(query string, args ...any) *InsertQuery {
q.addWhere(schema.SafeQueryWithSep(query, args, " OR "))
return q
}
//------------------------------------------------------------------------------
// Returning adds a RETURNING clause to the query.
//
// To suppress the auto-generated RETURNING clause, use `Returning("")`.
func (q *InsertQuery) Returning(query string, args ...any) *InsertQuery {
q.addReturning(schema.SafeQuery(query, args))
return q
}
//------------------------------------------------------------------------------
// Ignore generates different queries depending on the DBMS:
// - On MySQL, it generates `INSERT IGNORE INTO`.
// - On PostgreSQL, it generates `ON CONFLICT DO NOTHING`.
func (q *InsertQuery) Ignore() *InsertQuery {
if q.db.gen.HasFeature(feature.InsertOnConflict) {
return q.On("CONFLICT DO NOTHING")
}
if q.db.gen.HasFeature(feature.InsertIgnore) {
q.ignore = true
}
return q
}
// Replaces generates a `REPLACE INTO` query (MySQL and MariaDB).
func (q *InsertQuery) Replace() *InsertQuery {
q.replace = true
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *InsertQuery) Comment(comment string) *InsertQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *InsertQuery) Operation() string {
return "INSERT"
}
func (q *InsertQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
gen = formatterWithModel(gen, q)
b, err = q.appendWith(gen, b)
if err != nil {
return nil, err
}
if q.replace {
b = append(b, "REPLACE "...)
} else {
b = append(b, "INSERT "...)
if q.ignore {
b = append(b, "IGNORE "...)
}
}
b = append(b, "INTO "...)
if q.db.HasFeature(feature.InsertTableAlias) && !q.on.IsZero() {
b, err = q.appendFirstTableWithAlias(gen, b)
} else {
b, err = q.appendFirstTable(gen, b)
}
if err != nil {
return nil, err
}
b, err = q.appendColumnsValues(gen, b, false)
if err != nil {
return nil, err
}
b, err = q.appendOn(gen, b)
if err != nil {
return nil, err
}
if q.hasFeature(feature.InsertReturning) && q.hasReturning() {
b = append(b, " RETURNING "...)
b, err = q.appendReturning(gen, b)
if err != nil {
return nil, err
}
}
return b, nil
}
func (q *InsertQuery) appendColumnsValues(
gen schema.QueryGen, b []byte, skipOutput bool,
) (_ []byte, err error) {
if q.hasMultiTables() {
if q.columns != nil {
b = append(b, " ("...)
b, err = q.appendColumns(gen, b)
if err != nil {
return nil, err
}
b = append(b, ")"...)
}
if q.hasFeature(feature.Output) && q.hasReturning() {
b = append(b, " OUTPUT "...)
b, err = q.appendOutput(gen, b)
if err != nil {
return nil, err
}
}
b = append(b, " SELECT "...)
if q.columns != nil {
b, err = q.appendColumns(gen, b)
if err != nil {
return nil, err
}
} else {
b = append(b, "*"...)
}
b = append(b, " FROM "...)
b, err = q.appendOtherTables(gen, b)
if err != nil {
return nil, err
}
return b, nil
}
if m, ok := q.model.(*mapModel); ok {
return m.appendColumnsValues(gen, b), nil
}
if _, ok := q.model.(*mapSliceModel); ok {
return nil, fmt.Errorf("Insert(*[]map[string]any) is not supported")
}
if q.model == nil {
return nil, errNilModel
}
// Build fields to populate RETURNING clause.
fields, err := q.getFields()
if err != nil {
return nil, err
}
b = append(b, " ("...)
b = q.appendFields(gen, b, fields)
b = append(b, ")"...)
if q.hasFeature(feature.Output) && q.hasReturning() && !skipOutput {
b = append(b, " OUTPUT "...)
b, err = q.appendOutput(gen, b)
if err != nil {
return nil, err
}
}
b = append(b, " VALUES ("...)
switch model := q.tableModel.(type) {
case *structTableModel:
b, err = q.appendStructValues(gen, b, fields, model.strct)
if err != nil {
return nil, err
}
case *sliceTableModel:
b, err = q.appendSliceValues(gen, b, fields, model.slice)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("bun: Insert does not support %T", q.tableModel)
}
b = append(b, ')')
return b, nil
}
func (q *InsertQuery) appendStructValues(
gen schema.QueryGen, b []byte, fields []*schema.Field, strct reflect.Value,
) (_ []byte, err error) {
isTemplate := gen.IsNop()
for i, f := range fields {
if i > 0 {
b = append(b, ", "...)
}
app, ok := q.modelValues[f.Name]
if ok {
b, err = app.AppendQuery(gen, b)
if err != nil {
return nil, err
}
q.addReturningField(f)
continue
}
switch {
case isTemplate:
b = append(b, '?')
case q.marshalsToDefault(f, strct):
if q.db.HasFeature(feature.DefaultPlaceholder) {
b = append(b, "DEFAULT"...)
} else if f.SQLDefault != "" {
b = append(b, f.SQLDefault...)
} else {
b = append(b, "NULL"...)
}
q.addReturningField(f)
default:
b = f.AppendValue(gen, b, strct)
}
}
for i, v := range q.extraValues {
if i > 0 || len(fields) > 0 {
b = append(b, ", "...)
}
b, err = v.value.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
return b, nil
}
func (q *InsertQuery) appendSliceValues(
gen schema.QueryGen, b []byte, fields []*schema.Field, slice reflect.Value,
) (_ []byte, err error) {
if gen.IsNop() {
return q.appendStructValues(gen, b, fields, reflect.Value{})
}
sliceLen := slice.Len()
for i := 0; i < sliceLen; i++ {
if i > 0 {
b = append(b, "), ("...)
}
el := indirect(slice.Index(i))
b, err = q.appendStructValues(gen, b, fields, el)
if err != nil {
return nil, err
}
}
return b, nil
}
func (q *InsertQuery) getFields() ([]*schema.Field, error) {
hasIdentity := q.db.HasFeature(feature.Identity)
if len(q.columns) > 0 || q.db.HasFeature(feature.DefaultPlaceholder) && !hasIdentity {
return q.baseQuery.getFields()
}
var strct reflect.Value
switch model := q.tableModel.(type) {
case *structTableModel:
strct = model.strct
case *sliceTableModel:
if model.sliceLen == 0 {
return nil, fmt.Errorf("bun: Insert(empty %T)", model.slice.Type())
}
strct = indirect(model.slice.Index(0))
default:
return nil, errNilModel
}
fields := make([]*schema.Field, 0, len(q.table.Fields))
for _, f := range q.table.Fields {
if hasIdentity && f.AutoIncrement {
q.addReturningField(f)
continue
}
if f.NotNull && q.marshalsToDefault(f, strct) {
q.addReturningField(f)
continue
}
fields = append(fields, f)
}
return fields, nil
}
// marshalsToDefault checks if the value will be marshaled as DEFAULT or NULL (if DEFAULT placeholder is not supported)
// when appending it to the VALUES clause in place of the given field.
func (q InsertQuery) marshalsToDefault(f *schema.Field, v reflect.Value) bool {
return (f.IsPtr && f.HasNilValue(v)) ||
(f.HasZeroValue(v) && (f.NullZero || f.SQLDefault != ""))
}
func (q *InsertQuery) appendFields(
gen schema.QueryGen, b []byte, fields []*schema.Field,
) []byte {
b = appendColumns(b, "", fields)
for i, v := range q.extraValues {
if i > 0 || len(fields) > 0 {
b = append(b, ", "...)
}
b = gen.AppendIdent(b, v.column)
}
return b
}
//------------------------------------------------------------------------------
func (q *InsertQuery) On(s string, args ...any) *InsertQuery {
q.on = schema.SafeQuery(s, args)
return q
}
func (q *InsertQuery) Set(query string, args ...any) *InsertQuery {
q.addSet(schema.SafeQuery(query, args))
return q
}
func (q *InsertQuery) SetValues(values *ValuesQuery) *InsertQuery {
q.setValues = values
return q
}
func (q *InsertQuery) appendOn(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.on.IsZero() {
return b, nil
}
b = append(b, " ON "...)
b, err = q.on.AppendQuery(gen, b)
if err != nil {
return nil, err
}
if len(q.set) > 0 || q.setValues != nil {
if gen.HasFeature(feature.InsertOnDuplicateKey) {
b = append(b, ' ')
} else {
b = append(b, " SET "...)
}
b, err = q.appendSet(gen, b)
if err != nil {
return nil, err
}
} else if q.onConflictDoUpdate() {
fields, err := q.getDataFields()
if err != nil {
return nil, err
}
b = q.appendSetExcluded(b, fields)
} else if q.onDuplicateKeyUpdate() {
fields, err := q.getDataFields()
if err != nil {
return nil, err
}
b = q.appendSetValues(b, fields)
}
if len(q.where) > 0 {
b = append(b, " WHERE "...)
b, err = appendWhere(gen, b, q.where)
if err != nil {
return nil, err
}
}
return b, nil
}
func (q *InsertQuery) onConflictDoUpdate() bool {
return strings.HasSuffix(strings.ToUpper(q.on.Query), " DO UPDATE")
}
func (q *InsertQuery) onDuplicateKeyUpdate() bool {
return strings.ToUpper(q.on.Query) == "DUPLICATE KEY UPDATE"
}
func (q *InsertQuery) appendSetExcluded(b []byte, fields []*schema.Field) []byte {
b = append(b, " SET "...)
for i, f := range fields {
if i > 0 {
b = append(b, ", "...)
}
b = append(b, f.SQLName...)
b = append(b, " = EXCLUDED."...)
b = append(b, f.SQLName...)
}
return b
}
func (q *InsertQuery) appendSetValues(b []byte, fields []*schema.Field) []byte {
b = append(b, " "...)
for i, f := range fields {
if i > 0 {
b = append(b, ", "...)
}
b = append(b, f.SQLName...)
b = append(b, " = VALUES("...)
b = append(b, f.SQLName...)
b = append(b, ")"...)
}
return b
}
//------------------------------------------------------------------------------
func (q *InsertQuery) Scan(ctx context.Context, dest ...any) error {
_, err := q.scanOrExec(ctx, dest, true)
return err
}
func (q *InsertQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
return q.scanOrExec(ctx, dest, len(dest) > 0)
}
func (q *InsertQuery) scanOrExec(
ctx context.Context, dest []any, hasDest bool,
) (sql.Result, error) {
if q.err != nil {
return nil, q.err
}
if q.table != nil {
if err := q.beforeInsertHook(ctx); err != nil {
return nil, err
}
}
// Run append model hooks before generating the query.
if err := q.beforeAppendModel(ctx, q); err != nil {
return nil, err
}
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
// Generate the query before checking hasReturning.
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
useScan := hasDest || (q.hasReturning() && q.hasFeature(feature.InsertReturning|feature.Output))
var model Model
if useScan {
var err error
model, err = q.getModel(dest)
if err != nil {
return nil, err
}
}
query := internal.String(queryBytes)
var res sql.Result
if useScan {
res, err = q.scan(ctx, q, query, model, hasDest)
if err != nil {
return nil, err
}
} else {
res, err = q.exec(ctx, q, query)
if err != nil {
return nil, err
}
if err := q.tryLastInsertID(res, dest); err != nil {
return nil, err
}
}
if q.table != nil {
if err := q.afterInsertHook(ctx); err != nil {
return nil, err
}
}
return res, nil
}
func (q *InsertQuery) beforeInsertHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(BeforeInsertHook); ok {
if err := hook.BeforeInsert(ctx, q); err != nil {
return err
}
}
return nil
}
func (q *InsertQuery) afterInsertHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(AfterInsertHook); ok {
if err := hook.AfterInsert(ctx, q); err != nil {
return err
}
}
return nil
}
func (q *InsertQuery) tryLastInsertID(res sql.Result, dest []any) error {
if q.db.HasFeature(feature.Returning) ||
q.db.HasFeature(feature.Output) ||
q.table == nil ||
len(q.table.PKs) != 1 ||
!q.table.PKs[0].AutoIncrement {
return nil
}
id, err := res.LastInsertId()
if err != nil {
return err
}
if id == 0 {
return nil
}
model, err := q.getModel(dest)
if err != nil {
return err
}
pk := q.table.PKs[0]
switch model := model.(type) {
case *structTableModel:
if err := pk.ScanValue(model.strct, id); err != nil {
return err
}
case *sliceTableModel:
sliceLen := model.slice.Len()
for i := 0; i < sliceLen; i++ {
strct := indirect(model.slice.Index(i))
if err := pk.ScanValue(strct, id); err != nil {
return err
}
id++
}
}
return nil
}
// String returns the generated SQL query string. The InsertQuery instance must not be
// modified during query generation to ensure multiple calls to String() return identical results.
func (q *InsertQuery) String() string {
buf, err := q.AppendQuery(q.db.QueryGen(), nil)
if err != nil {
panic(err)
}
return string(buf)
}
+353
View File
@@ -0,0 +1,353 @@
package bun
import (
"context"
"database/sql"
"errors"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// MergeQuery builds MERGE statements for dialects that support them.
type MergeQuery struct {
baseQuery
returningQuery
using schema.QueryWithArgs
on schema.QueryWithArgs
when []schema.QueryAppender
comment string
}
var _ Query = (*MergeQuery)(nil)
// NewMergeQuery creates a MergeQuery associated with the provided DB.
func NewMergeQuery(db *DB) *MergeQuery {
q := &MergeQuery{
baseQuery: baseQuery{
db: db,
},
}
if !q.db.HasFeature(feature.Merge) {
q.setErr(errors.New("bun: merge not supported for current dialect"))
}
return q
}
func (q *MergeQuery) Conn(db IConn) *MergeQuery {
q.setConn(db)
return q
}
func (q *MergeQuery) Model(model any) *MergeQuery {
q.setModel(model)
return q
}
func (q *MergeQuery) Err(err error) *MergeQuery {
q.setErr(err)
return q
}
// Apply calls each function in fns, passing the MergeQuery as an argument.
func (q *MergeQuery) Apply(fns ...func(*MergeQuery) *MergeQuery) *MergeQuery {
for _, fn := range fns {
if fn != nil {
q = fn(q)
}
}
return q
}
func (q *MergeQuery) With(name string, query Query) *MergeQuery {
q.addWith(NewWithQuery(name, query))
return q
}
func (q *MergeQuery) WithRecursive(name string, query Query) *MergeQuery {
q.addWith(NewWithQuery(name, query).Recursive())
return q
}
func (q *MergeQuery) WithQuery(query *WithQuery) *MergeQuery {
q.addWith(query)
return q
}
// ------------------------------------------------------------------------------
func (q *MergeQuery) Table(tables ...string) *MergeQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *MergeQuery) TableExpr(query string, args ...any) *MergeQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *MergeQuery) ModelTableExpr(query string, args ...any) *MergeQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
// Returning adds a RETURNING clause to the query.
//
// To suppress the auto-generated RETURNING clause, use `Returning("NULL")`.
// Supported for PostgreSQL 17+ and MSSQL (via OUTPUT clause)
func (q *MergeQuery) Returning(query string, args ...any) *MergeQuery {
q.addReturning(schema.SafeQuery(query, args))
return q
}
//------------------------------------------------------------------------------
func (q *MergeQuery) Using(s string, args ...any) *MergeQuery {
q.using = schema.SafeQuery(s, args)
return q
}
func (q *MergeQuery) On(s string, args ...any) *MergeQuery {
q.on = schema.SafeQuery(s, args)
return q
}
// WhenInsert for when insert clause.
func (q *MergeQuery) WhenInsert(expr string, fn func(q *InsertQuery) *InsertQuery) *MergeQuery {
sq := NewInsertQuery(q.db)
// apply the model as default into sub query, since appendColumnsValues required
if q.model != nil {
sq = sq.Model(q.model)
}
sq = sq.Apply(fn)
q.when = append(q.when, &whenInsert{expr: expr, query: sq})
return q
}
// WhenUpdate for when update clause.
func (q *MergeQuery) WhenUpdate(expr string, fn func(q *UpdateQuery) *UpdateQuery) *MergeQuery {
sq := NewUpdateQuery(q.db)
// apply the model as default into sub query
if q.model != nil {
sq = sq.Model(q.model)
}
sq = sq.Apply(fn)
q.when = append(q.when, &whenUpdate{expr: expr, query: sq})
return q
}
// WhenDelete for when delete clause.
func (q *MergeQuery) WhenDelete(expr string) *MergeQuery {
q.when = append(q.when, &whenDelete{expr: expr})
return q
}
// When for raw expression clause.
func (q *MergeQuery) When(expr string, args ...any) *MergeQuery {
q.when = append(q.when, schema.SafeQuery(expr, args))
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *MergeQuery) Comment(comment string) *MergeQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *MergeQuery) Operation() string {
return "MERGE"
}
func (q *MergeQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
gen = formatterWithModel(gen, q)
b, err = q.appendWith(gen, b)
if err != nil {
return nil, err
}
b = append(b, "MERGE "...)
if q.db.dialect.Name() == dialect.PG {
b = append(b, "INTO "...)
}
b, err = q.appendFirstTableWithAlias(gen, b)
if err != nil {
return nil, err
}
b = append(b, " USING "...)
b, err = q.using.AppendQuery(gen, b)
if err != nil {
return nil, err
}
b = append(b, " ON "...)
b, err = q.on.AppendQuery(gen, b)
if err != nil {
return nil, err
}
for _, w := range q.when {
b = append(b, " WHEN "...)
b, err = w.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
if q.hasFeature(feature.Output) && q.hasReturning() {
b = append(b, " OUTPUT "...)
b, err = q.appendOutput(gen, b)
if err != nil {
return nil, err
}
}
if q.hasFeature(feature.MergeReturning) && q.hasReturning() {
b = append(b, " RETURNING "...)
b, err = q.appendReturning(gen, b)
if err != nil {
return nil, err
}
}
// A MERGE statement must be terminated by a semi-colon (;).
b = append(b, ";"...)
return b, nil
}
//------------------------------------------------------------------------------
func (q *MergeQuery) Scan(ctx context.Context, dest ...any) error {
_, err := q.scanOrExec(ctx, dest, true)
return err
}
func (q *MergeQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
return q.scanOrExec(ctx, dest, len(dest) > 0)
}
func (q *MergeQuery) scanOrExec(
ctx context.Context, dest []any, hasDest bool,
) (sql.Result, error) {
if q.err != nil {
return nil, q.err
}
// Run append model hooks before generating the query.
if err := q.beforeAppendModel(ctx, q); err != nil {
return nil, err
}
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
// Generate the query before checking hasReturning.
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
useScan := hasDest || (q.hasReturning() && q.hasFeature(feature.InsertReturning|feature.MergeReturning|feature.Output))
var model Model
if useScan {
var err error
model, err = q.getModel(dest)
if err != nil {
return nil, err
}
}
query := internal.String(queryBytes)
var res sql.Result
if useScan {
res, err = q.scan(ctx, q, query, model, true)
if err != nil {
return nil, err
}
} else {
res, err = q.exec(ctx, q, query)
if err != nil {
return nil, err
}
}
return res, nil
}
// String returns the generated SQL query string. The MergeQuery instance must not be
// modified during query generation to ensure multiple calls to String() return identical results.
func (q *MergeQuery) String() string {
buf, err := q.AppendQuery(q.db.QueryGen(), nil)
if err != nil {
panic(err)
}
return string(buf)
}
//------------------------------------------------------------------------------
type whenInsert struct {
expr string
query *InsertQuery
}
func (w *whenInsert) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
b = append(b, w.expr...)
if w.query != nil {
b = append(b, " THEN INSERT"...)
b, err = w.query.appendColumnsValues(gen, b, true)
if err != nil {
return nil, err
}
}
return b, nil
}
type whenUpdate struct {
expr string
query *UpdateQuery
}
func (w *whenUpdate) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
b = append(b, w.expr...)
if w.query != nil {
b = append(b, " THEN UPDATE SET "...)
b, err = w.query.appendSet(gen, b)
if err != nil {
return nil, err
}
}
return b, nil
}
type whenDelete struct {
expr string
}
func (w *whenDelete) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
b = append(b, w.expr...)
b = append(b, " THEN DELETE"...)
return b, nil
}
+109
View File
@@ -0,0 +1,109 @@
package bun
import (
"context"
"database/sql"
"github.com/uptrace/bun/schema"
)
// RawQuery executes a plain SQL query using Bun formatting and hooks.
type RawQuery struct {
baseQuery
query string
args []any
comment string
}
// NewRawQuery creates a RawQuery with the provided SQL template and arguments.
func NewRawQuery(db *DB, query string, args ...any) *RawQuery {
return &RawQuery{
baseQuery: baseQuery{
db: db,
},
query: query,
args: args,
}
}
func (q *RawQuery) Conn(db IConn) *RawQuery {
q.setConn(db)
return q
}
func (q *RawQuery) Err(err error) *RawQuery {
q.setErr(err)
return q
}
func (q *RawQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
return q.scanOrExec(ctx, dest, len(dest) > 0)
}
func (q *RawQuery) Scan(ctx context.Context, dest ...any) error {
_, err := q.scanOrExec(ctx, dest, true)
return err
}
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *RawQuery) Comment(comment string) *RawQuery {
q.comment = comment
return q
}
func (q *RawQuery) scanOrExec(
ctx context.Context, dest []any, hasDest bool,
) (sql.Result, error) {
if q.err != nil {
return nil, q.err
}
var model Model
var err error
if hasDest {
model, err = q.getModel(dest)
if err != nil {
return nil, err
}
}
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
query := q.db.format(q.query, q.args)
var res sql.Result
if hasDest {
res, err = q.scan(ctx, q, query, model, hasDest)
} else {
res, err = q.exec(ctx, q, query)
}
if err != nil {
return nil, err
}
return res, nil
}
func (q *RawQuery) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error) {
b = appendComment(b, q.comment)
return gen.AppendQuery(b, q.query, q.args...), nil
}
func (q *RawQuery) Operation() string {
return "SELECT"
}
// String returns the generated SQL query string. The RawQuery instance must not be
// modified during query generation to ensure multiple calls to String() return identical results.
func (q *RawQuery) String() string {
buf, err := q.AppendQuery(q.db.QueryGen(), nil)
if err != nil {
panic(err)
}
return string(buf)
}
+1439
View File
File diff suppressed because it is too large Load Diff
+429
View File
@@ -0,0 +1,429 @@
package bun
import (
"bytes"
"context"
"database/sql"
"fmt"
"slices"
"strconv"
"strings"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/dialect/sqltype"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// CreateTableQuery builds CREATE TABLE statements.
type CreateTableQuery struct {
baseQuery
temp bool
ifNotExists bool
fksFromRel bool // Create foreign keys captured in table's relations.
// varchar changes the default length for VARCHAR columns.
// Because some dialects require that length is always specified for VARCHAR type,
// we will use the exact user-defined type if length is set explicitly, as in `bun:",type:varchar(5)"`,
// but assume the new default length when it's omitted, e.g. `bun:",type:varchar"`.
varchar int
fks []schema.QueryWithArgs
partitionBy schema.QueryWithArgs
tablespace schema.QueryWithArgs
comment string
}
var _ Query = (*CreateTableQuery)(nil)
// NewCreateTableQuery returns a CreateTableQuery bound to the provided DB.
func NewCreateTableQuery(db *DB) *CreateTableQuery {
q := &CreateTableQuery{
baseQuery: baseQuery{
db: db,
},
varchar: db.Dialect().DefaultVarcharLen(),
}
return q
}
func (q *CreateTableQuery) Conn(db IConn) *CreateTableQuery {
q.setConn(db)
return q
}
func (q *CreateTableQuery) Model(model any) *CreateTableQuery {
q.setModel(model)
return q
}
func (q *CreateTableQuery) Err(err error) *CreateTableQuery {
q.setErr(err)
return q
}
// ------------------------------------------------------------------------------
func (q *CreateTableQuery) Table(tables ...string) *CreateTableQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *CreateTableQuery) TableExpr(query string, args ...any) *CreateTableQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *CreateTableQuery) ModelTableExpr(query string, args ...any) *CreateTableQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
func (q *CreateTableQuery) ColumnExpr(query string, args ...any) *CreateTableQuery {
q.addColumn(schema.SafeQuery(query, args))
return q
}
// ------------------------------------------------------------------------------
func (q *CreateTableQuery) Temp() *CreateTableQuery {
q.temp = true
return q
}
func (q *CreateTableQuery) IfNotExists() *CreateTableQuery {
q.ifNotExists = true
return q
}
// Varchar sets default length for VARCHAR columns.
func (q *CreateTableQuery) Varchar(n int) *CreateTableQuery {
if n <= 0 {
q.setErr(fmt.Errorf("bun: illegal VARCHAR length: %d", n))
return q
}
q.varchar = n
return q
}
func (q *CreateTableQuery) ForeignKey(query string, args ...any) *CreateTableQuery {
q.fks = append(q.fks, schema.SafeQuery(query, args))
return q
}
func (q *CreateTableQuery) PartitionBy(query string, args ...any) *CreateTableQuery {
q.partitionBy = schema.SafeQuery(query, args)
return q
}
func (q *CreateTableQuery) TableSpace(tablespace string) *CreateTableQuery {
q.tablespace = schema.UnsafeIdent(tablespace)
return q
}
// WithForeignKeys adds a FOREIGN KEY clause for each of the model's existing relations.
func (q *CreateTableQuery) WithForeignKeys() *CreateTableQuery {
q.fksFromRel = true
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *CreateTableQuery) Comment(comment string) *CreateTableQuery {
q.comment = comment
return q
}
// ------------------------------------------------------------------------------
func (q *CreateTableQuery) Operation() string {
return "CREATE TABLE"
}
func (q *CreateTableQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
if q.table == nil {
return nil, errNilModel
}
b = append(b, "CREATE "...)
if q.temp {
b = append(b, "TEMP "...)
}
b = append(b, "TABLE "...)
if q.ifNotExists && gen.HasFeature(feature.TableNotExists) {
b = append(b, "IF NOT EXISTS "...)
}
b, err = q.appendFirstTable(gen, b)
if err != nil {
return nil, err
}
b = append(b, " ("...)
for i, field := range q.table.Fields {
if i > 0 {
b = append(b, ", "...)
}
b = append(b, field.SQLName...)
b = append(b, " "...)
b = q.appendSQLType(b, field)
if field.NotNull && q.db.dialect.Name() != dialect.Oracle {
b = append(b, " NOT NULL"...)
}
if (field.Identity && gen.HasFeature(feature.GeneratedIdentity)) ||
(field.AutoIncrement && (gen.HasFeature(feature.AutoIncrement) || gen.HasFeature(feature.Identity))) {
b = q.db.dialect.AppendSequence(b, q.table, field)
}
if field.SQLDefault != "" {
b = append(b, " DEFAULT "...)
b = append(b, field.SQLDefault...)
}
}
for i, col := range q.columns {
// Only pre-pend the comma if we are on subsequent iterations, or if there were fields/columns appended before
// this. This way if we are only appending custom column expressions we will not produce a syntax error with a
// leading comma.
if i > 0 || len(q.table.Fields) > 0 {
b = append(b, ", "...)
}
b, err = col.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
// In SQLite AUTOINCREMENT is only valid for INTEGER PRIMARY KEY columns, so it might be that
// a primary key constraint has already been created in dialect.AppendSequence() call above.
// See sqldialect.Dialect.AppendSequence() for more details.
if len(q.table.PKs) > 0 && !bytes.Contains(b, []byte("PRIMARY KEY")) {
b = q.appendPKConstraint(b, q.table.PKs)
}
b = q.appendUniqueConstraints(gen, b)
if q.fksFromRel {
b, err = q.appendFKConstraintsRel(gen, b)
if err != nil {
return nil, err
}
}
b, err = q.appendFKConstraints(gen, b)
if err != nil {
return nil, err
}
b = append(b, ")"...)
if !q.partitionBy.IsZero() {
b = append(b, " PARTITION BY "...)
b, err = q.partitionBy.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
if !q.tablespace.IsZero() {
b = append(b, " TABLESPACE "...)
b, err = q.tablespace.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
return b, nil
}
func (q *CreateTableQuery) appendSQLType(b []byte, field *schema.Field) []byte {
// Most of the time these two will match, but for the cases where DiscoveredSQLType is dialect-specific,
// e.g. pgdialect would change sqltype.SmallInt to pgTypeSmallSerial for columns that have `bun:",autoincrement"`
if !strings.EqualFold(field.CreateTableSQLType, field.DiscoveredSQLType) {
return append(b, field.CreateTableSQLType...)
}
// For all common SQL types except VARCHAR, both UserDefinedSQLType and DiscoveredSQLType specify the correct type,
// and we needn't modify it. For VARCHAR columns, we will stop to check if a valid length has been set in .Varchar(int).
if !strings.EqualFold(field.CreateTableSQLType, sqltype.VarChar) || q.varchar <= 0 {
return append(b, field.CreateTableSQLType...)
}
if q.db.dialect.Name() == dialect.Oracle {
b = append(b, "VARCHAR2"...)
} else {
b = append(b, sqltype.VarChar...)
}
b = append(b, "("...)
b = strconv.AppendInt(b, int64(q.varchar), 10)
b = append(b, ")"...)
return b
}
func (q *CreateTableQuery) appendUniqueConstraints(gen schema.QueryGen, b []byte) []byte {
unique := q.table.Unique
keys := make([]string, 0, len(unique))
for key := range unique {
keys = append(keys, key)
}
slices.Sort(keys)
for _, key := range keys {
if key == "" {
for _, field := range unique[key] {
b = q.appendUniqueConstraint(gen, b, key, field)
}
continue
}
b = q.appendUniqueConstraint(gen, b, key, unique[key]...)
}
return b
}
func (q *CreateTableQuery) appendUniqueConstraint(
gen schema.QueryGen, b []byte, name string, fields ...*schema.Field,
) []byte {
if name != "" {
b = append(b, ", CONSTRAINT "...)
b = gen.AppendIdent(b, name)
} else {
b = append(b, ","...)
}
b = append(b, " UNIQUE ("...)
b = appendColumns(b, "", fields)
b = append(b, ")"...)
return b
}
// appendFKConstraintsRel appends a FOREIGN KEY clause for each of the model's existing relations.
func (q *CreateTableQuery) appendFKConstraintsRel(gen schema.QueryGen, b []byte) (_ []byte, err error) {
relations := q.tableModel.Table().Relations
keys := make([]string, 0, len(relations))
for key := range relations {
keys = append(keys, key)
}
slices.Sort(keys)
for _, key := range keys {
if rel := relations[key]; rel.References() {
query := "(?) REFERENCES ? (?)"
args := []any{
Safe(appendColumns(nil, "", rel.BasePKs)),
rel.JoinTable.SQLName,
Safe(appendColumns(nil, "", rel.JoinPKs)),
}
if len(rel.OnUpdate) > 0 {
query += " ?"
args = append(args, Safe(rel.OnUpdate))
}
if len(rel.OnDelete) > 0 {
query += " ?"
args = append(args, Safe(rel.OnDelete))
}
b, err = q.appendFK(gen, b, schema.QueryWithArgs{
Query: query,
Args: args,
})
if err != nil {
return nil, err
}
}
}
return b, nil
}
func (q *CreateTableQuery) appendFK(gen schema.QueryGen, b []byte, fk schema.QueryWithArgs) (_ []byte, err error) {
b = append(b, ", FOREIGN KEY "...)
return fk.AppendQuery(gen, b)
}
func (q *CreateTableQuery) appendFKConstraints(
gen schema.QueryGen, b []byte,
) (_ []byte, err error) {
for _, fk := range q.fks {
if b, err = q.appendFK(gen, b, fk); err != nil {
return nil, err
}
}
return b, nil
}
func (q *CreateTableQuery) appendPKConstraint(b []byte, pks []*schema.Field) []byte {
b = append(b, ", PRIMARY KEY ("...)
b = appendColumns(b, "", pks)
b = append(b, ")"...)
return b
}
// ------------------------------------------------------------------------------
func (q *CreateTableQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
if err := q.beforeCreateTableHook(ctx); err != nil {
return nil, err
}
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
query := internal.String(queryBytes)
res, err := q.exec(ctx, q, query)
if err != nil {
return nil, err
}
if q.table != nil {
if err := q.afterCreateTableHook(ctx); err != nil {
return nil, err
}
}
return res, nil
}
func (q *CreateTableQuery) beforeCreateTableHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(BeforeCreateTableHook); ok {
if err := hook.BeforeCreateTable(ctx, q); err != nil {
return err
}
}
return nil
}
func (q *CreateTableQuery) afterCreateTableHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(AfterCreateTableHook); ok {
if err := hook.AfterCreateTable(ctx, q); err != nil {
return err
}
}
return nil
}
// String returns the generated SQL query string. The CreateTableQuery instance must not be
// modified during query generation to ensure multiple calls to String() return identical results.
func (q *CreateTableQuery) String() string {
buf, err := q.AppendQuery(q.db.QueryGen(), nil)
if err != nil {
panic(err)
}
return string(buf)
}
+178
View File
@@ -0,0 +1,178 @@
package bun
import (
"context"
"database/sql"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// DropTableQuery builds DROP TABLE statements.
type DropTableQuery struct {
baseQuery
cascadeQuery
ifExists bool
comment string
}
var _ Query = (*DropTableQuery)(nil)
// NewDropTableQuery returns a DropTableQuery tied to the provided DB.
func NewDropTableQuery(db *DB) *DropTableQuery {
q := &DropTableQuery{
baseQuery: baseQuery{
db: db,
},
}
return q
}
func (q *DropTableQuery) Conn(db IConn) *DropTableQuery {
q.setConn(db)
return q
}
func (q *DropTableQuery) Model(model any) *DropTableQuery {
q.setModel(model)
return q
}
func (q *DropTableQuery) Err(err error) *DropTableQuery {
q.setErr(err)
return q
}
//------------------------------------------------------------------------------
func (q *DropTableQuery) Table(tables ...string) *DropTableQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *DropTableQuery) TableExpr(query string, args ...any) *DropTableQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *DropTableQuery) ModelTableExpr(query string, args ...any) *DropTableQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *DropTableQuery) IfExists() *DropTableQuery {
q.ifExists = true
return q
}
func (q *DropTableQuery) Cascade() *DropTableQuery {
q.cascade = true
return q
}
func (q *DropTableQuery) Restrict() *DropTableQuery {
q.restrict = true
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *DropTableQuery) Comment(comment string) *DropTableQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *DropTableQuery) Operation() string {
return "DROP TABLE"
}
func (q *DropTableQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
b = append(b, "DROP TABLE "...)
if q.ifExists {
b = append(b, "IF EXISTS "...)
}
b, err = q.appendTables(gen, b)
if err != nil {
return nil, err
}
b = q.appendCascade(gen, b)
return b, nil
}
//------------------------------------------------------------------------------
func (q *DropTableQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
if q.table != nil {
if err := q.beforeDropTableHook(ctx); err != nil {
return nil, err
}
}
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
query := internal.String(queryBytes)
res, err := q.exec(ctx, q, query)
if err != nil {
return nil, err
}
if q.table != nil {
if err := q.afterDropTableHook(ctx); err != nil {
return nil, err
}
}
return res, nil
}
func (q *DropTableQuery) beforeDropTableHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(BeforeDropTableHook); ok {
if err := hook.BeforeDropTable(ctx, q); err != nil {
return err
}
}
return nil
}
func (q *DropTableQuery) afterDropTableHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(AfterDropTableHook); ok {
if err := hook.AfterDropTable(ctx, q); err != nil {
return err
}
}
return nil
}
// String returns the generated SQL query string. The DropTableQuery instance must not be
// modified during query generation to ensure multiple calls to String() return identical results.
func (q *DropTableQuery) String() string {
buf, err := q.AppendQuery(q.db.QueryGen(), nil)
if err != nil {
panic(err)
}
return string(buf)
}
+157
View File
@@ -0,0 +1,157 @@
package bun
import (
"context"
"database/sql"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// TruncateTableQuery builds TRUNCATE TABLE statements.
type TruncateTableQuery struct {
baseQuery
cascadeQuery
continueIdentity bool
comment string
}
var _ Query = (*TruncateTableQuery)(nil)
// NewTruncateTableQuery creates a TruncateTableQuery attached to the given DB.
func NewTruncateTableQuery(db *DB) *TruncateTableQuery {
q := &TruncateTableQuery{
baseQuery: baseQuery{
db: db,
},
}
return q
}
func (q *TruncateTableQuery) Conn(db IConn) *TruncateTableQuery {
q.setConn(db)
return q
}
func (q *TruncateTableQuery) Model(model any) *TruncateTableQuery {
q.setModel(model)
return q
}
func (q *TruncateTableQuery) Err(err error) *TruncateTableQuery {
q.setErr(err)
return q
}
//------------------------------------------------------------------------------
func (q *TruncateTableQuery) Table(tables ...string) *TruncateTableQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *TruncateTableQuery) TableExpr(query string, args ...any) *TruncateTableQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *TruncateTableQuery) ModelTableExpr(query string, args ...any) *TruncateTableQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *TruncateTableQuery) ContinueIdentity() *TruncateTableQuery {
q.continueIdentity = true
return q
}
func (q *TruncateTableQuery) Cascade() *TruncateTableQuery {
q.cascade = true
return q
}
func (q *TruncateTableQuery) Restrict() *TruncateTableQuery {
q.restrict = true
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *TruncateTableQuery) Comment(comment string) *TruncateTableQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *TruncateTableQuery) Operation() string {
return "TRUNCATE TABLE"
}
func (q *TruncateTableQuery) AppendQuery(
gen schema.QueryGen, b []byte,
) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
if !gen.HasFeature(feature.TableTruncate) {
b = append(b, "DELETE FROM "...)
b, err = q.appendTables(gen, b)
if err != nil {
return nil, err
}
return b, nil
}
b = append(b, "TRUNCATE TABLE "...)
b, err = q.appendTables(gen, b)
if err != nil {
return nil, err
}
if q.db.HasFeature(feature.TableIdentity) {
if q.continueIdentity {
b = append(b, " CONTINUE IDENTITY"...)
} else {
b = append(b, " RESTART IDENTITY"...)
}
}
b = q.appendCascade(gen, b)
return b, nil
}
//------------------------------------------------------------------------------
func (q *TruncateTableQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
query := internal.String(queryBytes)
res, err := q.exec(ctx, q, query)
if err != nil {
return nil, err
}
return res, nil
}
+679
View File
@@ -0,0 +1,679 @@
package bun
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
// UpdateQuery builds SQL UPDATE statements.
type UpdateQuery struct {
whereBaseQuery
orderLimitOffsetQuery
returningQuery
setQuery
idxHintsQuery
joins []joinQuery
comment string
}
var _ Query = (*UpdateQuery)(nil)
// NewUpdateQuery returns a new UpdateQuery attached to the provided DB.
func NewUpdateQuery(db *DB) *UpdateQuery {
q := &UpdateQuery{
whereBaseQuery: whereBaseQuery{
baseQuery: baseQuery{
db: db,
},
},
}
return q
}
func (q *UpdateQuery) Conn(db IConn) *UpdateQuery {
q.setConn(db)
return q
}
func (q *UpdateQuery) Model(model any) *UpdateQuery {
q.setModel(model)
return q
}
func (q *UpdateQuery) Err(err error) *UpdateQuery {
q.setErr(err)
return q
}
// Apply calls each function in fns, passing the UpdateQuery as an argument.
func (q *UpdateQuery) Apply(fns ...func(*UpdateQuery) *UpdateQuery) *UpdateQuery {
for _, fn := range fns {
if fn != nil {
q = fn(q)
}
}
return q
}
func (q *UpdateQuery) With(name string, query Query) *UpdateQuery {
q.addWith(NewWithQuery(name, query))
return q
}
func (q *UpdateQuery) WithRecursive(name string, query Query) *UpdateQuery {
q.addWith(NewWithQuery(name, query).Recursive())
return q
}
func (q *UpdateQuery) WithQuery(query *WithQuery) *UpdateQuery {
q.addWith(query)
return q
}
// ------------------------------------------------------------------------------
func (q *UpdateQuery) Table(tables ...string) *UpdateQuery {
for _, table := range tables {
q.addTable(schema.UnsafeIdent(table))
}
return q
}
func (q *UpdateQuery) TableExpr(query string, args ...any) *UpdateQuery {
q.addTable(schema.SafeQuery(query, args))
return q
}
func (q *UpdateQuery) ModelTableExpr(query string, args ...any) *UpdateQuery {
q.modelTableName = schema.SafeQuery(query, args)
return q
}
//------------------------------------------------------------------------------
func (q *UpdateQuery) Column(columns ...string) *UpdateQuery {
for _, column := range columns {
q.addColumn(schema.UnsafeIdent(column))
}
return q
}
func (q *UpdateQuery) ExcludeColumn(columns ...string) *UpdateQuery {
q.excludeColumn(columns)
return q
}
func (q *UpdateQuery) Set(query string, args ...any) *UpdateQuery {
q.addSet(schema.SafeQuery(query, args))
return q
}
func (q *UpdateQuery) SetColumn(column string, query string, args ...any) *UpdateQuery {
if q.db.HasFeature(feature.UpdateMultiTable) {
column = q.table.Alias + "." + column
}
q.addSet(schema.SafeQuery(column+" = "+query, args))
return q
}
// Value overwrites model value for the column.
func (q *UpdateQuery) Value(column string, query string, args ...any) *UpdateQuery {
if q.table == nil {
q.setErr(errNilModel)
return q
}
q.addValue(q.table, column, query, args)
return q
}
func (q *UpdateQuery) OmitZero() *UpdateQuery {
q.omitZero = true
return q
}
//------------------------------------------------------------------------------
func (q *UpdateQuery) Join(join string, args ...any) *UpdateQuery {
q.joins = append(q.joins, joinQuery{
join: schema.SafeQuery(join, args),
})
return q
}
func (q *UpdateQuery) JoinOn(cond string, args ...any) *UpdateQuery {
return q.joinOn(cond, args, " AND ")
}
func (q *UpdateQuery) JoinOnOr(cond string, args ...any) *UpdateQuery {
return q.joinOn(cond, args, " OR ")
}
func (q *UpdateQuery) joinOn(cond string, args []any, sep string) *UpdateQuery {
if len(q.joins) == 0 {
q.setErr(errors.New("bun: query has no joins"))
return q
}
j := &q.joins[len(q.joins)-1]
j.on = append(j.on, schema.SafeQueryWithSep(cond, args, sep))
return q
}
//------------------------------------------------------------------------------
func (q *UpdateQuery) WherePK(cols ...string) *UpdateQuery {
q.addWhereCols(cols)
return q
}
func (q *UpdateQuery) Where(query string, args ...any) *UpdateQuery {
q.addWhere(schema.SafeQueryWithSep(query, args, " AND "))
return q
}
func (q *UpdateQuery) WhereOr(query string, args ...any) *UpdateQuery {
q.addWhere(schema.SafeQueryWithSep(query, args, " OR "))
return q
}
func (q *UpdateQuery) WhereGroup(sep string, fn func(*UpdateQuery) *UpdateQuery) *UpdateQuery {
saved := q.where
q.where = nil
q = fn(q)
where := q.where
q.where = saved
q.addWhereGroup(sep, where)
return q
}
func (q *UpdateQuery) WhereDeleted() *UpdateQuery {
q.whereDeleted()
return q
}
func (q *UpdateQuery) WhereAllWithDeleted() *UpdateQuery {
q.whereAllWithDeleted()
return q
}
// ------------------------------------------------------------------------------
func (q *UpdateQuery) Order(orders ...string) *UpdateQuery {
if !q.hasFeature(feature.UpdateOrderLimit) {
q.setErr(feature.NewNotSupportError(feature.UpdateOrderLimit))
return q
}
q.addOrder(orders...)
return q
}
func (q *UpdateQuery) OrderExpr(query string, args ...any) *UpdateQuery {
if !q.hasFeature(feature.UpdateOrderLimit) {
q.setErr(feature.NewNotSupportError(feature.UpdateOrderLimit))
return q
}
q.addOrderExpr(query, args...)
return q
}
func (q *UpdateQuery) Limit(n int) *UpdateQuery {
if !q.hasFeature(feature.UpdateOrderLimit) {
q.setErr(feature.NewNotSupportError(feature.UpdateOrderLimit))
return q
}
q.setLimit(n)
return q
}
//------------------------------------------------------------------------------
// Returning adds a RETURNING clause to the query.
//
// To suppress the auto-generated RETURNING clause, use `Returning("NULL")`.
func (q *UpdateQuery) Returning(query string, args ...any) *UpdateQuery {
q.addReturning(schema.SafeQuery(query, args))
return q
}
//------------------------------------------------------------------------------
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *UpdateQuery) Comment(comment string) *UpdateQuery {
q.comment = comment
return q
}
//------------------------------------------------------------------------------
func (q *UpdateQuery) Operation() string {
return "UPDATE"
}
func (q *UpdateQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
b = appendComment(b, q.comment)
gen = formatterWithModel(gen, q)
b, err = q.appendWith(gen, b)
if err != nil {
return nil, err
}
b = append(b, "UPDATE "...)
if gen.HasFeature(feature.UpdateMultiTable) {
b, err = q.appendTablesWithAlias(gen, b)
} else if gen.HasFeature(feature.UpdateTableAlias) {
b, err = q.appendFirstTableWithAlias(gen, b)
} else {
b, err = q.appendFirstTable(gen, b)
}
if err != nil {
return nil, err
}
b, err = q.appendIndexHints(gen, b)
if err != nil {
return nil, err
}
b, err = q.mustAppendSet(gen, b)
if err != nil {
return nil, err
}
if !gen.HasFeature(feature.UpdateMultiTable) {
b, err = q.appendOtherTables(gen, b)
if err != nil {
return nil, err
}
}
for _, j := range q.joins {
b, err = j.AppendQuery(gen, b)
if err != nil {
return nil, err
}
}
if q.hasFeature(feature.Output) && q.hasReturning() {
b = append(b, " OUTPUT "...)
b, err = q.appendOutput(gen, b)
if err != nil {
return nil, err
}
}
b, err = q.mustAppendWhere(gen, b, q.hasTableAlias(gen))
if err != nil {
return nil, err
}
b, err = q.appendOrder(gen, b)
if err != nil {
return nil, err
}
b, err = q.appendLimitOffset(gen, b)
if err != nil {
return nil, err
}
if q.hasFeature(feature.Returning) && q.hasReturning() {
b = append(b, " RETURNING "...)
b, err = q.appendReturning(gen, b)
if err != nil {
return nil, err
}
}
return b, nil
}
func (q *UpdateQuery) mustAppendSet(gen schema.QueryGen, b []byte) (_ []byte, err error) {
b = append(b, " SET "...)
pos := len(b)
switch model := q.model.(type) {
case *structTableModel:
if !model.strct.IsValid() { // Model((*Foo)(nil))
break
}
if len(q.set) > 0 && q.columns == nil {
break
}
fields, err := q.getDataFields()
if err != nil {
return nil, err
}
b, err = q.appendSetStruct(gen, b, model, fields)
if err != nil {
return nil, err
}
case *sliceTableModel:
if len(q.set) > 0 { // bulk-update
return q.appendSet(gen, b)
}
return nil, errors.New("bun: to bulk Update, use CTE and VALUES")
case *mapModel:
b = model.appendSet(gen, b)
case nil:
// continue below
default:
return nil, fmt.Errorf("bun: Update does not support %T", q.model)
}
if len(q.set) > 0 {
if len(b) > pos {
b = append(b, ", "...)
}
return q.appendSet(gen, b)
}
if len(b) == pos {
return nil, errors.New("bun: empty SET clause is not allowed in the UPDATE query")
}
return b, nil
}
func (q *UpdateQuery) appendOtherTables(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if !q.hasMultiTables() {
return b, nil
}
b = append(b, " FROM "...)
b, err = q.whereBaseQuery.appendOtherTables(gen, b)
if err != nil {
return nil, err
}
return b, nil
}
//------------------------------------------------------------------------------
func (q *UpdateQuery) Bulk() *UpdateQuery {
model, ok := q.model.(*sliceTableModel)
if !ok {
q.setErr(fmt.Errorf("bun: Bulk requires a slice, got %T", q.model))
return q
}
set, err := q.updateSliceSet(q.db.gen, model)
if err != nil {
q.setErr(err)
return q
}
values := q.db.NewValues(model)
values.customValueQuery = q.customValueQuery
return q.With("_data", values).
Model(model).
TableExpr("_data").
Set(set).
Where(q.updateSliceWhere(q.db.gen, model))
}
func (q *UpdateQuery) updateSliceSet(
gen schema.QueryGen, model *sliceTableModel,
) (string, error) {
fields, err := q.getDataFields()
if err != nil {
return "", err
}
var b []byte
pos := len(b)
for _, field := range fields {
if field.SkipUpdate() {
continue
}
if len(b) != pos {
b = append(b, ", "...)
pos = len(b)
}
if gen.HasFeature(feature.UpdateMultiTable) {
b = append(b, model.table.SQLAlias...)
b = append(b, '.')
}
b = append(b, field.SQLName...)
b = append(b, " = _data."...)
b = append(b, field.SQLName...)
}
return internal.String(b), nil
}
func (q *UpdateQuery) updateSliceWhere(gen schema.QueryGen, model *sliceTableModel) string {
var b []byte
for i, pk := range model.table.PKs {
if i > 0 {
b = append(b, " AND "...)
}
if q.hasTableAlias(gen) {
b = append(b, model.table.SQLAlias...)
} else {
b = append(b, model.table.SQLName...)
}
b = append(b, '.')
b = append(b, pk.SQLName...)
b = append(b, " = _data."...)
b = append(b, pk.SQLName...)
}
return internal.String(b)
}
//------------------------------------------------------------------------------
func (q *UpdateQuery) Scan(ctx context.Context, dest ...any) error {
_, err := q.scanOrExec(ctx, dest, true)
return err
}
func (q *UpdateQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) {
return q.scanOrExec(ctx, dest, len(dest) > 0)
}
func (q *UpdateQuery) scanOrExec(
ctx context.Context, dest []any, hasDest bool,
) (sql.Result, error) {
if q.err != nil {
return nil, q.err
}
if q.table != nil {
if err := q.beforeUpdateHook(ctx); err != nil {
return nil, err
}
}
// Run append model hooks before generating the query.
if err := q.beforeAppendModel(ctx, q); err != nil {
return nil, err
}
// if a comment is propagated via the context, use it
setCommentFromContext(ctx, q)
// Generate the query before checking hasReturning.
queryBytes, err := q.AppendQuery(q.db.gen, q.db.makeQueryBytes())
if err != nil {
return nil, err
}
useScan := hasDest || (q.hasReturning() && q.hasFeature(feature.Returning|feature.Output))
var model Model
if useScan {
var err error
model, err = q.getModel(dest)
if err != nil {
return nil, err
}
}
query := internal.String(queryBytes)
var res sql.Result
if useScan {
res, err = q.scan(ctx, q, query, model, hasDest)
if err != nil {
return nil, err
}
} else {
res, err = q.exec(ctx, q, query)
if err != nil {
return nil, err
}
}
if q.table != nil {
if err := q.afterUpdateHook(ctx); err != nil {
return nil, err
}
}
return res, nil
}
func (q *UpdateQuery) beforeUpdateHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(BeforeUpdateHook); ok {
if err := hook.BeforeUpdate(ctx, q); err != nil {
return err
}
}
return nil
}
func (q *UpdateQuery) afterUpdateHook(ctx context.Context) error {
if hook, ok := q.table.ZeroIface.(AfterUpdateHook); ok {
if err := hook.AfterUpdate(ctx, q); err != nil {
return err
}
}
return nil
}
// FQN returns a fully qualified column name, for example, table_name.column_name or
// table_alias.column_alias.
func (q *UpdateQuery) FQN(column string) Ident {
if q.table == nil {
panic("UpdateQuery.FQN requires a model")
}
if q.hasTableAlias(q.db.gen) {
return Ident(q.table.Alias + "." + column)
}
return Ident(q.table.Name + "." + column)
}
func (q *UpdateQuery) hasTableAlias(gen schema.QueryGen) bool {
return gen.HasFeature(feature.UpdateMultiTable | feature.UpdateTableAlias)
}
// String returns the generated SQL query string. The UpdateQuery instance must not be
// modified during query generation to ensure multiple calls to String() return identical results.
func (q *UpdateQuery) String() string {
buf, err := q.AppendQuery(q.db.QueryGen(), nil)
if err != nil {
panic(err)
}
return string(buf)
}
//------------------------------------------------------------------------------
func (q *UpdateQuery) QueryBuilder() QueryBuilder {
return &updateQueryBuilder{q}
}
func (q *UpdateQuery) ApplyQueryBuilder(fn func(QueryBuilder) QueryBuilder) *UpdateQuery {
return fn(q.QueryBuilder()).Unwrap().(*UpdateQuery)
}
type updateQueryBuilder struct {
*UpdateQuery
}
func (q *updateQueryBuilder) WhereGroup(
sep string, fn func(QueryBuilder) QueryBuilder,
) QueryBuilder {
q.UpdateQuery = q.UpdateQuery.WhereGroup(sep, func(qs *UpdateQuery) *UpdateQuery {
return fn(q).(*updateQueryBuilder).UpdateQuery
})
return q
}
func (q *updateQueryBuilder) Where(query string, args ...any) QueryBuilder {
q.UpdateQuery.Where(query, args...)
return q
}
func (q *updateQueryBuilder) WhereOr(query string, args ...any) QueryBuilder {
q.UpdateQuery.WhereOr(query, args...)
return q
}
func (q *updateQueryBuilder) WhereDeleted() QueryBuilder {
q.UpdateQuery.WhereDeleted()
return q
}
func (q *updateQueryBuilder) WhereAllWithDeleted() QueryBuilder {
q.UpdateQuery.WhereAllWithDeleted()
return q
}
func (q *updateQueryBuilder) WherePK(cols ...string) QueryBuilder {
q.UpdateQuery.WherePK(cols...)
return q
}
func (q *updateQueryBuilder) Unwrap() any {
return q.UpdateQuery
}
//------------------------------------------------------------------------------
func (q *UpdateQuery) UseIndex(indexes ...string) *UpdateQuery {
if q.db.dialect.Name() == dialect.MySQL {
q.addUseIndex(indexes...)
}
return q
}
func (q *UpdateQuery) IgnoreIndex(indexes ...string) *UpdateQuery {
if q.db.dialect.Name() == dialect.MySQL {
q.addIgnoreIndex(indexes...)
}
return q
}
func (q *UpdateQuery) ForceIndex(indexes ...string) *UpdateQuery {
if q.db.dialect.Name() == dialect.MySQL {
q.addForceIndex(indexes...)
}
return q
}
+252
View File
@@ -0,0 +1,252 @@
package bun
import (
"fmt"
"reflect"
"strconv"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/schema"
)
// ValuesQuery builds VALUES clauses that can be used as subqueries.
type ValuesQuery struct {
baseQuery
setQuery
withOrder bool
comment string
}
var (
_ Query = (*ValuesQuery)(nil)
_ schema.NamedArgAppender = (*ValuesQuery)(nil)
)
// NewValuesQuery creates a VALUES query for the given model.
func NewValuesQuery(db *DB, model any) *ValuesQuery {
q := &ValuesQuery{
baseQuery: baseQuery{
db: db,
},
}
q.setModel(model)
return q
}
func (q *ValuesQuery) Conn(db IConn) *ValuesQuery {
q.setConn(db)
return q
}
func (q *ValuesQuery) Err(err error) *ValuesQuery {
q.setErr(err)
return q
}
func (q *ValuesQuery) Column(columns ...string) *ValuesQuery {
for _, column := range columns {
q.addColumn(schema.UnsafeIdent(column))
}
return q
}
// Value overwrites model value for the column.
func (q *ValuesQuery) Value(column string, expr string, args ...any) *ValuesQuery {
if q.table == nil {
q.setErr(errNilModel)
return q
}
q.addValue(q.table, column, expr, args)
return q
}
func (q *ValuesQuery) OmitZero() *ValuesQuery {
q.omitZero = true
return q
}
func (q *ValuesQuery) WithOrder() *ValuesQuery {
q.withOrder = true
return q
}
// Comment adds a comment to the query, wrapped by /* ... */.
func (q *ValuesQuery) Comment(comment string) *ValuesQuery {
q.comment = comment
return q
}
func (q *ValuesQuery) AppendNamedArg(gen schema.QueryGen, b []byte, name string) ([]byte, bool) {
switch name {
case "Columns":
bb, err := q.AppendColumns(gen, b)
if err != nil {
q.setErr(err)
return b, true
}
return bb, true
}
return b, false
}
// AppendColumns appends the table columns. It is used by CTE.
func (q *ValuesQuery) AppendColumns(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
if q.model == nil {
return nil, errNilModel
}
if q.tableModel != nil {
fields, err := q.getFields()
if err != nil {
return nil, err
}
b = appendColumns(b, "", fields)
if q.withOrder {
b = append(b, ", _order"...)
}
return b, nil
}
switch model := q.model.(type) {
case *mapSliceModel:
return model.appendColumns(gen, b)
}
return nil, fmt.Errorf("bun: Values does not support %T", q.model)
}
func (q *ValuesQuery) Operation() string {
return "VALUES"
}
func (q *ValuesQuery) AppendQuery(gen schema.QueryGen, b []byte) (_ []byte, err error) {
if q.err != nil {
return nil, q.err
}
if q.model == nil {
return nil, errNilModel
}
b = appendComment(b, q.comment)
gen = formatterWithModel(gen, q)
b = append(b, "VALUES "...)
if q.db.HasFeature(feature.ValuesRow) {
b = append(b, "ROW("...)
} else {
b = append(b, '(')
}
switch model := q.model.(type) {
case *structTableModel:
fields, err := q.getFields()
if err != nil {
return nil, err
}
b, err = q.appendValues(gen, b, fields, model.strct)
if err != nil {
return nil, err
}
if q.withOrder {
b = append(b, ", "...)
b = strconv.AppendInt(b, 0, 10)
}
case *sliceTableModel:
fields, err := q.getFields()
if err != nil {
return nil, err
}
sliceLen := model.slice.Len()
for i := range sliceLen {
if i > 0 {
b = append(b, "), "...)
if q.db.HasFeature(feature.ValuesRow) {
b = append(b, "ROW("...)
} else {
b = append(b, '(')
}
}
b, err = q.appendValues(gen, b, fields, model.slice.Index(i))
if err != nil {
return nil, err
}
if q.withOrder {
b = append(b, ", "...)
b = strconv.AppendInt(b, int64(i), 10)
}
}
case *mapSliceModel:
b, err = model.appendValues(gen, b)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("bun: Values does not support %T", model)
}
b = append(b, ')')
return b, nil
}
func (q *ValuesQuery) appendValues(
gen schema.QueryGen, b []byte, fields []*schema.Field, strct reflect.Value,
) (_ []byte, err error) {
isTemplate := gen.IsNop()
for i, f := range fields {
if i > 0 {
b = append(b, ", "...)
}
app, ok := q.modelValues[f.Name]
if ok {
b, err = app.AppendQuery(gen, b)
if err != nil {
return nil, err
}
continue
}
if isTemplate {
b = append(b, '?')
} else {
b = f.AppendValue(gen, b, indirect(strct))
}
if gen.HasFeature(feature.DoubleColonCast) {
b = append(b, "::"...)
b = append(b, f.UserSQLType...)
}
}
return b, nil
}
func (q *ValuesQuery) appendSet(gen schema.QueryGen, b []byte) (_ []byte, err error) {
switch model := q.model.(type) {
case *mapModel:
return model.appendSet(gen, b), nil
case *structTableModel:
fields, err := q.getDataFields()
if err != nil {
return nil, err
}
return q.appendSetStruct(gen, b, model, fields)
default:
return nil, fmt.Errorf("bun: SetValues(unsupported %T)", model)
}
}
+453
View File
@@ -0,0 +1,453 @@
package bun
import (
"context"
"reflect"
"time"
"github.com/uptrace/bun/dialect/feature"
"github.com/uptrace/bun/internal"
"github.com/uptrace/bun/schema"
)
type relationJoin struct {
Parent *relationJoin
BaseModel TableModel
JoinModel TableModel
Relation *schema.Relation
additionalJoinOnConditions []schema.QueryWithArgs
apply func(*SelectQuery) *SelectQuery
columns []schema.QueryWithArgs
}
func (j *relationJoin) applyTo(q *SelectQuery) {
if j.apply == nil {
return
}
var table *schema.Table
var columns []schema.QueryWithArgs
// Save state.
table, q.table = q.table, j.JoinModel.Table()
columns, q.columns = q.columns, nil
q = j.apply(q)
// Restore state.
q.table = table
j.columns, q.columns = q.columns, columns
}
func (j *relationJoin) Select(ctx context.Context, q *SelectQuery) error {
switch j.Relation.Type {
}
panic("not reached")
}
func (j *relationJoin) selectMany(ctx context.Context, q *SelectQuery) error {
q = j.manyQuery(q)
if q == nil {
return nil
}
return q.Scan(ctx)
}
func (j *relationJoin) manyQuery(q *SelectQuery) *SelectQuery {
hasManyModel := newHasManyModel(j)
if hasManyModel == nil {
return nil
}
q = q.Model(hasManyModel)
var where []byte
if q.db.HasFeature(feature.CompositeIn) {
return j.manyQueryCompositeIn(where, q)
}
return j.manyQueryMulti(where, q)
}
func (j *relationJoin) manyQueryCompositeIn(where []byte, q *SelectQuery) *SelectQuery {
if len(j.Relation.JoinPKs) > 1 {
where = append(where, '(')
}
where = appendColumns(where, j.JoinModel.Table().SQLAlias, j.Relation.JoinPKs)
if len(j.Relation.JoinPKs) > 1 {
where = append(where, ')')
}
where = append(where, " IN ("...)
where = appendChildValues(
q.db.QueryGen(),
where,
j.JoinModel.rootValue(),
j.JoinModel.parentIndex(),
j.Relation.BasePKs,
)
where = append(where, ")"...)
if len(j.additionalJoinOnConditions) > 0 {
where = append(where, " AND "...)
where = appendAdditionalJoinOnConditions(q.db.QueryGen(), where, j.additionalJoinOnConditions)
}
q = q.Where(internal.String(where))
if j.Relation.PolymorphicField != nil {
q = q.Where("? = ?", j.Relation.PolymorphicField.SQLName, j.Relation.PolymorphicValue)
}
j.applyTo(q)
q = q.Apply(j.hasManyColumns)
return q
}
func (j *relationJoin) manyQueryMulti(where []byte, q *SelectQuery) *SelectQuery {
where = appendMultiValues(
q.db.QueryGen(),
where,
j.JoinModel.rootValue(),
j.JoinModel.parentIndex(),
j.Relation.BasePKs,
j.Relation.JoinPKs,
j.JoinModel.Table().SQLAlias,
)
q = q.Where(internal.String(where))
if len(j.additionalJoinOnConditions) > 0 {
q = q.Where(internal.String(appendAdditionalJoinOnConditions(q.db.QueryGen(), []byte{}, j.additionalJoinOnConditions)))
}
if j.Relation.PolymorphicField != nil {
q = q.Where("? = ?", j.Relation.PolymorphicField.SQLName, j.Relation.PolymorphicValue)
}
j.applyTo(q)
q = q.Apply(j.hasManyColumns)
return q
}
func (j *relationJoin) hasManyColumns(q *SelectQuery) *SelectQuery {
b := make([]byte, 0, 32)
joinTable := j.JoinModel.Table()
if len(j.columns) > 0 {
for i, col := range j.columns {
if i > 0 {
b = append(b, ", "...)
}
if col.Args == nil {
if field, ok := joinTable.FieldMap[col.Query]; ok {
b = append(b, joinTable.SQLAlias...)
b = append(b, '.')
b = append(b, field.SQLName...)
continue
}
}
var err error
b, err = col.AppendQuery(q.db.gen, b)
if err != nil {
q.setErr(err)
return q
}
}
} else {
b = appendColumns(b, joinTable.SQLAlias, joinTable.Fields)
}
q = q.ColumnExpr(internal.String(b))
return q
}
func (j *relationJoin) selectM2M(ctx context.Context, q *SelectQuery) error {
q = j.m2mQuery(q)
if q == nil {
return nil
}
return q.Scan(ctx)
}
func (j *relationJoin) m2mQuery(q *SelectQuery) *SelectQuery {
gen := q.db.gen
m2mModel := newM2MModel(j)
if m2mModel == nil {
return nil
}
q = q.Model(m2mModel)
index := j.JoinModel.parentIndex()
if j.Relation.M2MTable != nil {
// We only need base pks to park joined models to the base model.
fields := j.Relation.M2MBasePKs
b := make([]byte, 0, len(fields))
b = appendColumns(b, j.Relation.M2MTable.SQLAlias, fields)
q = q.ColumnExpr(internal.String(b))
}
//nolint
var join []byte
join = append(join, "JOIN "...)
join = gen.AppendQuery(join, string(j.Relation.M2MTable.SQLName))
join = append(join, " AS "...)
join = append(join, j.Relation.M2MTable.SQLAlias...)
join = append(join, " ON ("...)
for i, col := range j.Relation.M2MBasePKs {
if i > 0 {
join = append(join, ", "...)
}
join = append(join, j.Relation.M2MTable.SQLAlias...)
join = append(join, '.')
join = append(join, col.SQLName...)
}
join = append(join, ") IN ("...)
join = appendChildValues(gen, join, j.BaseModel.rootValue(), index, j.Relation.BasePKs)
join = append(join, ")"...)
if len(j.additionalJoinOnConditions) > 0 {
join = append(join, " AND "...)
join = appendAdditionalJoinOnConditions(gen, join, j.additionalJoinOnConditions)
}
q = q.Join(internal.String(join))
joinTable := j.JoinModel.Table()
for i, m2mJoinField := range j.Relation.M2MJoinPKs {
joinField := j.Relation.JoinPKs[i]
q = q.Where("?.? = ?.?",
joinTable.SQLAlias, joinField.SQLName,
j.Relation.M2MTable.SQLAlias, m2mJoinField.SQLName)
}
j.applyTo(q)
q = q.Apply(j.hasManyColumns)
return q
}
func (j *relationJoin) hasParent() bool {
if j.Parent != nil {
switch j.Parent.Relation.Type {
case schema.HasOneRelation, schema.BelongsToRelation:
return true
}
}
return false
}
func (j *relationJoin) appendAlias(gen schema.QueryGen, b []byte) []byte {
quote := gen.IdentQuote()
b = append(b, quote)
b = appendAlias(b, j)
b = append(b, quote)
return b
}
func (j *relationJoin) appendAliasColumn(gen schema.QueryGen, b []byte, column string) []byte {
quote := gen.IdentQuote()
b = append(b, quote)
b = appendAlias(b, j)
b = append(b, "__"...)
b = append(b, column...)
b = append(b, quote)
return b
}
func (j *relationJoin) appendBaseAlias(gen schema.QueryGen, b []byte) []byte {
quote := gen.IdentQuote()
if j.hasParent() {
b = append(b, quote)
b = appendAlias(b, j.Parent)
b = append(b, quote)
return b
}
return append(b, j.BaseModel.Table().SQLAlias...)
}
func (j *relationJoin) appendSoftDelete(
gen schema.QueryGen, b []byte, flags internal.Flag,
) []byte {
b = append(b, '.')
field := j.JoinModel.Table().SoftDeleteField
b = append(b, field.SQLName...)
if field.IsPtr || field.NullZero {
if flags.Has(deletedFlag) {
b = append(b, " IS NOT NULL"...)
} else {
b = append(b, " IS NULL"...)
}
} else {
if flags.Has(deletedFlag) {
b = append(b, " != "...)
} else {
b = append(b, " = "...)
}
b = gen.Dialect().AppendTime(b, time.Time{})
}
return b
}
func appendAlias(b []byte, j *relationJoin) []byte {
if j.hasParent() {
b = appendAlias(b, j.Parent)
b = append(b, "__"...)
}
b = append(b, j.Relation.Field.Name...)
return b
}
func (j *relationJoin) appendHasOneJoin(
gen schema.QueryGen, b []byte, q *SelectQuery,
) (_ []byte, err error) {
isSoftDelete := j.JoinModel.Table().SoftDeleteField != nil && !q.flags.Has(allWithDeletedFlag)
b = append(b, "LEFT JOIN "...)
b = gen.AppendQuery(b, string(j.JoinModel.Table().SQLNameForSelects))
b = append(b, " AS "...)
b = j.appendAlias(gen, b)
b = append(b, " ON "...)
b = append(b, '(')
for i, baseField := range j.Relation.BasePKs {
if i > 0 {
b = append(b, " AND "...)
}
b = j.appendAlias(gen, b)
b = append(b, '.')
b = append(b, j.Relation.JoinPKs[i].SQLName...)
b = append(b, " = "...)
b = j.appendBaseAlias(gen, b)
b = append(b, '.')
b = append(b, baseField.SQLName...)
}
b = append(b, ')')
if isSoftDelete {
b = append(b, " AND "...)
b = j.appendAlias(gen, b)
b = j.appendSoftDelete(gen, b, q.flags)
}
if len(j.additionalJoinOnConditions) > 0 {
b = append(b, " AND "...)
b = appendAdditionalJoinOnConditions(gen, b, j.additionalJoinOnConditions)
}
return b, nil
}
func appendChildValues(
gen schema.QueryGen, b []byte, v reflect.Value, index []int, fields []*schema.Field,
) []byte {
seen := make(map[string]struct{})
walk(v, index, func(v reflect.Value) {
start := len(b)
if len(fields) > 1 {
b = append(b, '(')
}
for i, f := range fields {
if i > 0 {
b = append(b, ", "...)
}
b = f.AppendValue(gen, b, v)
}
if len(fields) > 1 {
b = append(b, ')')
}
b = append(b, ", "...)
if _, ok := seen[string(b[start:])]; ok {
b = b[:start]
} else {
seen[string(b[start:])] = struct{}{}
}
})
if len(seen) > 0 {
b = b[:len(b)-2] // trim ", "
}
return b
}
// appendMultiValues is an alternative to appendChildValues that doesn't use the sql keyword ID
// but instead uses old style ((k1=v1) AND (k2=v2)) OR (...) conditions.
func appendMultiValues(
gen schema.QueryGen, b []byte, v reflect.Value, index []int, baseFields, joinFields []*schema.Field, joinTable schema.Safe,
) []byte {
// This is based on a mix of appendChildValues and query_base.appendColumns
// These should never mismatch in length but nice to know if it does
if len(joinFields) != len(baseFields) {
panic("not reached")
}
// walk the relations
b = append(b, '(')
seen := make(map[string]struct{})
walk(v, index, func(v reflect.Value) {
start := len(b)
for i, f := range baseFields {
if i > 0 {
b = append(b, " AND "...)
}
if len(baseFields) > 1 {
b = append(b, '(')
}
// Field name
b = append(b, joinTable...)
b = append(b, '.')
b = append(b, []byte(joinFields[i].SQLName)...)
// Equals value
b = append(b, '=')
b = f.AppendValue(gen, b, v)
if len(baseFields) > 1 {
b = append(b, ')')
}
}
b = append(b, ") OR ("...)
if _, ok := seen[string(b[start:])]; ok {
b = b[:start]
} else {
seen[string(b[start:])] = struct{}{}
}
})
if len(seen) > 0 {
b = b[:len(b)-6] // trim ") OR ("
}
b = append(b, ')')
return b
}
func appendAdditionalJoinOnConditions(
gen schema.QueryGen, b []byte, conditions []schema.QueryWithArgs,
) []byte {
for i, cond := range conditions {
if i > 0 {
b = append(b, " AND "...)
}
b = gen.AppendQuery(b, cond.Query, cond.Args...)
}
return b
}

Some files were not shown because too many files have changed in this diff Show More