fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
Generated
+4
@@ -0,0 +1,4 @@
|
||||
# Patterns for files created by this project.
|
||||
# For other files, use global gitignore.
|
||||
*.s3db
|
||||
.idea
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
trailingComma: all
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
singleQuote: true
|
||||
proseWrap: always
|
||||
printWidth: 100
|
||||
+1118
File diff suppressed because it is too large
Load Diff
+34
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,286 @@
|
||||
# Bun: SQL-first Golang ORM
|
||||
|
||||
[](https://github.com/uptrace/bun/actions)
|
||||
[](https://pkg.go.dev/github.com/uptrace/bun)
|
||||
[](https://bun.uptrace.dev/)
|
||||
[](https://discord.gg/rWtp5Aj)
|
||||
[](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
@@ -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
@@ -0,0 +1 @@
|
||||
module.exports = { extends: ['@commitlint/config-conventional'] }
|
||||
+881
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
package mssqldialect
|
||||
|
||||
// Version is the current release version.
|
||||
func Version() string {
|
||||
return "1.2.16"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2021 Vladimir Mihailenco. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"github.com/uptrace/bun/migrate/sqlschema"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
func (d *Dialect) NewMigrator(db *bun.DB, schemaName string) sqlschema.Migrator {
|
||||
return &migrator{db: db, schemaName: schemaName, BaseMigrator: sqlschema.NewBaseMigrator(db)}
|
||||
}
|
||||
|
||||
type migrator struct {
|
||||
*sqlschema.BaseMigrator
|
||||
|
||||
db *bun.DB
|
||||
schemaName string
|
||||
}
|
||||
|
||||
var _ sqlschema.Migrator = (*migrator)(nil)
|
||||
|
||||
func (m *migrator) AppendSQL(b []byte, operation any) (_ []byte, err error) {
|
||||
gen := m.db.QueryGen()
|
||||
|
||||
// Append ALTER TABLE statement to the enclosed query bytes []byte.
|
||||
appendAlterTable := func(query []byte, tableName string) []byte {
|
||||
query = append(query, "ALTER TABLE "...)
|
||||
query = m.appendFQN(gen, query, tableName)
|
||||
return append(query, " "...)
|
||||
}
|
||||
|
||||
switch change := operation.(type) {
|
||||
case *migrate.CreateTableOp:
|
||||
return m.AppendCreateTable(b, change.Model)
|
||||
case *migrate.DropTableOp:
|
||||
return m.AppendDropTable(b, m.schemaName, change.TableName)
|
||||
case *migrate.RenameTableOp:
|
||||
b, err = m.renameTable(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.RenameColumnOp:
|
||||
b, err = m.renameColumn(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.AddColumnOp:
|
||||
b, err = m.addColumn(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.DropColumnOp:
|
||||
b, err = m.dropColumn(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.AddPrimaryKeyOp:
|
||||
b, err = m.addPrimaryKey(gen, appendAlterTable(b, change.TableName), change.PrimaryKey)
|
||||
case *migrate.ChangePrimaryKeyOp:
|
||||
b, err = m.changePrimaryKey(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.DropPrimaryKeyOp:
|
||||
b, err = m.dropConstraint(gen, appendAlterTable(b, change.TableName), change.PrimaryKey.Name)
|
||||
case *migrate.AddUniqueConstraintOp:
|
||||
b, err = m.addUnique(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.DropUniqueConstraintOp:
|
||||
b, err = m.dropConstraint(gen, appendAlterTable(b, change.TableName), change.Unique.Name)
|
||||
case *migrate.ChangeColumnTypeOp:
|
||||
// If column changes to SERIAL, create sequence first.
|
||||
// https://gist.github.com/oleglomako/185df689706c5499612a0d54d3ffe856
|
||||
if !change.From.GetIsAutoIncrement() && change.To.GetIsAutoIncrement() {
|
||||
change.To, b, err = m.createDefaultSequence(gen, b, change)
|
||||
}
|
||||
b, err = m.changeColumnType(gen, appendAlterTable(b, change.TableName), change)
|
||||
case *migrate.AddForeignKeyOp:
|
||||
b, err = m.addForeignKey(gen, appendAlterTable(b, change.TableName()), change)
|
||||
case *migrate.DropForeignKeyOp:
|
||||
b, err = m.dropConstraint(gen, appendAlterTable(b, change.TableName()), change.ConstraintName)
|
||||
default:
|
||||
return nil, fmt.Errorf("append sql: unknown operation %T", change)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("append sql: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) appendFQN(gen schema.QueryGen, b []byte, tableName string) []byte {
|
||||
return gen.AppendQuery(b, "?.?", bun.Ident(m.schemaName), bun.Ident(tableName))
|
||||
}
|
||||
|
||||
func (m *migrator) renameTable(gen schema.QueryGen, b []byte, rename *migrate.RenameTableOp) (_ []byte, err error) {
|
||||
b = append(b, "RENAME TO "...)
|
||||
b = gen.AppendName(b, rename.NewName)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) renameColumn(gen schema.QueryGen, b []byte, rename *migrate.RenameColumnOp) (_ []byte, err error) {
|
||||
b = append(b, "RENAME COLUMN "...)
|
||||
b = gen.AppendName(b, rename.OldName)
|
||||
|
||||
b = append(b, " TO "...)
|
||||
b = gen.AppendName(b, rename.NewName)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) addColumn(gen schema.QueryGen, b []byte, add *migrate.AddColumnOp) (_ []byte, err error) {
|
||||
b = append(b, "ADD COLUMN "...)
|
||||
b = gen.AppendName(b, add.ColumnName)
|
||||
b = append(b, " "...)
|
||||
|
||||
b, err = add.Column.AppendQuery(gen, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if add.Column.GetDefaultValue() != "" {
|
||||
b = append(b, " DEFAULT "...)
|
||||
b = append(b, add.Column.GetDefaultValue()...)
|
||||
b = append(b, " "...)
|
||||
}
|
||||
|
||||
if add.Column.GetIsIdentity() {
|
||||
b = appendGeneratedAsIdentity(b)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) dropColumn(gen schema.QueryGen, b []byte, drop *migrate.DropColumnOp) (_ []byte, err error) {
|
||||
b = append(b, "DROP COLUMN "...)
|
||||
b = gen.AppendName(b, drop.ColumnName)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) addPrimaryKey(gen schema.QueryGen, b []byte, pk sqlschema.PrimaryKey) (_ []byte, err error) {
|
||||
b = append(b, "ADD PRIMARY KEY ("...)
|
||||
b, _ = pk.Columns.AppendQuery(gen, b)
|
||||
b = append(b, ")"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) changePrimaryKey(gen schema.QueryGen, b []byte, change *migrate.ChangePrimaryKeyOp) (_ []byte, err error) {
|
||||
b, _ = m.dropConstraint(gen, b, change.Old.Name)
|
||||
b = append(b, ", "...)
|
||||
b, _ = m.addPrimaryKey(gen, b, change.New)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) addUnique(gen schema.QueryGen, b []byte, change *migrate.AddUniqueConstraintOp) (_ []byte, err error) {
|
||||
b = append(b, "ADD CONSTRAINT "...)
|
||||
if change.Unique.Name != "" {
|
||||
b = gen.AppendName(b, change.Unique.Name)
|
||||
} else {
|
||||
// Default naming scheme for unique constraints in Postgres is <table>_<column>_key
|
||||
b = gen.AppendName(b, fmt.Sprintf("%s_%s_key", change.TableName, change.Unique.Columns))
|
||||
}
|
||||
b = append(b, " UNIQUE ("...)
|
||||
b, _ = change.Unique.Columns.AppendQuery(gen, b)
|
||||
b = append(b, ")"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) dropConstraint(gen schema.QueryGen, b []byte, name string) (_ []byte, err error) {
|
||||
b = append(b, "DROP CONSTRAINT "...)
|
||||
b = gen.AppendName(b, name)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) addForeignKey(gen schema.QueryGen, b []byte, add *migrate.AddForeignKeyOp) (_ []byte, err error) {
|
||||
b = append(b, "ADD CONSTRAINT "...)
|
||||
|
||||
name := add.ConstraintName
|
||||
if name == "" {
|
||||
colRef := add.ForeignKey.From
|
||||
columns := strings.Join(colRef.Column.Split(), "_")
|
||||
name = fmt.Sprintf("%s_%s_fkey", colRef.TableName, columns)
|
||||
}
|
||||
b = gen.AppendName(b, name)
|
||||
|
||||
b = append(b, " FOREIGN KEY ("...)
|
||||
if b, err = add.ForeignKey.From.Column.AppendQuery(gen, b); err != nil {
|
||||
return b, err
|
||||
}
|
||||
b = append(b, ")"...)
|
||||
|
||||
b = append(b, " REFERENCES "...)
|
||||
b = m.appendFQN(gen, b, add.ForeignKey.To.TableName)
|
||||
|
||||
b = append(b, " ("...)
|
||||
if b, err = add.ForeignKey.To.Column.AppendQuery(gen, b); err != nil {
|
||||
return b, err
|
||||
}
|
||||
b = append(b, ")"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// createDefaultSequence creates a SEQUENCE to back a serial column.
|
||||
// Having a backing sequence is necessary to change column type to SERIAL.
|
||||
// The updated Column's default is set to "nextval" of the new sequence.
|
||||
func (m *migrator) createDefaultSequence(_ schema.QueryGen, b []byte, op *migrate.ChangeColumnTypeOp) (_ sqlschema.Column, _ []byte, err error) {
|
||||
var last int
|
||||
if err = m.db.NewSelect().Table(op.TableName).
|
||||
ColumnExpr("MAX(?)", op.Column).Scan(context.TODO(), &last); err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
seq := op.TableName + "_" + op.Column + "_seq"
|
||||
fqn := op.TableName + "." + op.Column
|
||||
|
||||
// A sequence that is OWNED BY a table will be dropped
|
||||
// if the table is dropped with CASCADE action.
|
||||
b = append(b, "CREATE SEQUENCE "...)
|
||||
b = append(b, seq...)
|
||||
b = append(b, " START WITH "...)
|
||||
b = append(b, fmt.Sprint(last+1)...) // start with next value
|
||||
b = append(b, " OWNED BY "...)
|
||||
b = append(b, fqn...)
|
||||
b = append(b, ";\n"...)
|
||||
|
||||
return &Column{
|
||||
Name: op.To.GetName(),
|
||||
SQLType: op.To.GetSQLType(),
|
||||
VarcharLen: op.To.GetVarcharLen(),
|
||||
DefaultValue: fmt.Sprintf("nextval('%s'::regclass)", seq),
|
||||
IsNullable: op.To.GetIsNullable(),
|
||||
IsAutoIncrement: op.To.GetIsAutoIncrement(),
|
||||
IsIdentity: op.To.GetIsIdentity(),
|
||||
}, b, nil
|
||||
}
|
||||
|
||||
func (m *migrator) changeColumnType(gen schema.QueryGen, b []byte, colDef *migrate.ChangeColumnTypeOp) (_ []byte, err error) {
|
||||
// alterColumn never re-assigns err, so there is no need to check for err != nil after calling it
|
||||
var i int
|
||||
appendAlterColumn := func() {
|
||||
if i > 0 {
|
||||
b = append(b, ", "...)
|
||||
}
|
||||
b = append(b, "ALTER COLUMN "...)
|
||||
b = gen.AppendName(b, colDef.Column)
|
||||
i++
|
||||
}
|
||||
|
||||
got, want := colDef.From, colDef.To
|
||||
|
||||
inspector := m.db.Dialect().(sqlschema.InspectorDialect)
|
||||
if !inspector.CompareType(want, got) {
|
||||
appendAlterColumn()
|
||||
b = append(b, " SET DATA TYPE "...)
|
||||
if b, err = want.AppendQuery(gen, b); err != nil {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
|
||||
// Column must be declared NOT NULL before identity can be added.
|
||||
// Although PG can resolve the order of operations itself, we make this explicit in the query.
|
||||
if want.GetIsNullable() != got.GetIsNullable() {
|
||||
appendAlterColumn()
|
||||
if !want.GetIsNullable() {
|
||||
b = append(b, " SET NOT NULL"...)
|
||||
} else {
|
||||
b = append(b, " DROP NOT NULL"...)
|
||||
}
|
||||
}
|
||||
|
||||
if want.GetIsIdentity() != got.GetIsIdentity() {
|
||||
appendAlterColumn()
|
||||
if !want.GetIsIdentity() {
|
||||
b = append(b, " DROP IDENTITY"...)
|
||||
} else {
|
||||
b = append(b, " ADD"...)
|
||||
b = appendGeneratedAsIdentity(b)
|
||||
}
|
||||
}
|
||||
|
||||
if want.GetDefaultValue() != got.GetDefaultValue() {
|
||||
appendAlterColumn()
|
||||
if want.GetDefaultValue() == "" {
|
||||
b = append(b, " DROP DEFAULT"...)
|
||||
} else {
|
||||
b = append(b, " SET DEFAULT "...)
|
||||
b = append(b, want.GetDefaultValue()...)
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
driverValuerType = reflect.TypeFor[driver.Valuer]()
|
||||
|
||||
stringType = reflect.TypeFor[string]()
|
||||
sliceStringType = reflect.TypeFor[[]string]()
|
||||
|
||||
intType = reflect.TypeFor[int]()
|
||||
sliceIntType = reflect.TypeFor[[]int]()
|
||||
|
||||
int64Type = reflect.TypeFor[int64]()
|
||||
sliceInt64Type = reflect.TypeFor[[]int64]()
|
||||
|
||||
float64Type = reflect.TypeFor[float64]()
|
||||
sliceFloat64Type = reflect.TypeFor[[]float64]()
|
||||
|
||||
timeType = reflect.TypeFor[time.Time]()
|
||||
sliceTimeType = reflect.TypeFor[[]time.Time]()
|
||||
)
|
||||
|
||||
func appendTime(buf []byte, tm time.Time) []byte {
|
||||
return tm.UTC().AppendFormat(buf, "2006-01-02 15:04:05.999999-07:00")
|
||||
}
|
||||
|
||||
var mapStringStringType = reflect.TypeOf(map[string]string(nil))
|
||||
|
||||
func (d *Dialect) hstoreAppender(typ reflect.Type) schema.AppenderFunc {
|
||||
kind := typ.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if fn := d.hstoreAppender(typ.Elem()); fn != nil {
|
||||
return schema.PtrAppender(fn)
|
||||
}
|
||||
case reflect.Map:
|
||||
// ok:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if typ.Key() == stringType && typ.Elem() == stringType {
|
||||
return appendMapStringStringValue
|
||||
}
|
||||
|
||||
return func(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
err := fmt.Errorf("bun: Hstore(unsupported %s)", v.Type())
|
||||
return dialect.AppendError(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func appendMapStringString(b []byte, m map[string]string) []byte {
|
||||
if m == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
for key, value := range m {
|
||||
b = appendStringElem(b, key)
|
||||
b = append(b, '=', '>')
|
||||
b = appendStringElem(b, value)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(m) > 0 {
|
||||
b = b[:len(b)-1] // Strip trailing comma.
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendMapStringStringValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
m := v.Convert(mapStringStringType).Interface().(map[string]string)
|
||||
return appendMapStringString(b, m)
|
||||
}
|
||||
+608
@@ -0,0 +1,608 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/internal"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
type ArrayValue struct {
|
||||
v reflect.Value
|
||||
|
||||
append schema.AppenderFunc
|
||||
scan schema.ScannerFunc
|
||||
}
|
||||
|
||||
// Array accepts a slice and returns a wrapper for working with PostgreSQL
|
||||
// array data type.
|
||||
//
|
||||
// For struct fields you can use array tag:
|
||||
//
|
||||
// Emails []string `bun:",array"`
|
||||
func Array(vi any) *ArrayValue {
|
||||
v := reflect.ValueOf(vi)
|
||||
if !v.IsValid() {
|
||||
panic(fmt.Errorf("bun: Array(nil)"))
|
||||
}
|
||||
|
||||
return &ArrayValue{
|
||||
v: v,
|
||||
|
||||
append: pgDialect.arrayAppender(v.Type()),
|
||||
scan: arrayScanner(v.Type()),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ schema.QueryAppender = (*ArrayValue)(nil)
|
||||
_ sql.Scanner = (*ArrayValue)(nil)
|
||||
)
|
||||
|
||||
func (a *ArrayValue) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error) {
|
||||
if a.append == nil {
|
||||
panic(fmt.Errorf("bun: Array(unsupported %s)", a.v.Type()))
|
||||
}
|
||||
return a.append(gen, b, a.v), nil
|
||||
}
|
||||
|
||||
func (a *ArrayValue) Scan(src any) error {
|
||||
if a.scan == nil {
|
||||
return fmt.Errorf("bun: Array(unsupported %s)", a.v.Type())
|
||||
}
|
||||
if a.v.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("bun: Array(non-pointer %s)", a.v.Type())
|
||||
}
|
||||
return a.scan(a.v, src)
|
||||
}
|
||||
|
||||
func (a *ArrayValue) Value() any {
|
||||
if a.v.IsValid() {
|
||||
return a.v.Interface()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func (d *Dialect) arrayAppender(typ reflect.Type) schema.AppenderFunc {
|
||||
kind := typ.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if fn := d.arrayAppender(typ.Elem()); fn != nil {
|
||||
return schema.PtrAppender(fn)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
// continue below
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
elemType := typ.Elem()
|
||||
|
||||
if kind == reflect.Slice {
|
||||
switch elemType {
|
||||
case stringType:
|
||||
return appendStringSliceValue
|
||||
case intType:
|
||||
return appendIntSliceValue
|
||||
case int64Type:
|
||||
return appendInt64SliceValue
|
||||
case float64Type:
|
||||
return appendFloat64SliceValue
|
||||
case timeType:
|
||||
return appendTimeSliceValue
|
||||
}
|
||||
}
|
||||
|
||||
appendElem := d.arrayElemAppender(elemType)
|
||||
if appendElem == nil {
|
||||
panic(fmt.Errorf("pgdialect: %s is not supported", typ))
|
||||
}
|
||||
|
||||
return func(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
kind := v.Kind()
|
||||
switch kind {
|
||||
case reflect.Ptr, reflect.Slice:
|
||||
if v.IsNil() {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
}
|
||||
|
||||
if kind == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
b = append(b, "'{"...)
|
||||
|
||||
ln := v.Len()
|
||||
for i := 0; i < ln; i++ {
|
||||
elem := v.Index(i)
|
||||
if i > 0 {
|
||||
b = append(b, ',')
|
||||
}
|
||||
b = appendElem(gen, b, elem)
|
||||
}
|
||||
|
||||
b = append(b, "}'"...)
|
||||
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialect) arrayElemAppender(typ reflect.Type) schema.AppenderFunc {
|
||||
if typ.Implements(driverValuerType) {
|
||||
return arrayAppendDriverValue
|
||||
}
|
||||
if typ == timeType {
|
||||
return appendTimeElemValue
|
||||
}
|
||||
|
||||
switch typ.Kind() {
|
||||
case reflect.String:
|
||||
return appendStringElemValue
|
||||
case reflect.Slice:
|
||||
if typ.Elem().Kind() == reflect.Uint8 {
|
||||
return appendBytesElemValue
|
||||
}
|
||||
case reflect.Ptr:
|
||||
return schema.PtrAppender(d.arrayElemAppender(typ.Elem()))
|
||||
}
|
||||
return schema.Appender(d, typ)
|
||||
}
|
||||
|
||||
func appendTimeElemValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ts := v.Convert(timeType).Interface().(time.Time)
|
||||
|
||||
b = append(b, '"')
|
||||
b = appendTime(b, ts)
|
||||
return append(b, '"')
|
||||
}
|
||||
|
||||
func appendStringElemValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
return appendStringElem(b, v.String())
|
||||
}
|
||||
|
||||
func appendBytesElemValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
return appendBytesElem(b, v.Bytes())
|
||||
}
|
||||
|
||||
func arrayAppendDriverValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
iface, err := v.Interface().(driver.Valuer).Value()
|
||||
if err != nil {
|
||||
return dialect.AppendError(b, err)
|
||||
}
|
||||
return appendElem(b, iface)
|
||||
}
|
||||
|
||||
func appendStringSliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ss := v.Convert(sliceStringType).Interface().([]string)
|
||||
return appendStringSlice(b, ss)
|
||||
}
|
||||
|
||||
func appendStringSlice(b []byte, ss []string) []byte {
|
||||
if ss == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
b = append(b, '{')
|
||||
for _, s := range ss {
|
||||
b = appendStringElem(b, s)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(ss) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendIntSliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ints := v.Convert(sliceIntType).Interface().([]int)
|
||||
return appendIntSlice(b, ints)
|
||||
}
|
||||
|
||||
func appendIntSlice(b []byte, ints []int) []byte {
|
||||
if ints == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
b = append(b, '{')
|
||||
for _, n := range ints {
|
||||
b = strconv.AppendInt(b, int64(n), 10)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(ints) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendInt64SliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ints := v.Convert(sliceInt64Type).Interface().([]int64)
|
||||
return appendInt64Slice(b, ints)
|
||||
}
|
||||
|
||||
func appendInt64Slice(b []byte, ints []int64) []byte {
|
||||
if ints == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
b = append(b, '{')
|
||||
for _, n := range ints {
|
||||
b = strconv.AppendInt(b, n, 10)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(ints) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendFloat64SliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
floats := v.Convert(sliceFloat64Type).Interface().([]float64)
|
||||
return appendFloat64Slice(b, floats)
|
||||
}
|
||||
|
||||
func appendFloat64Slice(b []byte, floats []float64) []byte {
|
||||
if floats == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
b = append(b, '{')
|
||||
for _, n := range floats {
|
||||
b = arrayAppendFloat64(b, n)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(floats) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
|
||||
b = append(b, '\'')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func arrayAppendFloat64(b []byte, num float64) []byte {
|
||||
switch {
|
||||
case math.IsNaN(num):
|
||||
return append(b, "NaN"...)
|
||||
case math.IsInf(num, 1):
|
||||
return append(b, "Infinity"...)
|
||||
case math.IsInf(num, -1):
|
||||
return append(b, "-Infinity"...)
|
||||
default:
|
||||
return strconv.AppendFloat(b, num, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
|
||||
func appendTimeSliceValue(gen schema.QueryGen, b []byte, v reflect.Value) []byte {
|
||||
ts := v.Convert(sliceTimeType).Interface().([]time.Time)
|
||||
return appendTimeSlice(gen, b, ts)
|
||||
}
|
||||
|
||||
func appendTimeSlice(gen schema.QueryGen, b []byte, ts []time.Time) []byte {
|
||||
if ts == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
b = append(b, '\'')
|
||||
b = append(b, '{')
|
||||
for _, t := range ts {
|
||||
b = append(b, '"')
|
||||
b = appendTime(b, t)
|
||||
b = append(b, '"')
|
||||
b = append(b, ',')
|
||||
}
|
||||
if len(ts) > 0 {
|
||||
b[len(b)-1] = '}' // Replace trailing comma.
|
||||
} else {
|
||||
b = append(b, '}')
|
||||
}
|
||||
b = append(b, '\'')
|
||||
return b
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func arrayScanner(typ reflect.Type) schema.ScannerFunc {
|
||||
kind := typ.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if fn := arrayScanner(typ.Elem()); fn != nil {
|
||||
return schema.PtrScanner(fn)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
// ok:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
elemType := typ.Elem()
|
||||
|
||||
if kind == reflect.Slice {
|
||||
switch elemType {
|
||||
case stringType:
|
||||
return scanStringSliceValue
|
||||
case intType:
|
||||
return scanIntSliceValue
|
||||
case int64Type:
|
||||
return scanInt64SliceValue
|
||||
case float64Type:
|
||||
return scanFloat64SliceValue
|
||||
}
|
||||
}
|
||||
|
||||
scanElem := schema.Scanner(elemType)
|
||||
return func(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
kind := dest.Kind()
|
||||
|
||||
if src == nil {
|
||||
if kind != reflect.Slice || !dest.IsNil() {
|
||||
dest.Set(reflect.Zero(dest.Type()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if kind == reflect.Slice {
|
||||
if dest.IsNil() {
|
||||
dest.Set(reflect.MakeSlice(dest.Type(), 0, 0))
|
||||
} else if dest.Len() > 0 {
|
||||
dest.Set(dest.Slice(0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := newArrayParser(b)
|
||||
nextValue := internal.MakeSliceNextElemFunc(dest)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
elemValue := nextValue()
|
||||
if err := scanElem(elemValue, elem); err != nil {
|
||||
return fmt.Errorf("scanElem failed: %w", err)
|
||||
}
|
||||
}
|
||||
return p.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func scanStringSliceValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
slice, err := decodeStringSlice(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(slice))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeStringSlice(src any) ([]string, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice := make([]string, 0)
|
||||
|
||||
p := newArrayParser(b)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
slice = append(slice, string(elem))
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func scanIntSliceValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
slice, err := decodeIntSlice(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(slice))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeIntSlice(src any) ([]int, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice := make([]int, 0)
|
||||
|
||||
p := newArrayParser(b)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
|
||||
if elem == nil {
|
||||
slice = append(slice, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(internal.String(elem))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice = append(slice, n)
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func scanInt64SliceValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
slice, err := decodeInt64Slice(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(slice))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeInt64Slice(src any) ([]int64, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice := make([]int64, 0)
|
||||
|
||||
p := newArrayParser(b)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
|
||||
if elem == nil {
|
||||
slice = append(slice, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.ParseInt(internal.String(elem), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice = append(slice, n)
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func scanFloat64SliceValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
slice, err := scanFloat64Slice(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(slice))
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanFloat64Slice(src any) ([]float64, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice := make([]float64, 0)
|
||||
|
||||
p := newArrayParser(b)
|
||||
for p.Next() {
|
||||
elem := p.Elem()
|
||||
|
||||
if elem == nil {
|
||||
slice = append(slice, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.ParseFloat(internal.String(elem), 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slice = append(slice, n)
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func toBytes(src any) ([]byte, error) {
|
||||
switch src := src.(type) {
|
||||
case string:
|
||||
return internal.Bytes(src), nil
|
||||
case []byte:
|
||||
return src, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("pgdialect: got %T, wanted []byte or string", src)
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type arrayParser struct {
|
||||
p pgparser
|
||||
|
||||
elem []byte
|
||||
err error
|
||||
|
||||
isJson bool
|
||||
}
|
||||
|
||||
func newArrayParser(b []byte) *arrayParser {
|
||||
p := new(arrayParser)
|
||||
|
||||
if b[0] == 'n' {
|
||||
p.p.Reset(nil)
|
||||
return p
|
||||
}
|
||||
|
||||
if len(b) < 2 || (b[0] != '{' && b[0] != '[') || (b[len(b)-1] != '}' && b[len(b)-1] != ']') {
|
||||
p.err = fmt.Errorf("pgdialect: can't parse array: %q", b)
|
||||
return p
|
||||
}
|
||||
p.isJson = b[0] == '['
|
||||
|
||||
p.p.Reset(b[1 : len(b)-1])
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *arrayParser) Next() bool {
|
||||
if p.err != nil {
|
||||
return false
|
||||
}
|
||||
p.err = p.readNext()
|
||||
return p.err == nil
|
||||
}
|
||||
|
||||
func (p *arrayParser) Err() error {
|
||||
if p.err != io.EOF {
|
||||
return p.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *arrayParser) Elem() []byte {
|
||||
return p.elem
|
||||
}
|
||||
|
||||
func (p *arrayParser) readNext() error {
|
||||
ch := p.p.Read()
|
||||
if ch == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '}', ']':
|
||||
return io.EOF
|
||||
case '"':
|
||||
b, err := p.p.ReadSubstring(ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.p.Peek() == ',' {
|
||||
p.p.Advance()
|
||||
}
|
||||
|
||||
p.elem = b
|
||||
return nil
|
||||
case '[', '(':
|
||||
rng, err := p.p.ReadRange(ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.p.Peek() == ',' {
|
||||
p.p.Advance()
|
||||
}
|
||||
|
||||
p.elem = rng
|
||||
return nil
|
||||
default:
|
||||
if ch == '{' && p.isJson {
|
||||
json, err := p.p.ReadJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
if p.p.Peek() == ',' || p.p.Peek() == ' ' {
|
||||
p.p.Advance()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
p.elem = json
|
||||
return nil
|
||||
} else {
|
||||
lit := p.p.ReadLiteral(ch)
|
||||
if bytes.Equal(lit, []byte("NULL")) {
|
||||
lit = nil
|
||||
}
|
||||
|
||||
if p.p.Peek() == ',' {
|
||||
p.p.Advance()
|
||||
}
|
||||
|
||||
p.elem = lit
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
@@ -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
@@ -0,0 +1,87 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
||||
func appendElem(buf []byte, val any) []byte {
|
||||
switch val := val.(type) {
|
||||
case int64:
|
||||
return strconv.AppendInt(buf, val, 10)
|
||||
case float64:
|
||||
return arrayAppendFloat64(buf, val)
|
||||
case bool:
|
||||
return dialect.AppendBool(buf, val)
|
||||
case []byte:
|
||||
return appendBytesElem(buf, val)
|
||||
case string:
|
||||
return appendStringElem(buf, val)
|
||||
case time.Time:
|
||||
buf = append(buf, '"')
|
||||
buf = appendTime(buf, val)
|
||||
buf = append(buf, '"')
|
||||
return buf
|
||||
case driver.Valuer:
|
||||
val2, err := val.Value()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("pgdialect: can't append elem value: %w", err)
|
||||
return dialect.AppendError(buf, err)
|
||||
}
|
||||
return appendElem(buf, val2)
|
||||
default:
|
||||
err := fmt.Errorf("pgdialect: can't append elem %T", val)
|
||||
return dialect.AppendError(buf, err)
|
||||
}
|
||||
}
|
||||
|
||||
func appendBytesElem(b []byte, bs []byte) []byte {
|
||||
if bs == nil {
|
||||
return dialect.AppendNull(b)
|
||||
}
|
||||
|
||||
b = append(b, `"\\x`...)
|
||||
|
||||
s := len(b)
|
||||
b = append(b, make([]byte, hex.EncodedLen(len(bs)))...)
|
||||
hex.Encode(b[s:], bs)
|
||||
|
||||
b = append(b, '"')
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func appendStringElem(b []byte, s string) []byte {
|
||||
b = append(b, '"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case 0:
|
||||
// ignore
|
||||
case '\'':
|
||||
b = append(b, "''"...)
|
||||
case '"':
|
||||
b = append(b, '\\', '"')
|
||||
case '\\':
|
||||
b = append(b, '\\', '\\')
|
||||
default:
|
||||
if r < utf8.RuneSelf {
|
||||
b = append(b, byte(r))
|
||||
break
|
||||
}
|
||||
l := len(b)
|
||||
if cap(b)-l < utf8.UTFMax {
|
||||
b = append(b, make([]byte, utf8.UTFMax)...)
|
||||
}
|
||||
n := utf8.EncodeRune(b[l:l+utf8.UTFMax], r)
|
||||
b = b[:l+n]
|
||||
}
|
||||
}
|
||||
b = append(b, '"')
|
||||
return b
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
type HStoreValue struct {
|
||||
v reflect.Value
|
||||
|
||||
append schema.AppenderFunc
|
||||
scan schema.ScannerFunc
|
||||
}
|
||||
|
||||
// HStore accepts a map[string]string and returns a wrapper for working with PostgreSQL
|
||||
// hstore data type.
|
||||
//
|
||||
// For struct fields you can use hstore tag:
|
||||
//
|
||||
// Attrs map[string]string `bun:",hstore"`
|
||||
func HStore(vi any) *HStoreValue {
|
||||
v := reflect.ValueOf(vi)
|
||||
if !v.IsValid() {
|
||||
panic(fmt.Errorf("bun: HStore(nil)"))
|
||||
}
|
||||
|
||||
typ := v.Type()
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
if typ.Kind() != reflect.Map {
|
||||
panic(fmt.Errorf("bun: Hstore(unsupported %s)", typ))
|
||||
}
|
||||
|
||||
return &HStoreValue{
|
||||
v: v,
|
||||
|
||||
append: pgDialect.hstoreAppender(v.Type()),
|
||||
scan: hstoreScanner(v.Type()),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ schema.QueryAppender = (*HStoreValue)(nil)
|
||||
_ sql.Scanner = (*HStoreValue)(nil)
|
||||
)
|
||||
|
||||
func (h *HStoreValue) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error) {
|
||||
if h.append == nil {
|
||||
panic(fmt.Errorf("bun: HStore(unsupported %s)", h.v.Type()))
|
||||
}
|
||||
return h.append(gen, b, h.v), nil
|
||||
}
|
||||
|
||||
func (h *HStoreValue) Scan(src any) error {
|
||||
if h.scan == nil {
|
||||
return fmt.Errorf("bun: HStore(unsupported %s)", h.v.Type())
|
||||
}
|
||||
if h.v.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("bun: HStore(non-pointer %s)", h.v.Type())
|
||||
}
|
||||
return h.scan(h.v.Elem(), src)
|
||||
}
|
||||
|
||||
func (h *HStoreValue) Value() any {
|
||||
if h.v.IsValid() {
|
||||
return h.v.Interface()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type hstoreParser struct {
|
||||
p pgparser
|
||||
|
||||
key string
|
||||
value string
|
||||
err error
|
||||
}
|
||||
|
||||
func newHStoreParser(b []byte) *hstoreParser {
|
||||
p := new(hstoreParser)
|
||||
if len(b) != 0 && (len(b) < 6 || b[0] != '"') {
|
||||
p.err = fmt.Errorf("pgdialect: can't parse hstore: %q", b)
|
||||
return p
|
||||
}
|
||||
p.p.Reset(b)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *hstoreParser) Next() bool {
|
||||
if p.err != nil {
|
||||
return false
|
||||
}
|
||||
p.err = p.readNext()
|
||||
return p.err == nil
|
||||
}
|
||||
|
||||
func (p *hstoreParser) Err() error {
|
||||
if p.err != io.EOF {
|
||||
return p.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *hstoreParser) Key() string {
|
||||
return p.key
|
||||
}
|
||||
|
||||
func (p *hstoreParser) Value() string {
|
||||
return p.value
|
||||
}
|
||||
|
||||
func (p *hstoreParser) readNext() error {
|
||||
if !p.p.Valid() {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
if err := p.p.Skip('"'); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := p.p.ReadUnescapedSubstring('"')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.key = string(key)
|
||||
|
||||
if err := p.p.SkipPrefix([]byte("=>")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := p.p.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '"':
|
||||
value, err := p.p.ReadUnescapedSubstring(ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.skipComma()
|
||||
p.value = string(value)
|
||||
return nil
|
||||
default:
|
||||
value := p.p.ReadLiteral(ch)
|
||||
if bytes.Equal(value, []byte("NULL")) {
|
||||
p.value = ""
|
||||
}
|
||||
p.skipComma()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *hstoreParser) skipComma() {
|
||||
if p.p.Peek() == ',' {
|
||||
p.p.Advance()
|
||||
}
|
||||
if p.p.Peek() == ' ' {
|
||||
p.p.Advance()
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
func hstoreScanner(typ reflect.Type) schema.ScannerFunc {
|
||||
kind := typ.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if fn := hstoreScanner(typ.Elem()); fn != nil {
|
||||
return schema.PtrScanner(fn)
|
||||
}
|
||||
case reflect.Map:
|
||||
// ok:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if typ.Key() == stringType && typ.Elem() == stringType {
|
||||
return scanMapStringStringValue
|
||||
}
|
||||
return func(dest reflect.Value, src any) error {
|
||||
return fmt.Errorf("bun: Hstore(unsupported %s)", dest.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func scanMapStringStringValue(dest reflect.Value, src any) error {
|
||||
dest = reflect.Indirect(dest)
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type())
|
||||
}
|
||||
|
||||
m, err := decodeMapStringString(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest.Set(reflect.ValueOf(m))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeMapStringString(src any) (map[string]string, error) {
|
||||
if src == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := toBytes(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := make(map[string]string)
|
||||
|
||||
p := newHStoreParser(b)
|
||||
for p.Next() {
|
||||
m[p.Key()] = p.Value()
|
||||
}
|
||||
if err := p.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate/sqlschema"
|
||||
)
|
||||
|
||||
type (
|
||||
Schema = sqlschema.BaseDatabase
|
||||
Table = sqlschema.BaseTable
|
||||
Column = sqlschema.BaseColumn
|
||||
)
|
||||
|
||||
func (d *Dialect) NewInspector(db *bun.DB, options ...sqlschema.InspectorOption) sqlschema.Inspector {
|
||||
return newInspector(db, options...)
|
||||
}
|
||||
|
||||
type Inspector struct {
|
||||
sqlschema.InspectorConfig
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
var _ sqlschema.Inspector = (*Inspector)(nil)
|
||||
|
||||
func newInspector(db *bun.DB, options ...sqlschema.InspectorOption) *Inspector {
|
||||
i := &Inspector{db: db}
|
||||
sqlschema.ApplyInspectorOptions(&i.InspectorConfig, options...)
|
||||
return i
|
||||
}
|
||||
|
||||
func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) {
|
||||
dbSchema := Schema{
|
||||
ForeignKeys: make(map[sqlschema.ForeignKey]string),
|
||||
}
|
||||
|
||||
exclude := in.ExcludeTables
|
||||
if len(exclude) == 0 {
|
||||
// Avoid getting NOT LIKE ALL (ARRAY[NULL]) if bun.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
@@ -0,0 +1,143 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/uptrace/bun/internal/parser"
|
||||
)
|
||||
|
||||
type pgparser struct {
|
||||
parser.Parser
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func newParser(b []byte) *pgparser {
|
||||
p := new(pgparser)
|
||||
p.Reset(b)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadLiteral(ch byte) []byte {
|
||||
p.Unread()
|
||||
lit, _ := p.ReadSep(',')
|
||||
return lit
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadUnescapedSubstring(ch byte) ([]byte, error) {
|
||||
return p.readSubstring(ch, false)
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadSubstring(ch byte) ([]byte, error) {
|
||||
return p.readSubstring(ch, true)
|
||||
}
|
||||
|
||||
func (p *pgparser) readSubstring(ch byte, escaped bool) ([]byte, error) {
|
||||
ch, err := p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.buf = p.buf[:0]
|
||||
for {
|
||||
if ch == '"' {
|
||||
break
|
||||
}
|
||||
|
||||
next, err := p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ch == '\\' {
|
||||
switch next {
|
||||
case '\\', '"':
|
||||
p.buf = append(p.buf, next)
|
||||
|
||||
ch, err = p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
p.buf = append(p.buf, '\\')
|
||||
ch = next
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if escaped && ch == '\'' && next == '\'' {
|
||||
p.buf = append(p.buf, next)
|
||||
ch, err = p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
p.buf = append(p.buf, ch)
|
||||
ch = next
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(p.buf, []byte("\\x")) && len(p.buf)%2 == 0 {
|
||||
data := p.buf[2:]
|
||||
buf := make([]byte, hex.DecodedLen(len(data)))
|
||||
n, err := hex.Decode(buf, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
return p.buf, nil
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadRange(ch byte) ([]byte, error) {
|
||||
p.buf = p.buf[:0]
|
||||
p.buf = append(p.buf, ch)
|
||||
|
||||
for p.Valid() {
|
||||
ch = p.Read()
|
||||
p.buf = append(p.buf, ch)
|
||||
if ch == ']' || ch == ')' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return p.buf, nil
|
||||
}
|
||||
|
||||
func (p *pgparser) ReadJSON() ([]byte, error) {
|
||||
p.Unread()
|
||||
|
||||
c, err := p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.buf = p.buf[:0]
|
||||
|
||||
depth := 0
|
||||
for {
|
||||
switch c {
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
}
|
||||
|
||||
p.buf = append(p.buf, c)
|
||||
|
||||
if depth == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
next, err := p.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c = next
|
||||
}
|
||||
|
||||
return p.buf, nil
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun/internal"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
type Range[T any] struct {
|
||||
Lower, Upper T
|
||||
LowerBound, UpperBound RangeBound
|
||||
}
|
||||
|
||||
type MultiRange[T any] []Range[T]
|
||||
|
||||
type RangeBound byte
|
||||
|
||||
const (
|
||||
// RangeBoundUnset indicates that no bound is set.
|
||||
// This usually means the range is uninitialized or unspecified.
|
||||
RangeBoundUnset RangeBound = 0x0
|
||||
// RangeBoundEmpty is a special marker for an empty range.
|
||||
// This is NOT a valid PostgreSQL bound character, but is used internally
|
||||
// to represent a range that contains no values.
|
||||
RangeBoundEmpty RangeBound = 'E'
|
||||
|
||||
RangeBoundInclusiveLeft RangeBound = '['
|
||||
RangeBoundInclusiveRight RangeBound = ']'
|
||||
RangeBoundExclusiveLeft RangeBound = '('
|
||||
RangeBoundExclusiveRight RangeBound = ')'
|
||||
)
|
||||
|
||||
type RangeOption[T any] func(*Range[T])
|
||||
|
||||
func NewRange[T any](lower, upper T) Range[T] {
|
||||
r := Range[T]{
|
||||
Lower: lower,
|
||||
Upper: upper,
|
||||
LowerBound: RangeBoundInclusiveLeft,
|
||||
UpperBound: RangeBoundExclusiveRight,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func NewEmptyRange[T any]() Range[T] {
|
||||
return Range[T]{LowerBound: RangeBoundEmpty, UpperBound: RangeBoundEmpty}
|
||||
}
|
||||
|
||||
func (r *Range[T]) IsZero() bool {
|
||||
// NOTE: r.LowerBound represent
|
||||
return r == nil || r.LowerBound == 0
|
||||
}
|
||||
|
||||
func (r Range[T]) IsEmpty() bool {
|
||||
return r.LowerBound == RangeBoundEmpty
|
||||
}
|
||||
|
||||
var _ sql.Scanner = (*Range[any])(nil)
|
||||
|
||||
func (r *Range[T]) Scan(raw any) (err error) {
|
||||
var src []byte
|
||||
switch v := raw.(type) {
|
||||
case []byte:
|
||||
src = v
|
||||
case string:
|
||||
src = []byte(v)
|
||||
case nil:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("pgdialect: Range can't scan %T", raw)
|
||||
}
|
||||
|
||||
src = bytes.TrimSpace(src)
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if string(src) == "empty" {
|
||||
r.LowerBound, r.UpperBound = RangeBoundEmpty, RangeBoundEmpty
|
||||
return nil
|
||||
}
|
||||
|
||||
switch src[0] {
|
||||
case byte(RangeBoundInclusiveLeft), byte(RangeBoundExclusiveLeft):
|
||||
r.LowerBound = RangeBound(src[0])
|
||||
default:
|
||||
return fmt.Errorf("unexpected lower bound: %s", string(src[:1]))
|
||||
}
|
||||
switch src[len(src)-1] {
|
||||
case byte(RangeBoundInclusiveRight), byte(RangeBoundExclusiveRight):
|
||||
r.UpperBound = RangeBound(src[len(src)-1])
|
||||
default:
|
||||
return fmt.Errorf("unexpected upper bound: %s", string(src[len(src)-1:]))
|
||||
}
|
||||
|
||||
src = src[1 : len(src)-1]
|
||||
|
||||
ind := bytes.IndexByte(src, ',')
|
||||
if ind == -1 {
|
||||
return fmt.Errorf("invalid range: wanted comma, got %s", string(src))
|
||||
}
|
||||
left, right := src[:ind], src[ind+1:]
|
||||
|
||||
if len(left) > 0 {
|
||||
_, err := scanElem(&r.Lower, left)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r.LowerBound = RangeBoundUnset
|
||||
}
|
||||
|
||||
if len(right) > 0 {
|
||||
_, err = scanElem(&r.Upper, right)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r.UpperBound = RangeBoundUnset
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ schema.QueryAppender = (*Range[any])(nil)
|
||||
|
||||
func (r Range[T]) AppendQuery(_ schema.QueryGen, buf []byte) ([]byte, error) {
|
||||
buf = append(buf, '\'')
|
||||
buf = appendRange(buf, r)
|
||||
buf = append(buf, '\'')
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func appendRange[T any](buf []byte, r Range[T]) []byte {
|
||||
if r.IsEmpty() {
|
||||
buf = append(buf, []byte("empty")...)
|
||||
return buf
|
||||
}
|
||||
|
||||
if r.LowerBound == RangeBoundUnset {
|
||||
// NOTE from pg's document:
|
||||
// > Specifying a missing bound as inclusive is automatically converted to exclusive, e.g., [,] is converted to (,).
|
||||
buf = append(buf, byte(RangeBoundExclusiveLeft))
|
||||
} else {
|
||||
buf = append(buf, byte(r.LowerBound))
|
||||
buf = appendElem(buf, r.Lower)
|
||||
}
|
||||
buf = append(buf, ',')
|
||||
if r.UpperBound == RangeBoundUnset {
|
||||
buf = append(buf, byte(RangeBoundExclusiveRight))
|
||||
} else {
|
||||
buf = appendElem(buf, r.Upper)
|
||||
buf = append(buf, byte(r.UpperBound))
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (m *MultiRange[T]) Len() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return len(([]Range[T])(*m))
|
||||
}
|
||||
|
||||
func (m *MultiRange[T]) IsZero() bool {
|
||||
return m.Len() == 0
|
||||
}
|
||||
|
||||
func (m MultiRange[T]) AppendQuery(_ schema.QueryGen, buf []byte) ([]byte, error) {
|
||||
if m == nil {
|
||||
return append(buf, []byte("'{}'")...), nil
|
||||
}
|
||||
rs := ([]Range[T])(m)
|
||||
buf = append(buf, '\'', '{')
|
||||
for _, r := range rs {
|
||||
buf = appendRange(buf, r)
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
if len(rs) > 0 {
|
||||
buf[len(buf)-1] = '}'
|
||||
} else {
|
||||
buf = append(buf, '}')
|
||||
}
|
||||
buf = append(buf, '\'')
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func scanElem(ptr any, src []byte) ([]byte, error) {
|
||||
// NOTE: for daterange, pg return 2024-12-01, for tzrange, pg return "2024-12-01 12:00:00"
|
||||
if len(src) >= 2 && src[0] == '"' {
|
||||
src = src[1 : len(src)-1]
|
||||
}
|
||||
|
||||
switch ptr := ptr.(type) {
|
||||
case *time.Time:
|
||||
tm, err := internal.ParseTime(internal.String(src))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*ptr = tm
|
||||
|
||||
return src, nil
|
||||
|
||||
case sql.Scanner:
|
||||
if err := ptr.Scan(src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return src, nil
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported range type: %T", ptr))
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package pgdialect
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun/dialect/sqltype"
|
||||
"github.com/uptrace/bun/migrate/sqlschema"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
// Date / Time
|
||||
pgTypeTimestamp = "TIMESTAMP" // Timestamp
|
||||
pgTypeTimestampWithTz = "TIMESTAMP WITH TIME ZONE" // Timestamp with a time zone
|
||||
pgTypeTimestampTz = "TIMESTAMPTZ" // Timestamp with a time zone (alias)
|
||||
pgTypeDate = "DATE" // Date
|
||||
pgTypeTime = "TIME" // Time without a time zone
|
||||
pgTypeTimeTz = "TIME WITH TIME ZONE" // Time with a time zone
|
||||
pgTypeInterval = "INTERVAL" // Time interval
|
||||
|
||||
// Network Addresses
|
||||
pgTypeInet = "INET" // IPv4 or IPv6 hosts and networks
|
||||
pgTypeCidr = "CIDR" // IPv4 or IPv6 networks
|
||||
pgTypeMacaddr = "MACADDR" // MAC addresses
|
||||
|
||||
// Serial Types
|
||||
pgTypeSmallSerial = "SMALLSERIAL" // 2 byte autoincrementing integer
|
||||
pgTypeSerial = "SERIAL" // 4 byte autoincrementing integer
|
||||
pgTypeBigSerial = "BIGSERIAL" // 8 byte autoincrementing integer
|
||||
|
||||
// Character Types
|
||||
pgTypeChar = "CHAR" // fixed length string (blank padded)
|
||||
pgTypeCharacter = "CHARACTER" // alias for CHAR
|
||||
pgTypeText = "TEXT" // variable length string without limit
|
||||
pgTypeVarchar = "VARCHAR" // variable length string with optional limit
|
||||
pgTypeCharacterVarying = "CHARACTER VARYING" // alias for VARCHAR
|
||||
|
||||
// Binary Data Types
|
||||
pgTypeBytea = "BYTEA" // binary string
|
||||
)
|
||||
|
||||
var (
|
||||
ipType = reflect.TypeFor[net.IP]()
|
||||
ipNetType = reflect.TypeFor[net.IPNet]()
|
||||
jsonRawMessageType = reflect.TypeFor[json.RawMessage]()
|
||||
nullStringType = reflect.TypeFor[sql.NullString]()
|
||||
)
|
||||
|
||||
func (d *Dialect) DefaultVarcharLen() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d *Dialect) DefaultSchema() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
func fieldSQLType(field *schema.Field) string {
|
||||
if field.UserSQLType != "" {
|
||||
return field.UserSQLType
|
||||
}
|
||||
|
||||
if v, ok := field.Tag.Option("composite"); ok {
|
||||
return v
|
||||
}
|
||||
if field.Tag.HasOption("hstore") {
|
||||
return sqltype.HSTORE
|
||||
}
|
||||
|
||||
if field.Tag.HasOption("array") {
|
||||
switch field.IndirectType.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
sqlType := sqlType(field.IndirectType.Elem())
|
||||
return sqlType + "[]"
|
||||
}
|
||||
}
|
||||
|
||||
if field.DiscoveredSQLType == sqltype.Blob {
|
||||
return pgTypeBytea
|
||||
}
|
||||
|
||||
return sqlType(field.IndirectType)
|
||||
}
|
||||
|
||||
func sqlType(typ reflect.Type) string {
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case nullStringType: // typ.Kind() == reflect.Struct, test for exact match
|
||||
return sqltype.VarChar
|
||||
case ipType:
|
||||
return pgTypeInet
|
||||
case ipNetType:
|
||||
return pgTypeCidr
|
||||
case jsonRawMessageType:
|
||||
return sqltype.JSONB
|
||||
}
|
||||
|
||||
sqlType := schema.DiscoverSQLType(typ)
|
||||
switch sqlType {
|
||||
case sqltype.Timestamp:
|
||||
sqlType = pgTypeTimestampTz
|
||||
}
|
||||
|
||||
switch typ.Kind() {
|
||||
case reflect.Map, reflect.Struct: // except typ == nullStringType, see above
|
||||
if sqlType == sqltype.VarChar {
|
||||
return sqltype.JSONB
|
||||
}
|
||||
return sqlType
|
||||
case reflect.Array, reflect.Slice:
|
||||
if typ.Elem().Kind() == reflect.Uint8 {
|
||||
return pgTypeBytea
|
||||
}
|
||||
return sqltype.JSONB
|
||||
}
|
||||
|
||||
return sqlType
|
||||
}
|
||||
|
||||
var (
|
||||
char = newAliases(pgTypeChar, pgTypeCharacter)
|
||||
varchar = newAliases(pgTypeVarchar, pgTypeCharacterVarying)
|
||||
timestampTz = newAliases(sqltype.Timestamp, pgTypeTimestampTz, pgTypeTimestampWithTz)
|
||||
bigint = newAliases(sqltype.BigInt, pgTypeBigSerial)
|
||||
integer = newAliases(sqltype.Integer, pgTypeSerial)
|
||||
smallint = newAliases(sqltype.SmallInt, pgTypeSmallSerial)
|
||||
)
|
||||
|
||||
func (d *Dialect) CompareType(col1, col2 sqlschema.Column) bool {
|
||||
typ1, typ2 := strings.ToUpper(col1.GetSQLType()), strings.ToUpper(col2.GetSQLType())
|
||||
|
||||
if typ1 == typ2 {
|
||||
return checkVarcharLen(col1, col2, d.DefaultVarcharLen())
|
||||
}
|
||||
|
||||
switch {
|
||||
case char.IsAlias(typ1) && char.IsAlias(typ2):
|
||||
return checkVarcharLen(col1, col2, d.DefaultVarcharLen())
|
||||
case varchar.IsAlias(typ1) && varchar.IsAlias(typ2):
|
||||
return checkVarcharLen(col1, col2, d.DefaultVarcharLen())
|
||||
case timestampTz.IsAlias(typ1) && timestampTz.IsAlias(typ2):
|
||||
return true
|
||||
case bigint.IsAlias(typ1) && bigint.IsAlias(typ2),
|
||||
integer.IsAlias(typ1) && integer.IsAlias(typ2),
|
||||
smallint.IsAlias(typ1) && smallint.IsAlias(typ2):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checkVarcharLen returns true if columns have the same VarcharLen, or,
|
||||
// if one specifies no VarcharLen and the other one has the default lenght for pgdialect.
|
||||
// We assume that the types are otherwise equivalent and that any non-character column
|
||||
// would have VarcharLen == 0;
|
||||
func checkVarcharLen(col1, col2 sqlschema.Column, defaultLen int) bool {
|
||||
vl1, vl2 := col1.GetVarcharLen(), col2.GetVarcharLen()
|
||||
|
||||
if vl1 == vl2 {
|
||||
return true
|
||||
}
|
||||
|
||||
if (vl1 == 0 && vl2 == defaultLen) || (vl1 == defaultLen && vl2 == 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// typeAlias defines aliases for common data types. It is a lightweight string set implementation.
|
||||
type typeAlias map[string]struct{}
|
||||
|
||||
// IsAlias checks if typ1 and typ2 are aliases of the same data type.
|
||||
func (t typeAlias) IsAlias(typ string) bool {
|
||||
_, ok := t[typ]
|
||||
return ok
|
||||
}
|
||||
|
||||
// newAliases creates a set of aliases.
|
||||
func newAliases(aliases ...string) typeAlias {
|
||||
types := make(typeAlias)
|
||||
for _, a := range aliases {
|
||||
types[a] = struct{}{}
|
||||
}
|
||||
return types
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package pgdialect
|
||||
|
||||
// Version is the current release version.
|
||||
func Version() string {
|
||||
return "1.2.16"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2021 Vladimir Mihailenco. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+149
@@ -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
@@ -0,0 +1,6 @@
|
||||
package sqlitedialect
|
||||
|
||||
// Version is the current release version.
|
||||
func Version() string {
|
||||
return "1.2.16"
|
||||
}
|
||||
+16
@@ -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
@@ -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
@@ -0,0 +1,38 @@
|
||||
# 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+1100
File diff suppressed because it is too large
Load Diff
+11
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+152
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+429
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user