parent
ea7163af0e
commit
6c8a603646
@ -0,0 +1,16 @@
|
||||
package dorm
|
||||
|
||||
import (
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
type ConfigRelClient struct {
|
||||
Dns string // 地址
|
||||
}
|
||||
|
||||
// RelClient
|
||||
// https://go-rel.github.io/
|
||||
type RelClient struct {
|
||||
Db *rel.Repository // 驱动
|
||||
config *ConfigRelClient // 配置
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package dorm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-rel/mysql"
|
||||
"github.com/go-rel/rel"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func NewRelMysqlClient(config *ConfigRelClient) (*RelClient, error) {
|
||||
|
||||
var err error
|
||||
c := &RelClient{config: config}
|
||||
|
||||
adapter, err := mysql.Open(c.config.Dns)
|
||||
defer adapter.Close()
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("连接失败:%v", err))
|
||||
}
|
||||
|
||||
repo := rel.New(adapter)
|
||||
|
||||
c.Db = &repo
|
||||
|
||||
return c, nil
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "go"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
import_root = "github.com/go-rel/mysql"
|
||||
|
||||
[[transformers]]
|
||||
name = "gofmt"
|
||||
enabled = true
|
@ -0,0 +1,8 @@
|
||||
vendor
|
||||
.tool-versions
|
||||
*.db
|
||||
.vscode/
|
||||
debug.test
|
||||
.idea/
|
||||
*.out
|
||||
*.test
|
@ -0,0 +1,8 @@
|
||||
builds:
|
||||
- skip: true
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 REL
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,89 @@
|
||||
# mysql
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/go-rel/mysql?status.svg)](https://pkg.go.dev/github.com/go-rel/mysql)
|
||||
[![Tesst](https://github.com/go-rel/mysql/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/go-rel/mysql/actions/workflows/test.yml)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-rel/mysql)](https://goreportcard.com/report/github.com/go-rel/mysql)
|
||||
[![codecov](https://codecov.io/gh/go-rel/mysql/branch/main/graph/badge.svg?token=56qOCsVPJF)](https://codecov.io/gh/go-rel/mysql)
|
||||
[![Gitter chat](https://badges.gitter.im/go-rel/rel.png)](https://gitter.im/go-rel/rel)
|
||||
|
||||
MySQL adapter for REL.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/go-rel/mysql"
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// open mysql connection.
|
||||
// note: `clientFoundRows=true` is required for update and delete to works correctly.
|
||||
adapter, err := mysql.Open("root@(127.0.0.1:3306)/rel_test?clientFoundRows=true&charset=utf8&parseTime=True&loc=Local")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer adapter.Close()
|
||||
|
||||
// initialize REL's repo.
|
||||
repo := rel.New(adapter)
|
||||
repo.Ping(context.TODO())
|
||||
}
|
||||
```
|
||||
|
||||
## Example Replication (Source/Replica)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/go-rel/primaryreplica"
|
||||
"github.com/go-rel/mysql"
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// open mysql connection.
|
||||
// note: `clientFoundRows=true` is required for update and delete to works correctly.
|
||||
adapter := primaryreplica.New(
|
||||
mysql.MustOpen("root@(source:23306)/rel_test?charset=utf8&parseTime=True&loc=Local"),
|
||||
mysql.MustOpen("root@(replica:23307)/rel_test?charset=utf8&parseTime=True&loc=Local"),
|
||||
)
|
||||
defer adapter.Close()
|
||||
|
||||
// initialize REL's repo.
|
||||
repo := rel.New(adapter)
|
||||
repo.Ping(context.TODO())
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Driver
|
||||
|
||||
- github.com/go-sql-driver/mysql
|
||||
|
||||
## Supported Database
|
||||
|
||||
- MySQL 5 and 8
|
||||
- MariaDB 10
|
||||
|
||||
## Testing
|
||||
|
||||
### Start MariaDB server in Docker
|
||||
|
||||
```console
|
||||
docker run -it --rm -p 3307:3306 -e "MARIADB_ROOT_PASSWORD=test" -e "MARIADB_DATABASE=rel_test" mariadb:10
|
||||
```
|
||||
|
||||
### Run tests
|
||||
|
||||
```console
|
||||
MYSQL_DATABASE="root:test@tcp(localhost:3307)/rel_test" go test ./...
|
||||
```
|
@ -0,0 +1,42 @@
|
||||
version: '2.1'
|
||||
|
||||
services:
|
||||
mariadb-master:
|
||||
image: docker.io/bitnami/mariadb:10.6
|
||||
ports:
|
||||
- '23306:3306'
|
||||
environment:
|
||||
- MARIADB_REPLICATION_MODE=master
|
||||
- MARIADB_REPLICATION_USER=repl_user
|
||||
- MARIADB_USER=rel
|
||||
- MARIADB_PASSWORD=rel
|
||||
- MARIADB_DATABASE=rel_test
|
||||
- MARIADB_ROOT_PASSWORD=rel
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
healthcheck:
|
||||
test: ['CMD', '/opt/bitnami/scripts/mariadb/healthcheck.sh']
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
|
||||
mariadb-slave:
|
||||
image: docker.io/bitnami/mariadb:10.6
|
||||
ports:
|
||||
- '23307:3306'
|
||||
depends_on:
|
||||
- mariadb-master
|
||||
environment:
|
||||
- MARIADB_REPLICATION_MODE=slave
|
||||
- MARIADB_REPLICATION_USER=repl_user
|
||||
- MARIADB_USER=rel
|
||||
- MARIADB_PASSWORD=rel
|
||||
- MARIADB_DATABASE=rel_test
|
||||
- MARIADB_MASTER_HOST=mariadb-master
|
||||
- MARIADB_MASTER_PORT_NUMBER=3306
|
||||
- MARIADB_MASTER_ROOT_PASSWORD=rel
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
healthcheck:
|
||||
test: ['CMD', '/opt/bitnami/scripts/mariadb/healthcheck.sh']
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 6
|
@ -0,0 +1,144 @@
|
||||
// Package mysql wraps mysql driver as an adapter for REL.
|
||||
//
|
||||
// Usage:
|
||||
// // open mysql connection.
|
||||
// // note: `clientFoundRows=true` is required for update and delete to works correctly.
|
||||
// adapter, err := mysql.Open("root@(127.0.0.1:3306)/rel_test?clientFoundRows=true&charset=utf8&parseTime=True&loc=Local")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// defer adapter.Close()
|
||||
//
|
||||
// // initialize REL's repo.
|
||||
// repo := rel.New(adapter)
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
db "database/sql"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rel/rel"
|
||||
"github.com/go-rel/sql"
|
||||
"github.com/go-rel/sql/builder"
|
||||
)
|
||||
|
||||
// New mysql adapter using existing connection.
|
||||
// Existing connection needs to be created with `clientFoundRows=true` options for update and delete to works correctly.
|
||||
func New(database *db.DB) rel.Adapter {
|
||||
var (
|
||||
bufferFactory = builder.BufferFactory{ArgumentPlaceholder: "?", BoolTrueValue: "true", BoolFalseValue: "false", Quoter: Quote{}, ValueConverter: ValueConvert{}}
|
||||
filterBuilder = builder.Filter{}
|
||||
queryBuilder = builder.Query{BufferFactory: bufferFactory, Filter: filterBuilder}
|
||||
onConflictBuilder = builder.OnConflict{Statement: "ON DUPLICATE KEY", UpdateStatement: "UPDATE", UseValues: true}
|
||||
InsertBuilder = builder.Insert{BufferFactory: bufferFactory, InsertDefaultValues: true, OnConflict: onConflictBuilder}
|
||||
insertAllBuilder = builder.InsertAll{BufferFactory: bufferFactory, OnConflict: onConflictBuilder}
|
||||
updateBuilder = builder.Update{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder}
|
||||
deleteBuilder = builder.Delete{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder}
|
||||
ddlBufferFactory = builder.BufferFactory{InlineValues: true, BoolTrueValue: "true", BoolFalseValue: "false", Quoter: Quote{}, ValueConverter: ValueConvert{}}
|
||||
ddlQueryBuilder = builder.Query{BufferFactory: ddlBufferFactory, Filter: filterBuilder}
|
||||
tableBuilder = builder.Table{BufferFactory: ddlBufferFactory, ColumnMapper: columnMapper}
|
||||
indexBuilder = builder.Index{BufferFactory: ddlBufferFactory, Query: ddlQueryBuilder, Filter: filterBuilder, DropIndexOnTable: true}
|
||||
)
|
||||
|
||||
return &sql.SQL{
|
||||
QueryBuilder: queryBuilder,
|
||||
InsertBuilder: InsertBuilder,
|
||||
InsertAllBuilder: insertAllBuilder,
|
||||
UpdateBuilder: updateBuilder,
|
||||
DeleteBuilder: deleteBuilder,
|
||||
TableBuilder: tableBuilder,
|
||||
IndexBuilder: indexBuilder,
|
||||
IncrementFunc: incrementFunc,
|
||||
ErrorMapper: errorMapper,
|
||||
DB: database,
|
||||
}
|
||||
}
|
||||
|
||||
// Open mysql connection using dsn.
|
||||
func Open(dsn string) (rel.Adapter, error) {
|
||||
// force clientFoundRows=true
|
||||
// this allows not found record check when updating a record.
|
||||
if strings.ContainsRune(dsn, '?') {
|
||||
dsn += "&clientFoundRows=true"
|
||||
} else {
|
||||
dsn += "?clientFoundRows=true"
|
||||
}
|
||||
|
||||
var database, err = db.Open("mysql", dsn)
|
||||
return New(database), err
|
||||
}
|
||||
|
||||
// MustOpen mysql connection using dsn.
|
||||
func MustOpen(dsn string) rel.Adapter {
|
||||
var (
|
||||
adapter, err = Open(dsn)
|
||||
)
|
||||
|
||||
check(err)
|
||||
return adapter
|
||||
}
|
||||
|
||||
func incrementFunc(adapter sql.SQL) int {
|
||||
var (
|
||||
variable string
|
||||
increment int
|
||||
rows, err = adapter.DoQuery(context.TODO(), "SHOW VARIABLES LIKE 'auto_increment_increment';", nil)
|
||||
)
|
||||
|
||||
check(err)
|
||||
|
||||
defer rows.Close()
|
||||
rows.Next()
|
||||
check(rows.Scan(&variable, &increment))
|
||||
|
||||
return increment
|
||||
}
|
||||
|
||||
func errorMapper(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
msg = err.Error()
|
||||
errCodeSep = ':'
|
||||
errCodeIndex = strings.IndexRune(msg, errCodeSep)
|
||||
)
|
||||
|
||||
if errCodeIndex < 0 {
|
||||
errCodeIndex = 0
|
||||
}
|
||||
|
||||
switch msg[:errCodeIndex] {
|
||||
case "Error 1062":
|
||||
return rel.ConstraintError{
|
||||
Key: sql.ExtractString(msg, "key '", "'"),
|
||||
Type: rel.UniqueConstraint,
|
||||
Err: err,
|
||||
}
|
||||
case "Error 1452":
|
||||
return rel.ConstraintError{
|
||||
Key: sql.ExtractString(msg, "CONSTRAINT `", "`"),
|
||||
Type: rel.ForeignKeyConstraint,
|
||||
Err: err,
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func columnMapper(column *rel.Column) (string, int, int) {
|
||||
switch column.Type {
|
||||
case rel.JSON:
|
||||
return "JSON", 0, 0
|
||||
default:
|
||||
return sql.ColumnMapper(column)
|
||||
}
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Quote MySQL identifiers and literals.
|
||||
type Quote struct{}
|
||||
|
||||
func (q Quote) ID(name string) string {
|
||||
end := strings.IndexRune(name, 0)
|
||||
if end > -1 {
|
||||
name = name[:end]
|
||||
}
|
||||
return "`" + strings.ReplaceAll(name, "`", "``") + "`"
|
||||
}
|
||||
|
||||
func (q Quote) Value(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
default:
|
||||
panic("unsupported value")
|
||||
case string:
|
||||
// TODO: Need to check on connection for NO_BACKSLASH_ESCAPES
|
||||
rv := []rune(v)
|
||||
buf := make([]rune, len(rv)*2)
|
||||
pos := 0
|
||||
for i := 0; i < len(rv); i++ {
|
||||
c := rv[i]
|
||||
switch c {
|
||||
case '\x00':
|
||||
buf[pos] = '\\'
|
||||
buf[pos+1] = '0'
|
||||
pos += 2
|
||||
case '\n':
|
||||
buf[pos] = '\\'
|
||||
buf[pos+1] = 'n'
|
||||
pos += 2
|
||||
case '\r':
|
||||
buf[pos] = '\\'
|
||||
buf[pos+1] = 'r'
|
||||
pos += 2
|
||||
case '\x1a':
|
||||
buf[pos] = '\\'
|
||||
buf[pos+1] = 'Z'
|
||||
pos += 2
|
||||
case '\'':
|
||||
buf[pos] = '\\'
|
||||
buf[pos+1] = '\''
|
||||
pos += 2
|
||||
case '"':
|
||||
buf[pos] = '\\'
|
||||
buf[pos+1] = '"'
|
||||
pos += 2
|
||||
case '\\':
|
||||
buf[pos] = '\\'
|
||||
buf[pos+1] = '\\'
|
||||
pos += 2
|
||||
default:
|
||||
buf[pos] = c
|
||||
pos++
|
||||
}
|
||||
}
|
||||
|
||||
return "'" + string(buf[:pos]) + "'"
|
||||
}
|
||||
}
|
||||
|
||||
// ValueConvert converts values to MySQL literals.
|
||||
type ValueConvert struct{}
|
||||
|
||||
func (c ValueConvert) ConvertValue(v interface{}) (driver.Value, error) {
|
||||
v, err := driver.DefaultParameterConverter.ConvertValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch v := v.(type) {
|
||||
default:
|
||||
return v, nil
|
||||
case time.Time:
|
||||
return v.Truncate(time.Microsecond).Format("2006-01-02 15:04:05.999999"), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
exclude_patterns:
|
||||
- "**/vendor/"
|
||||
- "**/*_test.go"
|
||||
- adapter/specs/
|
@ -0,0 +1,12 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "go"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
import_root = "github.com/go-rel/rel"
|
||||
|
||||
[[transformers]]
|
||||
name = "gofmt"
|
||||
enabled = true
|
@ -0,0 +1,9 @@
|
||||
vendor
|
||||
.tool-versions
|
||||
*.db
|
||||
.vscode/
|
||||
debug.test
|
||||
.idea/
|
||||
*.out
|
||||
*.test
|
||||
dist/
|
@ -0,0 +1,65 @@
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
- go generate ./...
|
||||
builds:
|
||||
- main: ./cmd/rel/main.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^chore:'
|
||||
nfpms:
|
||||
- file_name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
||||
homepage: https://go-rel.github.io
|
||||
description: Modern Database Access Layer for Golang
|
||||
maintainer: Muhammad Surya Asriadie <surya.asriadie@gmail.com>
|
||||
license: MIT
|
||||
vendor: REL
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
dependencies:
|
||||
- golang
|
||||
brews:
|
||||
- tap:
|
||||
owner: go-rel
|
||||
name: homebrew-tap
|
||||
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
|
||||
commit_author:
|
||||
name: REL
|
||||
homepage: "https://go-rel.github.io/"
|
||||
description: "Database migration using REL"
|
||||
license: "MIT"
|
||||
folder: Formula
|
||||
dependencies:
|
||||
- name: golang
|
||||
type: optional
|
@ -0,0 +1,12 @@
|
||||
# How to contribute
|
||||
|
||||
I'm really glad you're reading this, because we need volunteer developers to help this project come to fruition.
|
||||
|
||||
Here's some way you on how you can contribute to this project:
|
||||
|
||||
- Report any bug, feature request and questions using issues.
|
||||
- Contribute directly to the development, don't hestitate to take any task available on [projects](https://github.com/go-rel/rel/projects) page. You can use issues if you need further discussion and help about the implementation.
|
||||
- Improvement to the documentation is always welcomed.
|
||||
- Star and let the world know about this project.
|
||||
|
||||
Thanks :heart: :heart: :heart:
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Muhammad Surya Asriadie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,44 @@
|
||||
# REL
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/go-rel/rel?status.svg)](https://godoc.org/github.com/go-rel/rel)
|
||||
[![Build Status](https://github.com/go-rel/rel/workflows/Build/badge.svg)](https://github.com/go-rel/rel/actions)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-rel/rel)](https://goreportcard.com/report/github.com/go-rel/rel)
|
||||
[![Maintainability](https://api.codeclimate.com/v1/badges/194611cc82f02edcda6e/maintainability)](https://codeclimate.com/github/go-rel/rel/maintainability)
|
||||
[![Codecov](https://codecov.io/gh/go-rel/rel/branch/master/graph/badge.svg?token=0P505E1IWB)](https://codecov.io/gh/go-rel/rel)
|
||||
[![Gitter chat](https://badges.gitter.im/go-rel/rel.png)](https://gitter.im/go-rel/rel)
|
||||
|
||||
> Modern Database Access Layer for Golang.
|
||||
|
||||
REL is golang orm-ish database layer for layered architecture. It's testable and comes with its own test library. REL also features extendable query builder that allows you to write query using builder or plain sql.
|
||||
|
||||
## Features
|
||||
|
||||
- Testable repository with builtin reltest package.
|
||||
- Seamless nested transactions.
|
||||
- Elegant, yet extendable query builder with mix of syntactic sugar.
|
||||
- Supports Eager loading.
|
||||
- Composite Primary Key.
|
||||
- Multi adapter.
|
||||
- Soft Deletion.
|
||||
- Pagination.
|
||||
- Schema Migration.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/go-rel/rel
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Guides [https://go-rel.github.io](https://go-rel.github.io)
|
||||
|
||||
## Examples
|
||||
|
||||
- [gin-example](https://github.com/go-rel/gin-example) - Todo Backend using Gin and REL
|
||||
- [go-todo-backend](https://github.com/Fs02/go-todo-backend) - Todo Backend using Chi and REL
|
||||
- [iris-example](https://github.com/iris-contrib/go-rel-iris-example) - Todo Backend using Iris and REL
|
||||
|
||||
## License
|
||||
|
||||
Released under the [MIT License](https://github.com/go-rel/rel/blob/master/LICENSE)
|
@ -0,0 +1,26 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Adapter interface
|
||||
type Adapter interface {
|
||||
Close() error
|
||||
|
||||
Instrumentation(instrumenter Instrumenter)
|
||||
Ping(ctx context.Context) error
|
||||
Aggregate(ctx context.Context, query Query, mode string, field string) (int, error)
|
||||
Query(ctx context.Context, query Query) (Cursor, error)
|
||||
Insert(ctx context.Context, query Query, primaryField string, mutates map[string]Mutate, onConflict OnConflict) (interface{}, error)
|
||||
InsertAll(ctx context.Context, query Query, primaryField string, fields []string, bulkMutates []map[string]Mutate, onConflict OnConflict) ([]interface{}, error)
|
||||
Update(ctx context.Context, query Query, primaryField string, mutates map[string]Mutate) (int, error)
|
||||
Delete(ctx context.Context, query Query) (int, error)
|
||||
Exec(ctx context.Context, stmt string, args []interface{}) (int64, int64, error)
|
||||
|
||||
Begin(ctx context.Context) (Adapter, error)
|
||||
Commit(ctx context.Context) error
|
||||
Rollback(ctx context.Context) error
|
||||
|
||||
Apply(ctx context.Context, migration Migration) error
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Association provides abstraction to work with association of document or collection.
|
||||
type Association struct {
|
||||
meta AssociationMeta
|
||||
rv reflect.Value
|
||||
}
|
||||
|
||||
// Type of association.
|
||||
func (a Association) Type() AssociationType {
|
||||
return a.meta.Type()
|
||||
}
|
||||
|
||||
// Document returns association target as document.
|
||||
// If association is zero, second return value will be false.
|
||||
func (a Association) Document() (*Document, bool) {
|
||||
return a.document(false)
|
||||
}
|
||||
|
||||
// LazyDocument is a lazy version of Document.
|
||||
// If rv is a null pointer, it returns a document that delays setting the value of rv
|
||||
// until Document#Add() is called.
|
||||
func (a Association) LazyDocument() (*Document, bool) {
|
||||
return a.document(true)
|
||||
}
|
||||
|
||||
func (a Association) document(lazy bool) (*Document, bool) {
|
||||
var (
|
||||
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, !lazy)
|
||||
)
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Ptr:
|
||||
if rv.IsNil() {
|
||||
if !lazy {
|
||||
rv.Set(reflect.New(rv.Type().Elem()))
|
||||
}
|
||||
|
||||
return NewDocument(rv), false
|
||||
}
|
||||
|
||||
var (
|
||||
doc = NewDocument(rv)
|
||||
)
|
||||
|
||||
return doc, doc.Persisted()
|
||||
default:
|
||||
var (
|
||||
doc = NewDocument(rv.Addr())
|
||||
)
|
||||
|
||||
return doc, doc.Persisted()
|
||||
}
|
||||
}
|
||||
|
||||
// Collection returns association target as collection.
|
||||
// If association is zero, second return value will be false.
|
||||
func (a Association) Collection() (*Collection, bool) {
|
||||
var (
|
||||
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, true)
|
||||
loaded = !rv.IsNil()
|
||||
)
|
||||
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
if !loaded {
|
||||
rv.Set(reflect.New(rv.Type().Elem()))
|
||||
rv.Elem().Set(reflect.MakeSlice(rv.Elem().Type(), 0, 0))
|
||||
}
|
||||
|
||||
return NewCollection(rv), loaded
|
||||
}
|
||||
|
||||
if !loaded {
|
||||
rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
|
||||
}
|
||||
|
||||
return NewCollection(rv.Addr()), loaded
|
||||
}
|
||||
|
||||
// IsZero returns true if association is not loaded.
|
||||
func (a Association) IsZero() bool {
|
||||
var (
|
||||
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, false)
|
||||
)
|
||||
|
||||
return isDeepZero(reflect.Indirect(rv), 1)
|
||||
}
|
||||
|
||||
// ReferenceField of the association.
|
||||
func (a Association) ReferenceField() string {
|
||||
return a.meta.ReferenceField()
|
||||
}
|
||||
|
||||
// ReferenceValue of the association.
|
||||
func (a Association) ReferenceValue() interface{} {
|
||||
return indirectInterface(reflectValueFieldByIndex(a.rv, a.meta.referenceIndex, false))
|
||||
}
|
||||
|
||||
// ForeignField of the association.
|
||||
func (a Association) ForeignField() string {
|
||||
return a.meta.ForeignField()
|
||||
}
|
||||
|
||||
// ForeignValue of the association.
|
||||
// It'll panic if association type is has many.
|
||||
func (a Association) ForeignValue() interface{} {
|
||||
if a.Type() == HasMany {
|
||||
panic("rel: cannot infer foreign value for has many or many to many association")
|
||||
}
|
||||
|
||||
var (
|
||||
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, false)
|
||||
)
|
||||
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
return indirectInterface(reflectValueFieldByIndex(rv, a.meta.foreignIndex, false))
|
||||
}
|
||||
|
||||
// Through return intermediary association.
|
||||
func (a Association) Through() string {
|
||||
return a.meta.Through()
|
||||
}
|
||||
|
||||
// Autoload assoc setting when parent is loaded.
|
||||
func (a Association) Autoload() bool {
|
||||
return a.meta.Autoload()
|
||||
}
|
||||
|
||||
// Autosave setting when parent is created/updated/deleted.
|
||||
func (a Association) Autosave() bool {
|
||||
return a.meta.Autosave()
|
||||
}
|
||||
|
||||
func newAssociation(rv reflect.Value, index []int) Association {
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
return Association{
|
||||
meta: getAssociationMeta(rv.Type(), index),
|
||||
rv: rv,
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/serenize/snaker"
|
||||
)
|
||||
|
||||
var associationMetaCache sync.Map
|
||||
|
||||
type associationKey struct {
|
||||
rt reflect.Type
|
||||
// string repr of index, because []int is not hashable
|
||||
index string
|
||||
}
|
||||
|
||||
// AssociationType defines the type of association in database.
|
||||
type AssociationType uint8
|
||||
|
||||
const (
|
||||
// BelongsTo association.
|
||||
BelongsTo = iota
|
||||
// HasOne association.
|
||||
HasOne
|
||||
// HasMany association.
|
||||
HasMany
|
||||
)
|
||||
|
||||
type cachedAssociationMeta struct {
|
||||
typ AssociationType
|
||||
targetIndex []int
|
||||
referenceField string
|
||||
referenceIndex []int
|
||||
foreignField string
|
||||
foreignIndex []int
|
||||
through string
|
||||
autoload bool
|
||||
autosave bool
|
||||
}
|
||||
|
||||
type AssociationMeta struct {
|
||||
rt reflect.Type
|
||||
cachedAssociationMeta
|
||||
}
|
||||
|
||||
// Type of association.
|
||||
func (am AssociationMeta) Type() AssociationType {
|
||||
return am.typ
|
||||
}
|
||||
|
||||
// ReferenceField of the association.
|
||||
func (am AssociationMeta) ReferenceField() string {
|
||||
return am.referenceField
|
||||
}
|
||||
|
||||
// ForeignField of the association.
|
||||
func (am AssociationMeta) ForeignField() string {
|
||||
return am.foreignField
|
||||
}
|
||||
|
||||
// Through return intermediary association.
|
||||
func (am AssociationMeta) Through() string {
|
||||
return am.through
|
||||
}
|
||||
|
||||
// Autoload assoc setting when parent is loaded.
|
||||
func (am AssociationMeta) Autoload() bool {
|
||||
return am.autoload
|
||||
}
|
||||
|
||||
// Autosave setting when parent is created/updated/deleted.
|
||||
func (am AssociationMeta) Autosave() bool {
|
||||
return am.autosave
|
||||
}
|
||||
|
||||
// Document returns association target document meta.
|
||||
func (am AssociationMeta) DocumentMeta() DocumentMeta {
|
||||
var (
|
||||
rt = am.rt.FieldByIndex(am.targetIndex).Type
|
||||
)
|
||||
|
||||
if rt.Kind() == reflect.Ptr {
|
||||
rt = rt.Elem()
|
||||
}
|
||||
|
||||
if rt.Kind() == reflect.Slice {
|
||||
rt = rt.Elem()
|
||||
}
|
||||
|
||||
return getDocumentMeta(rt, false)
|
||||
}
|
||||
|
||||
func getAssociationMeta(rt reflect.Type, index []int) AssociationMeta {
|
||||
var (
|
||||
key = associationKey{
|
||||
rt: rt,
|
||||
index: encodeIndices(index),
|
||||
}
|
||||
)
|
||||
|
||||
if val, cached := associationMetaCache.Load(key); cached {
|
||||
return AssociationMeta{
|
||||
rt: rt,
|
||||
cachedAssociationMeta: val.(cachedAssociationMeta),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
sf = rt.FieldByIndex(index)
|
||||
ft = sf.Type
|
||||
ref = sf.Tag.Get("ref")
|
||||
fk = sf.Tag.Get("fk")
|
||||
fName, _ = fieldName(sf)
|
||||
assocMeta = cachedAssociationMeta{
|
||||
targetIndex: index,
|
||||
through: sf.Tag.Get("through"),
|
||||
autoload: sf.Tag.Get("auto") == "true" || sf.Tag.Get("autoload") == "true",
|
||||
autosave: sf.Tag.Get("auto") == "true" || sf.Tag.Get("autosave") == "true",
|
||||
}
|
||||
)
|
||||
|
||||
if assocMeta.autosave && assocMeta.through != "" {
|
||||
panic("rel: autosave is not supported for has one/has many through association")
|
||||
}
|
||||
|
||||
for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
|
||||
var (
|
||||
refDocMeta = getDocumentMeta(rt, true)
|
||||
fkDocMeta = getDocumentMeta(ft, true)
|
||||
)
|
||||
|
||||
// Try to guess ref and fk if not defined.
|
||||
if ref == "" || fk == "" {
|
||||
// TODO: replace "id" with inferred primary field
|
||||
if assocMeta.through != "" {
|
||||
ref = "id"
|
||||
fk = "id"
|
||||
} else if _, isBelongsTo := refDocMeta.index[fName+"_id"]; isBelongsTo {
|
||||
ref = fName + "_id"
|
||||
fk = "id"
|
||||
} else {
|
||||
ref = "id"
|
||||
fk = snaker.CamelToSnake(rt.Name()) + "_id"
|
||||
}
|
||||
}
|
||||
|
||||
if id, exist := refDocMeta.index[ref]; !exist {
|
||||
panic("rel: references (" + ref + ") field not found ")
|
||||
} else {
|
||||
assocMeta.referenceIndex = id
|
||||
assocMeta.referenceField = ref
|
||||
}
|
||||
|
||||
if id, exist := fkDocMeta.index[fk]; !exist {
|
||||
panic("rel: foreign_key (" + fk + ") field not found")
|
||||
} else {
|
||||
assocMeta.foreignIndex = id
|
||||
assocMeta.foreignField = fk
|
||||
}
|
||||
|
||||
// guess assoc type
|
||||
if sf.Type.Kind() == reflect.Slice || (sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Slice) {
|
||||
assocMeta.typ = HasMany
|
||||
} else {
|
||||
if len(assocMeta.referenceField) > len(assocMeta.foreignField) {
|
||||
assocMeta.typ = BelongsTo
|
||||
} else {
|
||||
assocMeta.typ = HasOne
|
||||
}
|
||||
}
|
||||
|
||||
associationMetaCache.Store(key, assocMeta)
|
||||
|
||||
return AssociationMeta{
|
||||
rt: rt,
|
||||
cachedAssociationMeta: assocMeta,
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pair = [2]interface{}
|
||||
|
||||
// Changeset mutator for structs.
|
||||
// This allows REL to efficiently to perform update operation only on updated fields and association.
|
||||
// The catch is, enabling changeset will duplicates the original struct values which consumes more memory.
|
||||
type Changeset struct {
|
||||
doc *Document
|
||||
snapshot []interface{}
|
||||
assoc map[string]Changeset
|
||||
assocMany map[string]map[interface{}]Changeset
|
||||
}
|
||||
|
||||
func (c Changeset) valueChanged(typ reflect.Type, old interface{}, new interface{}) bool {
|
||||
if oeq, ok := old.(interface{ Equal(interface{}) bool }); ok {
|
||||
return !oeq.Equal(new)
|
||||
}
|
||||
|
||||
if ot, ok := old.(time.Time); ok {
|
||||
return !ot.Equal(new.(time.Time))
|
||||
}
|
||||
|
||||
if typ.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 {
|
||||
return !bytes.Equal(reflect.ValueOf(old).Bytes(), reflect.ValueOf(new).Bytes())
|
||||
}
|
||||
|
||||
return !(typ.Comparable() && old == new)
|
||||
}
|
||||
|
||||
// FieldChanged returns true if field exists and it's already changed.
|
||||
// returns false otherwise.
|
||||
func (c Changeset) FieldChanged(field string) bool {
|
||||
for i, f := range c.doc.Fields() {
|
||||
if f == field {
|
||||
var (
|
||||
typ, _ = c.doc.Type(field)
|
||||
old = c.snapshot[i]
|
||||
new, _ = c.doc.Value(field)
|
||||
)
|
||||
|
||||
return c.valueChanged(typ, old, new)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Changes returns map of changes.
|
||||
func (c Changeset) Changes() map[string]interface{} {
|
||||
return buildChanges(c.doc, c)
|
||||
}
|
||||
|
||||
// Apply mutation.
|
||||
func (c Changeset) Apply(doc *Document, mut *Mutation) {
|
||||
var (
|
||||
t = Now()
|
||||
)
|
||||
|
||||
for i, field := range c.doc.Fields() {
|
||||
var (
|
||||
typ, _ = c.doc.Type(field)
|
||||
old = c.snapshot[i]
|
||||
new, _ = c.doc.Value(field)
|
||||
)
|
||||
|
||||
if c.valueChanged(typ, old, new) {
|
||||
mut.Add(Set(field, new))
|
||||
}
|
||||
}
|
||||
|
||||
if !mut.IsMutatesEmpty() && c.doc.Flag(HasUpdatedAt) && c.doc.SetValue("updated_at", t) {
|
||||
mut.Add(Set("updated_at", t))
|
||||
}
|
||||
|
||||
if mut.Cascade {
|
||||
for _, field := range doc.BelongsTo() {
|
||||
c.applyAssoc(field, mut)
|
||||
}
|
||||
|
||||
for _, field := range doc.HasOne() {
|
||||
c.applyAssoc(field, mut)
|
||||
}
|
||||
|
||||
for _, field := range doc.HasMany() {
|
||||
c.applyAssocMany(field, mut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c Changeset) applyAssoc(field string, mut *Mutation) {
|
||||
assoc := c.doc.Association(field)
|
||||
if assoc.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
doc, _ := assoc.Document()
|
||||
|
||||
if ch, ok := c.assoc[field]; ok {
|
||||
if amod := Apply(doc, ch); !amod.IsEmpty() {
|
||||
mut.SetAssoc(field, amod)
|
||||
}
|
||||
} else {
|
||||
amod := Apply(doc, newStructset(doc, false))
|
||||
mut.SetAssoc(field, amod)
|
||||
}
|
||||
}
|
||||
|
||||
func (c Changeset) applyAssocMany(field string, mut *Mutation) {
|
||||
if chs, ok := c.assocMany[field]; ok {
|
||||
var (
|
||||
assoc = c.doc.Association(field)
|
||||
col, _ = assoc.Collection()
|
||||
muts = make([]Mutation, 0, col.Len())
|
||||
updatedIDs = make(map[interface{}]struct{})
|
||||
deletedIDs []interface{}
|
||||
)
|
||||
|
||||
for i := 0; i < col.Len(); i++ {
|
||||
var (
|
||||
doc = col.Get(i)
|
||||
pValue = doc.PrimaryValue()
|
||||
)
|
||||
|
||||
if ch, ok := chs[pValue]; ok {
|
||||
updatedIDs[pValue] = struct{}{}
|
||||
|
||||
if amod := Apply(doc, ch); !amod.IsEmpty() {
|
||||
muts = append(muts, amod)
|
||||
}
|
||||
} else {
|
||||
muts = append(muts, Apply(doc, newStructset(doc, false)))
|
||||
}
|
||||
}
|
||||
|
||||
// leftover snapshot.
|
||||
if len(updatedIDs) != len(chs) {
|
||||
for id := range chs {
|
||||
if _, ok := updatedIDs[id]; !ok {
|
||||
deletedIDs = append(deletedIDs, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(muts) > 0 || len(deletedIDs) > 0 {
|
||||
mut.SetAssoc(field, muts...)
|
||||
mut.SetDeletedIDs(field, deletedIDs)
|
||||
}
|
||||
} else {
|
||||
newStructset(c.doc, false).buildAssocMany(field, mut)
|
||||
}
|
||||
}
|
||||
|
||||
// NewChangeset returns new changeset mutator for given record.
|
||||
func NewChangeset(record interface{}) Changeset {
|
||||
return newChangeset(NewDocument(record))
|
||||
}
|
||||
|
||||
func newChangeset(doc *Document) Changeset {
|
||||
c := Changeset{
|
||||
doc: doc,
|
||||
snapshot: make([]interface{}, len(doc.Fields())),
|
||||
assoc: make(map[string]Changeset),
|
||||
assocMany: make(map[string]map[interface{}]Changeset),
|
||||
}
|
||||
|
||||
for i, field := range doc.Fields() {
|
||||
c.snapshot[i], _ = doc.Value(field)
|
||||
}
|
||||
|
||||
for _, field := range doc.BelongsTo() {
|
||||
initChangesetAssoc(doc, c.assoc, field)
|
||||
}
|
||||
|
||||
for _, field := range doc.HasOne() {
|
||||
initChangesetAssoc(doc, c.assoc, field)
|
||||
}
|
||||
|
||||
for _, field := range doc.HasMany() {
|
||||
initChangesetAssocMany(doc, c.assocMany, field)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func initChangesetAssoc(doc *Document, assoc map[string]Changeset, field string) {
|
||||
doc, loaded := doc.Association(field).Document()
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
|
||||
assoc[field] = newChangeset(doc)
|
||||
}
|
||||
|
||||
func initChangesetAssocMany(doc *Document, assoc map[string]map[interface{}]Changeset, field string) {
|
||||
col, loaded := doc.Association(field).Collection()
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
|
||||
assoc[field] = make(map[interface{}]Changeset)
|
||||
|
||||
for i := 0; i < col.Len(); i++ {
|
||||
var (
|
||||
doc = col.Get(i)
|
||||
pValue = doc.PrimaryValue()
|
||||
)
|
||||
|
||||
if !isZero(pValue) {
|
||||
assoc[field][pValue] = newChangeset(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildChanges(doc *Document, c Changeset) map[string]interface{} {
|
||||
var (
|
||||
changes = make(map[string]interface{})
|
||||
fields []string
|
||||
)
|
||||
|
||||
if doc != nil {
|
||||
fields = doc.Fields()
|
||||
} else {
|
||||
fields = c.doc.Fields()
|
||||
}
|
||||
|
||||
for i, field := range fields {
|
||||
switch {
|
||||
case doc == nil:
|
||||
if old := c.snapshot[i]; old != nil {
|
||||
changes[field] = pair{old, nil}
|
||||
}
|
||||
case i >= len(c.snapshot):
|
||||
if new, _ := doc.Value(field); new != nil {
|
||||
changes[field] = pair{nil, new}
|
||||
}
|
||||
default:
|
||||
old := c.snapshot[i]
|
||||
new, _ := doc.Value(field)
|
||||
if typ, _ := doc.Type(field); c.valueChanged(typ, old, new) {
|
||||
changes[field] = pair{old, new}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if doc == nil || len(c.snapshot) == 0 {
|
||||
return changes
|
||||
}
|
||||
|
||||
for _, field := range doc.BelongsTo() {
|
||||
buildChangesAssoc(changes, c, field)
|
||||
}
|
||||
|
||||
for _, field := range doc.HasOne() {
|
||||
buildChangesAssoc(changes, c, field)
|
||||
}
|
||||
|
||||
for _, field := range doc.HasMany() {
|
||||
buildChangesAssocMany(changes, c, field)
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
func buildChangesAssoc(out map[string]interface{}, c Changeset, field string) {
|
||||
assoc := c.doc.Association(field)
|
||||
if assoc.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
doc, _ := assoc.Document()
|
||||
if changes := buildChanges(doc, c.assoc[field]); len(changes) != 0 {
|
||||
out[field] = changes
|
||||
}
|
||||
}
|
||||
|
||||
func buildChangesAssocMany(out map[string]interface{}, c Changeset, field string) {
|
||||
var (
|
||||
changes []map[string]interface{}
|
||||
chs = c.assocMany[field]
|
||||
assoc = c.doc.Association(field)
|
||||
col, _ = assoc.Collection()
|
||||
updatedIDs = make(map[interface{}]struct{})
|
||||
)
|
||||
|
||||
for i := 0; i < col.Len(); i++ {
|
||||
var (
|
||||
doc = col.Get(i)
|
||||
pValue = doc.PrimaryValue()
|
||||
ch, isUpdate = chs[pValue]
|
||||
)
|
||||
|
||||
if isUpdate {
|
||||
updatedIDs[pValue] = struct{}{}
|
||||
}
|
||||
|
||||
if dChanges := buildChanges(doc, ch); len(dChanges) != 0 {
|
||||
changes = append(changes, dChanges)
|
||||
}
|
||||
}
|
||||
|
||||
// leftover snapshot.
|
||||
if len(updatedIDs) != len(chs) {
|
||||
for id, ch := range chs {
|
||||
if _, ok := updatedIDs[id]; !ok {
|
||||
changes = append(changes, buildChanges(nil, ch))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) != 0 {
|
||||
out[field] = changes
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type slice interface {
|
||||
table
|
||||
Reset()
|
||||
Add() *Document
|
||||
Get(index int) *Document
|
||||
Len() int
|
||||
Meta() DocumentMeta
|
||||
}
|
||||
|
||||
// Collection provides an abstraction over reflect to easily works with slice for database purpose.
|
||||
type Collection struct {
|
||||
v interface{}
|
||||
rv reflect.Value
|
||||
rt reflect.Type
|
||||
meta DocumentMeta
|
||||
swapper func(i, j int)
|
||||
}
|
||||
|
||||
// ReflectValue of referenced document.
|
||||
func (c Collection) ReflectValue() reflect.Value {
|
||||
return c.rv
|
||||
}
|
||||
|
||||
// Table returns name of the table.
|
||||
func (c *Collection) Table() string {
|
||||
return c.meta.Table()
|
||||
}
|
||||
|
||||
// PrimaryFields column name of this collection.
|
||||
func (c Collection) PrimaryFields() []string {
|
||||
if p, ok := c.v.(primary); ok {
|
||||
return p.PrimaryFields()
|
||||
}
|
||||
|
||||
if len(c.meta.primaryField) == 0 {
|
||||
panic("rel: failed to infer primary key for type " + c.rt.String())
|
||||
}
|
||||
|
||||
return c.meta.primaryField
|
||||
}
|
||||
|
||||
// PrimaryField column name of this document.
|
||||
// panic if document uses composite key.
|
||||
func (c Collection) PrimaryField() string {
|
||||
if fields := c.PrimaryFields(); len(fields) == 1 {
|
||||
return fields[0]
|
||||
}
|
||||
|
||||
panic("rel: composite primary key is not supported")
|
||||
}
|
||||
|
||||
// PrimaryValues of collection.
|
||||
// Returned value will be interface of slice interface.
|
||||
func (c Collection) PrimaryValues() []interface{} {
|
||||
if p, ok := c.v.(primary); ok {
|
||||
return p.PrimaryValues()
|
||||
}
|
||||
|
||||
var (
|
||||
index = c.meta.primaryIndex
|
||||
pValues = make([]interface{}, len(c.PrimaryFields()))
|
||||
)
|
||||
|
||||
if index != nil {
|
||||
for i := range index {
|
||||
var (
|
||||
idxLen = c.rv.Len()
|
||||
values = make([]interface{}, 0, idxLen)
|
||||
)
|
||||
|
||||
for j := 0; j < idxLen; j++ {
|
||||
if item := c.rvIndex(j); item.IsValid() {
|
||||
values = append(values, reflectValueFieldByIndex(item, index[i], false).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
pValues[i] = values
|
||||
}
|
||||
} else {
|
||||
// using interface.
|
||||
var (
|
||||
tmp = make([][]interface{}, len(pValues))
|
||||
)
|
||||
|
||||
for i := 0; i < c.rv.Len(); i++ {
|
||||
item := c.rvIndex(i)
|
||||
if !item.IsValid() {
|
||||
continue
|
||||
}
|
||||
for j, id := range item.Interface().(primary).PrimaryValues() {
|
||||
tmp[j] = append(tmp[j], id)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range tmp {
|
||||
pValues[i] = tmp[i]
|
||||
}
|
||||
}
|
||||
|
||||
return pValues
|
||||
}
|
||||
|
||||
// PrimaryValue of this document.
|
||||
// panic if document uses composite key.
|
||||
func (c Collection) PrimaryValue() interface{} {
|
||||
if values := c.PrimaryValues(); len(values) == 1 {
|
||||
return values[0]
|
||||
}
|
||||
|
||||
panic("rel: composite primary key is not supported")
|
||||
}
|
||||
|
||||
func (c Collection) rvIndex(index int) reflect.Value {
|
||||
return reflect.Indirect(c.rv.Index(index))
|
||||
}
|
||||
|
||||
// Get an element from the underlying slice as a document.
|
||||
func (c Collection) Get(index int) *Document {
|
||||
return NewDocument(c.rvIndex(index).Addr())
|
||||
}
|
||||
|
||||
// Len of the underlying slice.
|
||||
func (c Collection) Len() int {
|
||||
return c.rv.Len()
|
||||
}
|
||||
|
||||
// Meta returns document meta.
|
||||
func (c Collection) Meta() DocumentMeta {
|
||||
return c.meta
|
||||
}
|
||||
|
||||
// Reset underlying slice to be zero length.
|
||||
func (c Collection) Reset() {
|
||||
c.rv.Set(reflect.MakeSlice(c.rt, 0, 0))
|
||||
}
|
||||
|
||||
// Add new document into collection.
|
||||
func (c Collection) Add() *Document {
|
||||
var (
|
||||
index = c.Len()
|
||||
typ = c.rt.Elem()
|
||||
drv = reflect.Zero(typ)
|
||||
)
|
||||
|
||||
if typ.Kind() == reflect.Ptr && drv.IsNil() {
|
||||
drv = reflect.New(drv.Type().Elem())
|
||||
}
|
||||
|
||||
c.rv.Set(reflect.Append(c.rv, drv))
|
||||
|
||||
return NewDocument(c.rvIndex(index).Addr())
|
||||
}
|
||||
|
||||
// Truncate collection.
|
||||
func (c Collection) Truncate(i, j int) {
|
||||
c.rv.Set(c.rv.Slice(i, j))
|
||||
}
|
||||
|
||||
// Slice returns a new collection that is a slice of the original collection.s
|
||||
func (c Collection) Slice(i, j int) *Collection {
|
||||
return NewCollection(c.rv.Slice(i, j), true)
|
||||
}
|
||||
|
||||
// Swap element in the collection.
|
||||
func (c Collection) Swap(i, j int) {
|
||||
if c.swapper == nil {
|
||||
c.swapper = reflect.Swapper(c.rv.Interface())
|
||||
}
|
||||
|
||||
c.swapper(i, j)
|
||||
}
|
||||
|
||||
// NewCollection used to create abstraction to work with slice.
|
||||
// COllection can be created using interface or reflect.Value.
|
||||
func NewCollection(records interface{}, readonly ...bool) *Collection {
|
||||
switch v := records.(type) {
|
||||
case *Collection:
|
||||
return v
|
||||
case reflect.Value:
|
||||
return newCollection(v.Interface(), v, len(readonly) > 0 && readonly[0])
|
||||
case reflect.Type:
|
||||
panic("rel: cannot use reflect.Type")
|
||||
case nil:
|
||||
panic("rel: cannot be nil")
|
||||
default:
|
||||
return newCollection(v, reflect.ValueOf(v), len(readonly) > 0 && readonly[0])
|
||||
}
|
||||
}
|
||||
|
||||
func newCollection(v interface{}, rv reflect.Value, readonly bool) *Collection {
|
||||
var (
|
||||
rt = rv.Type()
|
||||
)
|
||||
|
||||
if rt.Kind() != reflect.Ptr {
|
||||
if !readonly {
|
||||
panic("rel: must be a pointer to slice")
|
||||
}
|
||||
} else {
|
||||
rv = rv.Elem()
|
||||
rt = rt.Elem()
|
||||
}
|
||||
|
||||
if rt.Kind() != reflect.Slice {
|
||||
panic("rel: must be a slice or pointer to a slice")
|
||||
}
|
||||
|
||||
return &Collection{
|
||||
v: v,
|
||||
rv: rv,
|
||||
rt: rt,
|
||||
meta: getDocumentMeta(indirectReflectType(rt.Elem()), false),
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package rel
|
||||
|
||||
// ColumnType definition.
|
||||
type ColumnType string
|
||||
|
||||
const (
|
||||
// ID ColumnType.
|
||||
ID ColumnType = "ID"
|
||||
// BigID ColumnType.
|
||||
BigID ColumnType = "BigID"
|
||||
// Bool ColumnType.
|
||||
Bool ColumnType = "BOOL"
|
||||
// SmallInt ColumnType.
|
||||
SmallInt ColumnType = "SMALLINT"
|
||||
// Int ColumnType.
|
||||
Int ColumnType = "INT"
|
||||
// BigInt ColumnType.
|
||||
BigInt ColumnType = "BIGINT"
|
||||
// Float ColumnType.
|
||||
Float ColumnType = "FLOAT"
|
||||
// Decimal ColumnType.
|
||||
Decimal ColumnType = "DECIMAL"
|
||||
// String ColumnType.
|
||||
String ColumnType = "STRING"
|
||||
// Text ColumnType.
|
||||
Text ColumnType = "TEXT"
|
||||
// JSON ColumnType that will fallback to Text ColumnType if adapter does not support it.
|
||||
JSON ColumnType = "JSON"
|
||||
// Date ColumnType.
|
||||
Date ColumnType = "DATE"
|
||||
// DateTime ColumnType.
|
||||
DateTime ColumnType = "DATETIME"
|
||||
// Time ColumnType.
|
||||
Time ColumnType = "TIME"
|
||||
)
|
||||
|
||||
// Column definition.
|
||||
type Column struct {
|
||||
Op SchemaOp
|
||||
Name string
|
||||
Type ColumnType
|
||||
Rename string
|
||||
Primary bool
|
||||
Unique bool
|
||||
Required bool
|
||||
Unsigned bool
|
||||
Limit int
|
||||
Precision int
|
||||
Scale int
|
||||
Default interface{}
|
||||
Options string
|
||||
}
|
||||
|
||||
func (Column) internalTableDefinition() {}
|
||||
|
||||
func createColumn(name string, typ ColumnType, options []ColumnOption) Column {
|
||||
column := Column{
|
||||
Op: SchemaCreate,
|
||||
Name: name,
|
||||
Type: typ,
|
||||
}
|
||||
|
||||
applyColumnOptions(&column, options)
|
||||
return column
|
||||
}
|
||||
|
||||
func renameColumn(name string, newName string, options []ColumnOption) Column {
|
||||
column := Column{
|
||||
Op: SchemaRename,
|
||||
Name: name,
|
||||
Rename: newName,
|
||||
}
|
||||
|
||||
applyColumnOptions(&column, options)
|
||||
return column
|
||||
}
|
||||
|
||||
func dropColumn(name string, options []ColumnOption) Column {
|
||||
column := Column{
|
||||
Op: SchemaDrop,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
applyColumnOptions(&column, options)
|
||||
return column
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type contextKey int8
|
||||
|
||||
type contextWrapper struct {
|
||||
ctx context.Context
|
||||
adapter Adapter
|
||||
}
|
||||
|
||||
var ctxKey contextKey
|
||||
|
||||
// fetchContext and use adapter passed by context if exists.
|
||||
// it stores contextData values to struct for fast repeated access.
|
||||
func fetchContext(ctx context.Context, adapter Adapter) contextWrapper {
|
||||
if adp, ok := ctx.Value(ctxKey).(Adapter); ok {
|
||||
adapter = adp
|
||||
}
|
||||
|
||||
return contextWrapper{
|
||||
ctx: ctx,
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
// wrapContext wraps adapter inside context.
|
||||
func wrapContext(ctx context.Context, adapter Adapter) contextWrapper {
|
||||
return contextWrapper{
|
||||
ctx: context.WithValue(ctx, ctxKey, adapter),
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
@ -0,0 +1,277 @@
|
||||
// Modified from: database/sql/convert.go
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
|
||||
package rel
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _, localTimeOffset = time.Now().Local().Zone()
|
||||
|
||||
// convertAssign copies to dest the value in src, converting it if possible.
|
||||
// An error is returned if the copy would result in loss of information.
|
||||
// dest should be a pointer type.
|
||||
// dest will be set to zero value if src is nil.
|
||||
// this function assumes dest will never be nil.
|
||||
func convertAssign(dest, src interface{}) error {
|
||||
// Common cases, without reflect.
|
||||
switch s := src.(type) {
|
||||
case string:
|
||||
switch d := dest.(type) {
|
||||
case *string:
|
||||
*d = s
|
||||
return nil
|
||||
case *[]byte:
|
||||
*d = []byte(s)
|
||||
return nil
|
||||
case *sql.RawBytes:
|
||||
*d = append((*d)[:0], s...)
|
||||
return nil
|
||||
}
|
||||
case []byte:
|
||||
switch d := dest.(type) {
|
||||
case *string:
|
||||
*d = string(s)
|
||||
return nil
|
||||
case *interface{}:
|
||||
*d = cloneBytes(s)
|
||||
return nil
|
||||
case *[]byte:
|
||||
*d = cloneBytes(s)
|
||||
return nil
|
||||
case *sql.RawBytes:
|
||||
*d = s
|
||||
return nil
|
||||
}
|
||||
case time.Time:
|
||||
switch d := dest.(type) {
|
||||
case *time.Time:
|
||||
// make sure timezone equal for test assertion.
|
||||
if _, offset := s.Zone(); offset == localTimeOffset {
|
||||
*d = s.Local()
|
||||
} else {
|
||||
*d = s
|
||||
}
|
||||
return nil
|
||||
case *string:
|
||||
*d = s.Format(time.RFC3339Nano)
|
||||
return nil
|
||||
case *[]byte:
|
||||
*d = []byte(s.Format(time.RFC3339Nano))
|
||||
return nil
|
||||
case *sql.RawBytes:
|
||||
*d = s.AppendFormat((*d)[:0], time.RFC3339Nano)
|
||||
return nil
|
||||
}
|
||||
case nil:
|
||||
assignZero(dest)
|
||||
return nil
|
||||
}
|
||||
|
||||
var sv reflect.Value
|
||||
|
||||
switch d := dest.(type) {
|
||||
case *string:
|
||||
sv = reflect.ValueOf(src)
|
||||
switch sv.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
if s, ok := asString(src); ok {
|
||||
*d = s
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case *[]byte:
|
||||
sv = reflect.ValueOf(src)
|
||||
if b, ok := asBytes(nil, sv); ok {
|
||||
*d = b
|
||||
return nil
|
||||
}
|
||||
case *sql.RawBytes:
|
||||
sv = reflect.ValueOf(src)
|
||||
if b, ok := asBytes([]byte(*d)[:0], sv); ok {
|
||||
*d = sql.RawBytes(b)
|
||||
return nil
|
||||
}
|
||||
case *bool:
|
||||
bv, err := driver.Bool.ConvertValue(src)
|
||||
if err == nil {
|
||||
*d = bv.(bool)
|
||||
}
|
||||
return err
|
||||
case *interface{}:
|
||||
*d = src
|
||||
return nil
|
||||
}
|
||||
|
||||
dpv := reflect.ValueOf(dest)
|
||||
|
||||
if !sv.IsValid() {
|
||||
sv = reflect.ValueOf(src)
|
||||
}
|
||||
|
||||
dv := reflect.Indirect(dpv)
|
||||
if sv.IsValid() && sv.Type().AssignableTo(dv.Type()) {
|
||||
switch b := src.(type) {
|
||||
case []byte:
|
||||
dv.Set(reflect.ValueOf(cloneBytes(b)))
|
||||
default:
|
||||
dv.Set(sv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if dv.Kind() == sv.Kind() && sv.Type().ConvertibleTo(dv.Type()) {
|
||||
dv.Set(sv.Convert(dv.Type()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// The following conversions use a string value as an intermediate representation
|
||||
// to convert between various numeric types.
|
||||
//
|
||||
// This also allows scanning into user defined types such as "type Int int64".
|
||||
// For symmetry, also check for string destination types.
|
||||
if s, ok := asString(src); ok {
|
||||
switch dv.Kind() {
|
||||
case reflect.Ptr:
|
||||
dv.Set(reflect.New(dv.Type().Elem()))
|
||||
return convertAssign(dv.Interface(), src)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i64, err := strconv.ParseInt(s, 10, dv.Type().Bits())
|
||||
if err != nil {
|
||||
// The errors that ParseInt returns have concrete type *NumError
|
||||
err = err.(*strconv.NumError).Err
|
||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
|
||||
}
|
||||
dv.SetInt(i64)
|
||||
return nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
u64, err := strconv.ParseUint(s, 10, dv.Type().Bits())
|
||||
if err != nil {
|
||||
// The errors that ParseUint returns have concrete type *NumError
|
||||
err = err.(*strconv.NumError).Err
|
||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
|
||||
}
|
||||
dv.SetUint(u64)
|
||||
return nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f64, err := strconv.ParseFloat(s, dv.Type().Bits())
|
||||
if err != nil {
|
||||
// The errors that ParseFloat returns have concrete type *NumError
|
||||
err = err.(*strconv.NumError).Err
|
||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
|
||||
}
|
||||
dv.SetFloat(f64)
|
||||
return nil
|
||||
case reflect.String:
|
||||
dv.SetString(s)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest)
|
||||
}
|
||||
|
||||
func cloneBytes(b []byte) []byte {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := make([]byte, len(b))
|
||||
copy(c, b)
|
||||
return c
|
||||
}
|
||||
|
||||
func asString(src interface{}) (string, bool) {
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
return v, true
|
||||
case []byte:
|
||||
return string(v), true
|
||||
}
|
||||
rv := reflect.ValueOf(src)
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(rv.Int(), 10), true
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return strconv.FormatUint(rv.Uint(), 10), true
|
||||
case reflect.Float64:
|
||||
return strconv.FormatFloat(rv.Float(), 'g', -1, 64), true
|
||||
case reflect.Float32:
|
||||
return strconv.FormatFloat(rv.Float(), 'g', -1, 32), true
|
||||
case reflect.Bool:
|
||||
return strconv.FormatBool(rv.Bool()), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) {
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.AppendInt(buf, rv.Int(), 10), true
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return strconv.AppendUint(buf, rv.Uint(), 10), true
|
||||
case reflect.Float32:
|
||||
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 32), true
|
||||
case reflect.Float64:
|
||||
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 64), true
|
||||
case reflect.Bool:
|
||||
return strconv.AppendBool(buf, rv.Bool()), true
|
||||
case reflect.String:
|
||||
s := rv.String()
|
||||
return append(buf, s...), true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func assignZero(dest interface{}) {
|
||||
switch d := dest.(type) {
|
||||
case *bool:
|
||||
*d = false
|
||||
case *string:
|
||||
*d = ""
|
||||
case *int:
|
||||
*d = 0
|
||||
case *int8:
|
||||
*d = 0
|
||||
case *int16:
|
||||
*d = 0
|
||||
case *int32:
|
||||
*d = 0
|
||||
case *int64:
|
||||
*d = 0
|
||||
case *uint:
|
||||
*d = 0
|
||||
case *uint8:
|
||||
*d = 0
|
||||
case *uint16:
|
||||
*d = 0
|
||||
case *uint32:
|
||||
*d = 0
|
||||
case *uint64:
|
||||
*d = 0
|
||||
case *uintptr:
|
||||
*d = 0
|
||||
case *float32:
|
||||
*d = 0
|
||||
case *float64:
|
||||
*d = 0
|
||||
case *interface{}:
|
||||
*d = nil
|
||||
case *[]byte:
|
||||
*d = nil
|
||||
case *sql.RawBytes:
|
||||
*d = nil
|
||||
default:
|
||||
rv := reflect.ValueOf(dest)
|
||||
rv.Elem().Set(reflect.Zero(rv.Type().Elem()))
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Cursor is interface to work with database result (used by adapter).
|
||||
type Cursor interface {
|
||||
Close() error
|
||||
Fields() ([]string, error)
|
||||
Next() bool
|
||||
Scan(...interface{}) error
|
||||
NopScanner() interface{} // TODO: conflict with manual scanners interface
|
||||
}
|
||||
|
||||
func scanOne(cur Cursor, doc *Document) error {
|
||||
defer cur.Close()
|
||||
|
||||
fields, err := cur.Fields()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !cur.Next() {
|
||||
return NotFoundError{}
|
||||
}
|
||||
|
||||
var (
|
||||
scanners = doc.Scanners(fields)
|
||||
)
|
||||
|
||||
return cur.Scan(scanners...)
|
||||
}
|
||||
|
||||
func scanAll(cur Cursor, col *Collection) error {
|
||||
defer cur.Close()
|
||||
|
||||
fields, err := cur.Fields()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for cur.Next() {
|
||||
var (
|
||||
doc = col.Add()
|
||||
scanners = doc.Scanners(fields)
|
||||
)
|
||||
|
||||
if err := cur.Scan(scanners...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanMulti(cur Cursor, keyField string, keyType reflect.Type, cols map[interface{}][]slice) error {
|
||||
defer cur.Close()
|
||||
|
||||
fields, err := cur.Fields()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
found = false
|
||||
keyValue = reflect.New(keyType)
|
||||
keyScanners = make([]interface{}, len(fields))
|
||||
)
|
||||
|
||||
for i, field := range fields {
|
||||
if keyField == field {
|
||||
found = true
|
||||
keyScanners[i] = keyValue.Interface()
|
||||
} else {
|
||||
// need to create distinct copies
|
||||
// otherwise next scan result will be corrupted
|
||||
keyScanners[i] = &sql.RawBytes{}
|
||||
}
|
||||
}
|
||||
|
||||
if !found && fields != nil {
|
||||
panic("rel: primary key row does not exists")
|
||||
}
|
||||
|
||||
// scan the result
|
||||
for cur.Next() {
|
||||
// scan key
|
||||
if err := cur.Scan(keyScanners...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
key = reflect.Indirect(keyValue).Interface()
|
||||
)
|
||||
|
||||
for _, col := range cols[key] {
|
||||
var (
|
||||
doc = col.Add()
|
||||
scanners = doc.Scanners(fields)
|
||||
)
|
||||
|
||||
if err := cur.Scan(scanners...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,338 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Document provides an abstraction over reflect to easily works with struct for database purpose.
|
||||
type Document struct {
|
||||
v interface{}
|
||||
rv reflect.Value
|
||||
rt reflect.Type
|
||||
meta DocumentMeta
|
||||
}
|
||||
|
||||
// ReflectValue of referenced document.
|
||||
func (d Document) ReflectValue() reflect.Value {
|
||||
return d.rv
|
||||
}
|
||||
|
||||
// Table returns name of the table.
|
||||
func (d Document) Table() string {
|
||||
// TODO: handle anonymous struct
|
||||
return d.meta.Table()
|
||||
}
|
||||
|
||||
// PrimaryFields column name of this document.
|
||||
func (d Document) PrimaryFields() []string {
|
||||
return d.meta.PrimaryFields()
|
||||
}
|
||||
|
||||
// PrimaryField column name of this document.
|
||||
// panic if document uses composite key.
|
||||
func (d Document) PrimaryField() string {
|
||||
return d.meta.PrimaryField()
|
||||
}
|
||||
|
||||
// PrimaryValues of this document.
|
||||
func (d Document) PrimaryValues() []interface{} {
|
||||
if p, ok := d.v.(primary); ok {
|
||||
return p.PrimaryValues()
|
||||
}
|
||||
|
||||
if len(d.meta.primaryIndex) == 0 {
|
||||
panic("rel: failed to infer primary key for type " + d.rt.String())
|
||||
}
|
||||
|
||||
var (
|
||||
pValues = make([]interface{}, len(d.meta.primaryIndex))
|
||||
)
|
||||
|
||||
for i := range pValues {
|
||||
pValues[i] = reflectValueFieldByIndex(d.rv, d.meta.primaryIndex[i], false).Interface()
|
||||
}
|
||||
|
||||
return pValues
|
||||
}
|
||||
|
||||
// PrimaryValue of this document.
|
||||
// panic if document uses composite key.
|
||||
func (d Document) PrimaryValue() interface{} {
|
||||
if values := d.PrimaryValues(); len(values) == 1 {
|
||||
return values[0]
|
||||
}
|
||||
|
||||
panic("rel: composite primary key is not supported")
|
||||
}
|
||||
|
||||
// Persisted returns true if document primary key is not zero.
|
||||
func (d Document) Persisted() bool {
|
||||
var (
|
||||
pValues = d.PrimaryValues()
|
||||
)
|
||||
|
||||
for i := range pValues {
|
||||
if !isZero(pValues[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Index returns map of column name and it's struct index.
|
||||
func (d Document) Index() map[string][]int {
|
||||
return d.meta.Index()
|
||||
}
|
||||
|
||||
// Fields returns list of fields available on this document.
|
||||
func (d Document) Fields() []string {
|
||||
return d.meta.Fields()
|
||||
}
|
||||
|
||||
// Type returns reflect.Type of given field. if field does not exist, second returns value will be false.
|
||||
func (d Document) Type(field string) (reflect.Type, bool) {
|
||||
return d.meta.Type(field)
|
||||
}
|
||||
|
||||
// Value returns value of given field. if field does not exist, second returns value will be false.
|
||||
func (d Document) Value(field string) (interface{}, bool) {
|
||||
if i, ok := d.meta.index[field]; ok {
|
||||
|
||||
var (
|
||||
value interface{}
|
||||
fv = reflectValueFieldByIndex(d.rv, i, false)
|
||||
ft = fv.Type()
|
||||
)
|
||||
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
if !fv.IsNil() {
|
||||
value = fv.Elem().Interface()
|
||||
}
|
||||
} else {
|
||||
value = fv.Interface()
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// SetValue of the field, it returns false if field does not exist, or it's not assignable.
|
||||
func (d Document) SetValue(field string, value interface{}) bool {
|
||||
if i, ok := d.meta.index[field]; ok {
|
||||
var (
|
||||
rv reflect.Value
|
||||
rt reflect.Type
|
||||
fv = reflectValueFieldByIndex(d.rv, i, true)
|
||||
ft = fv.Type()
|
||||
)
|
||||
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
rv = reflect.Zero(ft)
|
||||
case reflect.Value:
|
||||
rv = reflect.Indirect(v)
|
||||
default:
|
||||
rv = reflect.Indirect(reflect.ValueOf(value))
|
||||
}
|
||||
|
||||
rt = rv.Type()
|
||||
|
||||
if fv.Type() == rt || rt.AssignableTo(ft) {
|
||||
fv.Set(rv)
|
||||
return true
|
||||
}
|
||||
|
||||
if rt.ConvertibleTo(ft) {
|
||||
return setConvertValue(ft, fv, rt, rv)
|
||||
}
|
||||
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
return setPointerValue(ft, fv, rt, rv)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Scanners returns slice of sql.Scanner for given fields.
|
||||
func (d Document) Scanners(fields []string) []interface{} {
|
||||
var (
|
||||
result = make([]interface{}, len(fields))
|
||||
assocRefs map[string]struct {
|
||||
fields []string
|
||||
indexes []int
|
||||
}
|
||||
)
|
||||
|
||||
for index, field := range fields {
|
||||
if structIndex, ok := d.meta.index[field]; ok {
|
||||
var (
|
||||
fv = reflectValueFieldByIndex(d.rv, structIndex, true)
|
||||
ft = fv.Type()
|
||||
)
|
||||
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
result[index] = fv.Addr().Interface()
|
||||
} else {
|
||||
result[index] = Nullable(fv.Addr().Interface())
|
||||
}
|
||||
} else if split := strings.SplitN(field, ".", 2); len(split) == 2 {
|
||||
if assocRefs == nil {
|
||||
assocRefs = make(map[string]struct {
|
||||
fields []string
|
||||
indexes []int
|
||||
})
|
||||
}
|
||||
|
||||
refs := assocRefs[split[0]]
|
||||
refs.fields = append(refs.fields, split[1])
|
||||
refs.indexes = append(refs.indexes, index)
|
||||
assocRefs[split[0]] = refs
|
||||
} else {
|
||||
result[index] = &sql.RawBytes{}
|
||||
}
|
||||
}
|
||||
|
||||
// get scanners from associations
|
||||
for assocName, refs := range assocRefs {
|
||||
if assoc, ok := d.association(assocName); ok && assoc.Type() == BelongsTo || assoc.Type() == HasOne {
|
||||
var (
|
||||
assocDoc, _ = assoc.Document()
|
||||
assocScanners = assocDoc.Scanners(refs.fields)
|
||||
)
|
||||
|
||||
for i, index := range refs.indexes {
|
||||
result[index] = assocScanners[i]
|
||||
}
|
||||
} else {
|
||||
for _, index := range refs.indexes {
|
||||
result[index] = &sql.RawBytes{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// BelongsTo fields of this document.
|
||||
func (d Document) BelongsTo() []string {
|
||||
return d.meta.BelongsTo()
|
||||
}
|
||||
|
||||
// HasOne fields of this document.
|
||||
func (d Document) HasOne() []string {
|
||||
return d.meta.HasOne()
|
||||
}
|
||||
|
||||
// HasMany fields of this document.
|
||||
func (d Document) HasMany() []string {
|
||||
return d.meta.HasMany()
|
||||
}
|
||||
|
||||
// Preload fields of this document.
|
||||
func (d Document) Preload() []string {
|
||||
return d.meta.Preload()
|
||||
}
|
||||
|
||||
// Association of this document with given name.
|
||||
func (d Document) Association(name string) Association {
|
||||
if assoc, ok := d.association(name); ok {
|
||||
return assoc
|
||||
}
|
||||
|
||||
panic("rel: no field named (" + name + ") in type " + d.rt.String() + " found ")
|
||||
}
|
||||
|
||||
func (d Document) association(name string) (Association, bool) {
|
||||
index, ok := d.meta.index[name]
|
||||
if !ok {
|
||||
return Association{}, false
|
||||
}
|
||||
|
||||
return newAssociation(d.rv, index), true
|
||||
}
|
||||
|
||||
// Reset this document, this is a noop for compatibility with collection.
|
||||
func (d Document) Reset() {
|
||||
}
|
||||
|
||||
// Add returns this document.
|
||||
func (d *Document) Add() *Document {
|
||||
// if d.rv is a null pointer, set it to a new struct.
|
||||
if d.rv.Kind() == reflect.Ptr && d.rv.IsNil() {
|
||||
d.rv.Set(reflect.New(d.rv.Type().Elem()))
|
||||
d.rv = d.rv.Elem()
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Get always returns this document, this is a noop for compatibility with collection.
|
||||
func (d *Document) Get(index int) *Document {
|
||||
return d
|
||||
}
|
||||
|
||||
// Len always returns 1 for document, this is a noop for compatibility with collection.
|
||||
func (d *Document) Len() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Meta returns document meta.
|
||||
func (d Document) Meta() DocumentMeta {
|
||||
return d.meta
|
||||
}
|
||||
|
||||
// Flag returns true if struct contains specified flag.
|
||||
func (d Document) Flag(flag DocumentFlag) bool {
|
||||
return d.meta.Flag(flag)
|
||||
}
|
||||
|
||||
// NewDocument used to create abstraction to work with struct.
|
||||
// Document can be created using interface or reflect.Value.
|
||||
func NewDocument(record interface{}, readonly ...bool) *Document {
|
||||
switch v := record.(type) {
|
||||
case *Document:
|
||||
return v
|
||||
case reflect.Value:
|
||||
return newDocument(v.Interface(), v, len(readonly) > 0 && readonly[0])
|
||||
case reflect.Type:
|
||||
panic("rel: cannot use reflect.Type")
|
||||
case nil:
|
||||
panic("rel: cannot be nil")
|
||||
default:
|
||||
return newDocument(v, reflect.ValueOf(v), len(readonly) > 0 && readonly[0])
|
||||
}
|
||||
}
|
||||
|
||||
func newDocument(v interface{}, rv reflect.Value, readonly bool) *Document {
|
||||
var (
|
||||
rt = rv.Type()
|
||||
)
|
||||
|
||||
if rt.Kind() != reflect.Ptr {
|
||||
if !readonly {
|
||||
panic("rel: must be a pointer to struct")
|
||||
}
|
||||
} else {
|
||||
if !rv.IsNil() {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
rt = rt.Elem()
|
||||
}
|
||||
|
||||
if rt.Kind() != reflect.Struct {
|
||||
panic("rel: must be a struct or pointer to a struct")
|
||||
}
|
||||
|
||||
return &Document{
|
||||
v: v,
|
||||
rv: rv,
|
||||
rt: rt,
|
||||
meta: getDocumentMeta(rt, false),
|
||||
}
|
||||
}
|
@ -0,0 +1,431 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/inflection"
|
||||
"github.com/serenize/snaker"
|
||||
)
|
||||
|
||||
var (
|
||||
primariesCache sync.Map
|
||||
documentMetaCache sync.Map
|
||||
rtTime = reflect.TypeOf(time.Time{})
|
||||
rtBool = reflect.TypeOf(false)
|
||||
rtInt = reflect.TypeOf(int(0))
|
||||
rtTable = reflect.TypeOf((*table)(nil)).Elem()
|
||||
rtPrimary = reflect.TypeOf((*primary)(nil)).Elem()
|
||||
)
|
||||
|
||||
// DocumentFlag stores information about document as a flag.
|
||||
type DocumentFlag int8
|
||||
|
||||
// Is returns true if it's defined.
|
||||
func (df DocumentFlag) Is(flag DocumentFlag) bool {
|
||||
return (df & flag) == flag
|
||||
}
|
||||
|
||||
const (
|
||||
// Invalid flag.
|
||||
Invalid DocumentFlag = 1 << iota
|
||||
// HasCreatedAt flag.
|
||||
HasCreatedAt
|
||||
// HasUpdatedAt flag.
|
||||
HasUpdatedAt
|
||||
// HasDeletedAt flag.
|
||||
HasDeletedAt
|
||||
// HasDeleted flag.
|
||||
HasDeleted
|
||||
// Versioning
|
||||
HasVersioning
|
||||
)
|
||||
|
||||
type table interface {
|
||||
Table() string
|
||||
}
|
||||
|
||||
type primary interface {
|
||||
PrimaryFields() []string
|
||||
PrimaryValues() []interface{}
|
||||
}
|
||||
|
||||
type primaryData struct {
|
||||
field []string
|
||||
index [][]int
|
||||
}
|
||||
|
||||
type cachedDocumentMeta struct {
|
||||
table string
|
||||
index map[string][]int
|
||||
fields []string
|
||||
belongsTo []string
|
||||
hasOne []string
|
||||
hasMany []string
|
||||
primaryField []string
|
||||
primaryIndex [][]int
|
||||
preload []string
|
||||
flag DocumentFlag
|
||||
}
|
||||
|
||||
// Adds a prefix to field names
|
||||
func appendWithPrefix(target, fieldNames []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return append(target, fieldNames...)
|
||||
}
|
||||
for _, name := range fieldNames {
|
||||
target = append(target, prefix+name)
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
// Adds a field index and checks for conflicts
|
||||
func (cdm *cachedDocumentMeta) addFieldIndex(name string, index []int) {
|
||||
if _, ok := cdm.index[name]; ok {
|
||||
panic("rel: conflicting field (" + name + ") in struct")
|
||||
}
|
||||
cdm.index[name] = index
|
||||
}
|
||||
|
||||
// Transfer values from other document data
|
||||
func (cdm *cachedDocumentMeta) mergeEmbedded(other cachedDocumentMeta, indexPrefix int, namePrefix string) {
|
||||
for name, path := range other.index {
|
||||
cdm.addFieldIndex(namePrefix+name, append([]int{indexPrefix}, path...))
|
||||
}
|
||||
cdm.fields = appendWithPrefix(cdm.fields, other.fields, namePrefix)
|
||||
cdm.belongsTo = appendWithPrefix(cdm.belongsTo, other.belongsTo, namePrefix)
|
||||
cdm.hasOne = appendWithPrefix(cdm.hasOne, other.hasOne, namePrefix)
|
||||
cdm.hasMany = appendWithPrefix(cdm.hasMany, other.hasMany, namePrefix)
|
||||
cdm.primaryField = appendWithPrefix(cdm.primaryField, other.primaryField, namePrefix)
|
||||
for index := range other.primaryIndex {
|
||||
cdm.primaryIndex = append(cdm.primaryIndex, append([]int{indexPrefix}, index))
|
||||
}
|
||||
cdm.preload = appendWithPrefix(cdm.preload, other.preload, namePrefix)
|
||||
cdm.flag |= other.flag
|
||||
}
|
||||
|
||||
type DocumentMeta struct {
|
||||
rt reflect.Type
|
||||
cachedDocumentMeta
|
||||
}
|
||||
|
||||
// Table returns name of the table.
|
||||
func (dm DocumentMeta) Table() string {
|
||||
return dm.table
|
||||
}
|
||||
|
||||
// PrimaryFields column name of this document.
|
||||
func (dm DocumentMeta) PrimaryFields() []string {
|
||||
if len(dm.primaryField) == 0 {
|
||||
panic("rel: failed to infer primary key for type " + dm.rt.String())
|
||||
}
|
||||
|
||||
return dm.primaryField
|
||||
}
|
||||
|
||||
// PrimaryField column name of this document.
|
||||
// panic if document uses composite key.
|
||||
func (dm DocumentMeta) PrimaryField() string {
|
||||
if fields := dm.PrimaryFields(); len(fields) == 1 {
|
||||
return fields[0]
|
||||
}
|
||||
|
||||
panic("rel: composite primary key is not supported")
|
||||
}
|
||||
|
||||
// Index returns map of column name and it's struct index.
|
||||
func (dm DocumentMeta) Index() map[string][]int {
|
||||
return dm.index
|
||||
}
|
||||
|
||||
// Fields returns list of fields available on this document.
|
||||
func (dm DocumentMeta) Fields() []string {
|
||||
return dm.fields
|
||||
}
|
||||
|
||||
// Type returns reflect.Type of given field. if field does not exist, second returns value will be false.
|
||||
func (dm DocumentMeta) Type(field string) (reflect.Type, bool) {
|
||||
if i, ok := dm.index[field]; ok {
|
||||
var (
|
||||
ft = dm.rt.FieldByIndex(i).Type
|
||||
)
|
||||
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
} else if ft.Kind() == reflect.Slice && ft.Elem().Kind() == reflect.Ptr {
|
||||
ft = reflect.SliceOf(ft.Elem().Elem())
|
||||
}
|
||||
|
||||
return ft, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// BelongsTo fields of this document.
|
||||
func (dm DocumentMeta) BelongsTo() []string {
|
||||
return dm.belongsTo
|
||||
}
|
||||
|
||||
// HasOne fields of this document.
|
||||
func (dm DocumentMeta) HasOne() []string {
|
||||
return dm.hasOne
|
||||
}
|
||||
|
||||
// HasMany fields of this document.
|
||||
func (dm DocumentMeta) HasMany() []string {
|
||||
return dm.hasMany
|
||||
}
|
||||
|
||||
// Preload fields of this document.
|
||||
func (dm DocumentMeta) Preload() []string {
|
||||
return dm.preload
|
||||
}
|
||||
|
||||
// Association of this document with given name.
|
||||
func (dm DocumentMeta) Association(name string) AssociationMeta {
|
||||
if assoc, ok := dm.association(name); ok {
|
||||
return assoc
|
||||
}
|
||||
|
||||
panic("rel: no field named (" + name + ") in type " + dm.rt.String() + " found ")
|
||||
}
|
||||
|
||||
func (dm DocumentMeta) association(name string) (AssociationMeta, bool) {
|
||||
index, ok := dm.index[name]
|
||||
if !ok {
|
||||
return AssociationMeta{}, false
|
||||
}
|
||||
|
||||
return getAssociationMeta(dm.rt, index), true
|
||||
}
|
||||
|
||||
// Flag returns true if struct contains specified flag.
|
||||
func (dm DocumentMeta) Flag(flag DocumentFlag) bool {
|
||||
return dm.flag.Is(flag)
|
||||
}
|
||||
|
||||
func getDocumentMeta(rt reflect.Type, skipAssoc bool) DocumentMeta {
|
||||
if meta, cached := documentMetaCache.Load(rt); cached {
|
||||
return DocumentMeta{
|
||||
cachedDocumentMeta: meta.(cachedDocumentMeta),
|
||||
rt: rt,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
meta = cachedDocumentMeta{
|
||||
table: tableName(rt),
|
||||
index: make(map[string][]int, rt.NumField()),
|
||||
}
|
||||
)
|
||||
|
||||
// TODO probably better to use slice index instead.
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
var (
|
||||
sf = rt.Field(i)
|
||||
typ = sf.Type
|
||||
name, tagged = fieldName(sf)
|
||||
)
|
||||
|
||||
if c := sf.Name[0]; c < 'A' || c > 'Z' || name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
for typ.Kind() == reflect.Ptr || typ.Kind() == reflect.Interface || typ.Kind() == reflect.Slice {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
if typ.Kind() == reflect.Struct && isEmbedded(sf) {
|
||||
embedded := getDocumentMeta(typ, skipAssoc)
|
||||
embeddedName := ""
|
||||
if tagged {
|
||||
embeddedName = name
|
||||
}
|
||||
meta.mergeEmbedded(embedded.cachedDocumentMeta, i, embeddedName)
|
||||
continue
|
||||
}
|
||||
|
||||
meta.addFieldIndex(name, sf.Index)
|
||||
|
||||
if flag := extractFlag(typ, name); flag != Invalid {
|
||||
meta.fields = append(meta.fields, name)
|
||||
meta.flag |= flag
|
||||
continue
|
||||
}
|
||||
|
||||
if typ.Kind() != reflect.Struct {
|
||||
meta.fields = append(meta.fields, name)
|
||||
continue
|
||||
}
|
||||
|
||||
// struct without primary key is a field
|
||||
// TODO: test by scanner/valuer instead?
|
||||
if pk, _ := searchPrimary(typ); len(pk) == 0 {
|
||||
meta.fields = append(meta.fields, name)
|
||||
continue
|
||||
}
|
||||
|
||||
if !skipAssoc {
|
||||
var (
|
||||
assocMeta = getAssociationMeta(rt, sf.Index)
|
||||
)
|
||||
|
||||
switch assocMeta.typ {
|
||||
case BelongsTo:
|
||||
meta.belongsTo = append(meta.belongsTo, name)
|
||||
case HasOne:
|
||||
meta.hasOne = append(meta.hasOne, name)
|
||||
case HasMany:
|
||||
meta.hasMany = append(meta.hasMany, name)
|
||||
}
|
||||
|
||||
if assocMeta.autoload {
|
||||
meta.preload = append(meta.preload, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
primaryField, primaryIndex := searchPrimary(rt)
|
||||
meta.primaryField = append(meta.primaryField, primaryField...)
|
||||
meta.primaryIndex = append(meta.primaryIndex, primaryIndex...)
|
||||
|
||||
if !skipAssoc {
|
||||
documentMetaCache.Store(rt, meta)
|
||||
}
|
||||
|
||||
return DocumentMeta{
|
||||
rt: rt,
|
||||
cachedDocumentMeta: meta,
|
||||
}
|
||||
}
|
||||
|
||||
func extractTimeFlag(name string) DocumentFlag {
|
||||
switch name {
|
||||
case "created_at", "inserted_at":
|
||||
return HasCreatedAt
|
||||
case "updated_at":
|
||||
return HasUpdatedAt
|
||||
case "deleted_at":
|
||||
return HasDeletedAt
|
||||
}
|
||||
return Invalid
|
||||
}
|
||||
|
||||
func extractBoolFlag(name string) DocumentFlag {
|
||||
if name == "deleted" {
|
||||
return HasDeleted
|
||||
}
|
||||
return Invalid
|
||||
}
|
||||
|
||||
func extractIntFlag(name string) DocumentFlag {
|
||||
if name == "lock_version" {
|
||||
return HasVersioning
|
||||
}
|
||||
return Invalid
|
||||
}
|
||||
|
||||
func extractFlag(rt reflect.Type, name string) DocumentFlag {
|
||||
if rt == rtTime {
|
||||
return extractTimeFlag(name)
|
||||
}
|
||||
if rt == rtBool {
|
||||
return extractBoolFlag(name)
|
||||
}
|
||||
if rt == rtInt {
|
||||
return extractIntFlag(name)
|
||||
}
|
||||
return Invalid
|
||||
}
|
||||
|
||||
func fieldName(sf reflect.StructField) (string, bool) {
|
||||
if tag := sf.Tag.Get("db"); tag != "" {
|
||||
name := strings.Split(tag, ",")[0]
|
||||
|
||||
if name == "-" {
|
||||
return "", true
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
return name, true
|
||||
}
|
||||
}
|
||||
|
||||
return snaker.CamelToSnake(sf.Name), false
|
||||
}
|
||||
|
||||
func isEmbedded(sf reflect.StructField) bool {
|
||||
// anonymous structs are always embedded
|
||||
if sf.Anonymous {
|
||||
return true
|
||||
}
|
||||
if tag := sf.Tag.Get("db"); strings.HasSuffix(tag, ",embedded") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func searchPrimary(rt reflect.Type) ([]string, [][]int) {
|
||||
if result, cached := primariesCache.Load(rt); cached {
|
||||
p := result.(primaryData)
|
||||
return p.field, p.index
|
||||
}
|
||||
|
||||
var (
|
||||
field []string
|
||||
index [][]int
|
||||
fallbackIndex = -1
|
||||
)
|
||||
|
||||
if rt.Implements(rtPrimary) {
|
||||
var (
|
||||
v = reflect.Zero(rt).Interface().(primary)
|
||||
)
|
||||
|
||||
field = v.PrimaryFields()
|
||||
// index kept nil to mark interface usage
|
||||
} else {
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
sf := rt.Field(i)
|
||||
|
||||
if tag := sf.Tag.Get("db"); strings.HasSuffix(tag, ",primary") {
|
||||
index = append(index, sf.Index)
|
||||
name, _ := fieldName(sf)
|
||||
field = append(field, name)
|
||||
continue
|
||||
}
|
||||
|
||||
// check fallback for id field
|
||||
if strings.EqualFold("id", sf.Name) {
|
||||
fallbackIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(field) == 0 && fallbackIndex >= 0 {
|
||||
field = []string{"id"}
|
||||
index = [][]int{{fallbackIndex}}
|
||||
}
|
||||
|
||||
primariesCache.Store(rt, primaryData{
|
||||
field: field,
|
||||
index: index,
|
||||
})
|
||||
|
||||
return field, index
|
||||
}
|
||||
|
||||
func tableName(rt reflect.Type) string {
|
||||
var name string
|
||||
if rt.Implements(rtTable) {
|
||||
name = reflect.Zero(rt).Interface().(table).Table()
|
||||
} else {
|
||||
name = inflection.Plural(rt.Name())
|
||||
name = snaker.CamelToSnake(name)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound returned when records not found.
|
||||
ErrNotFound = NotFoundError{}
|
||||
|
||||
// ErrCheckConstraint is an auxiliary variable for error handling.
|
||||
// This is only to be used when checking error with errors.Is(err, ErrCheckConstraint).
|
||||
ErrCheckConstraint = ConstraintError{Type: CheckConstraint}
|
||||
|
||||
// ErrNotNullConstraint is an auxiliary variable for error handling.
|
||||
// This is only to be used when checking error with errors.Is(err, ErrNotNullConstraint).
|
||||
ErrNotNullConstraint = ConstraintError{Type: NotNullConstraint}
|
||||
|
||||
// ErrUniqueConstraint is an auxiliary variable for error handling.
|
||||
// This is only to be used when checking error with errors.Is(err, ErrUniqueConstraint).
|
||||
ErrUniqueConstraint = ConstraintError{Type: UniqueConstraint}
|
||||
|
||||
// ErrPrimaryKeyConstraint is an auxiliary variable for error handling.
|
||||
// This is only to be used when checking error with errors.Is(err, ErrPrimaryKeyConstraint).
|
||||
ErrPrimaryKeyConstraint = ConstraintError{Type: PrimaryKeyConstraint}
|
||||
|
||||
// ErrForeignKeyConstraint is an auxiliary variable for error handling.
|
||||
// This is only to be used when checking error with errors.Is(err, ErrForeignKeyConstraint).
|
||||
ErrForeignKeyConstraint = ConstraintError{Type: ForeignKeyConstraint}
|
||||
)
|
||||
|
||||
// NotFoundError returned whenever Find returns no result.
|
||||
type NotFoundError struct{}
|
||||
|
||||
// Error message.
|
||||
func (nfe NotFoundError) Error() string {
|
||||
return "Record not found"
|
||||
}
|
||||
|
||||
// Is returns true when target error is sql.ErrNoRows.
|
||||
func (nfe NotFoundError) Is(target error) bool {
|
||||
return errors.Is(target, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
// ConstraintType defines the type of constraint error.
|
||||
type ConstraintType int8
|
||||
|
||||
const (
|
||||
// CheckConstraint error type.
|
||||
CheckConstraint ConstraintType = iota
|
||||
// NotNullConstraint error type.1
|
||||
NotNullConstraint
|
||||
// UniqueConstraint error type.1
|
||||
UniqueConstraint
|
||||
// PrimaryKeyConstraint error type.1
|
||||
PrimaryKeyConstraint
|
||||
// ForeignKeyConstraint error type.1
|
||||
ForeignKeyConstraint
|
||||
)
|
||||
|
||||
// String representation of the constraint type.
|
||||
func (ct ConstraintType) String() string {
|
||||
switch ct {
|
||||
case CheckConstraint:
|
||||
return "CheckConstraint"
|
||||
case NotNullConstraint:
|
||||
return "NotNullConstraint"
|
||||
case UniqueConstraint:
|
||||
return "UniqueConstraint"
|
||||
case PrimaryKeyConstraint:
|
||||
return "PrimaryKeyConstraint"
|
||||
case ForeignKeyConstraint:
|
||||
return "ForeignKeyConstraint"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ConstraintError returned whenever constraint error encountered.
|
||||
type ConstraintError struct {
|
||||
Key string
|
||||
Type ConstraintType
|
||||
Err error
|
||||
}
|
||||
|
||||
// Is returns true when target error have the same type and key if defined.
|
||||
func (ce ConstraintError) Is(target error) bool {
|
||||
if err, ok := target.(ConstraintError); ok {
|
||||
return ce.Type == err.Type && (ce.Key == "" || err.Key == "" || ce.Key == err.Key)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Unwrap internal error returned by database driver.
|
||||
func (ce ConstraintError) Unwrap() error {
|
||||
return ce.Err
|
||||
}
|
||||
|
||||
// Error message.
|
||||
func (ce ConstraintError) Error() string {
|
||||
if ce.Err != nil {
|
||||
return ce.Type.String() + "Error: " + ce.Err.Error()
|
||||
}
|
||||
|
||||
return ce.Type.String() + "Error"
|
||||
}
|
@ -0,0 +1,679 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FilterOp defines enumeration of all supported filter types.
|
||||
type FilterOp int
|
||||
|
||||
func (fo FilterOp) String() string {
|
||||
return [...]string{
|
||||
"And",
|
||||
"Or",
|
||||
"Not",
|
||||
"Eq",
|
||||
"Ne",
|
||||
"Lt",
|
||||
"Lte",
|
||||
"Gt",
|
||||
"Gte",
|
||||
"Nil",
|
||||
"NotNil",
|
||||
"In",
|
||||
"Nin",
|
||||
"Like",
|
||||
"NotLike",
|
||||
"Fragment",
|
||||
}[fo]
|
||||
}
|
||||
|
||||
const (
|
||||
// FilterAndOp is filter type for and operator.
|
||||
FilterAndOp FilterOp = iota
|
||||
// FilterOrOp is filter type for or operator.
|
||||
FilterOrOp
|
||||
// FilterNotOp is filter type for not operator.
|
||||
FilterNotOp
|
||||
|
||||
// FilterEqOp is filter type for equal comparison.
|
||||
FilterEqOp
|
||||
// FilterNeOp is filter type for not equal comparison.
|
||||
FilterNeOp
|
||||
|
||||
// FilterLtOp is filter type for less than comparison.
|
||||
FilterLtOp
|
||||
// FilterLteOp is filter type for less than or equal comparison.
|
||||
FilterLteOp
|
||||
// FilterGtOp is filter type for greater than comparison.
|
||||
FilterGtOp
|
||||
// FilterGteOp is filter type for greter than or equal comparison.
|
||||
FilterGteOp
|
||||
|
||||
// FilterNilOp is filter type for nil check.
|
||||
FilterNilOp
|
||||
// FilterNotNilOp is filter type for not nil check.
|
||||
FilterNotNilOp
|
||||
|
||||
// FilterInOp is filter type for inclusion comparison.
|
||||
FilterInOp
|
||||
// FilterNinOp is filter type for not inclusion comparison.
|
||||
FilterNinOp
|
||||
|
||||
// FilterLikeOp is filter type for like comparison.
|
||||
FilterLikeOp
|
||||
// FilterNotLikeOp is filter type for not like comparison.
|
||||
FilterNotLikeOp
|
||||
|
||||
// FilterFragmentOp is filter type for custom filter.
|
||||
FilterFragmentOp
|
||||
)
|
||||
|
||||
// FilterQuery defines details of a condition type.
|
||||
type FilterQuery struct {
|
||||
Type FilterOp
|
||||
Field string
|
||||
Value interface{}
|
||||
Inner []FilterQuery
|
||||
}
|
||||
|
||||
// Build Filter query.
|
||||
func (fq FilterQuery) Build(query *Query) {
|
||||
query.WhereQuery = query.WhereQuery.And(fq)
|
||||
}
|
||||
|
||||
// None returns true if no filter is specified.
|
||||
func (fq FilterQuery) None() bool {
|
||||
return (fq.Type == FilterAndOp ||
|
||||
fq.Type == FilterOrOp ||
|
||||
fq.Type == FilterNotOp) &&
|
||||
len(fq.Inner) == 0
|
||||
}
|
||||
|
||||
func (fq FilterQuery) String() string {
|
||||
if fq.None() {
|
||||
return ""
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("where.")
|
||||
builder.WriteString(fq.Type.String())
|
||||
builder.WriteByte('(')
|
||||
|
||||
switch fq.Type {
|
||||
case FilterAndOp, FilterOrOp, FilterNotOp:
|
||||
for i := range fq.Inner {
|
||||
if i > 0 {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
|
||||
builder.WriteString(fq.Inner[i].String())
|
||||
}
|
||||
case FilterEqOp, FilterNeOp, FilterLtOp, FilterLteOp, FilterGtOp, FilterGteOp:
|
||||
builder.WriteByte('"')
|
||||
builder.WriteString(fq.Field)
|
||||
builder.WriteString("\", ")
|
||||
builder.WriteString(fmtiface(fq.Value))
|
||||
case FilterNilOp, FilterNotNilOp, FilterLikeOp, FilterNotLikeOp:
|
||||
builder.WriteByte('"')
|
||||
builder.WriteString(fq.Field)
|
||||
builder.WriteByte('"')
|
||||
case FilterInOp, FilterNinOp:
|
||||
builder.WriteByte('"')
|
||||
builder.WriteString(fq.Field)
|
||||
builder.WriteString("\", ")
|
||||
builder.WriteString(fmtifaces(fq.Value.([]interface{})))
|
||||
case FilterFragmentOp:
|
||||
v := fq.Value.([]interface{})
|
||||
builder.WriteByte('"')
|
||||
builder.WriteString(fq.Field)
|
||||
builder.WriteByte('"')
|
||||
|
||||
if len(v) > 0 {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString(fmtifaces(v))
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteByte(')')
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// And wraps filters using and.
|
||||
func (fq FilterQuery) And(filters ...FilterQuery) FilterQuery {
|
||||
if fq.None() && len(filters) == 1 {
|
||||
return filters[0]
|
||||
} else if fq.Type == FilterAndOp {
|
||||
fq.Inner = append(fq.Inner, filters...)
|
||||
return fq
|
||||
}
|
||||
|
||||
inner := append([]FilterQuery{fq}, filters...)
|
||||
return And(inner...)
|
||||
}
|
||||
|
||||
// Or wraps filters using or.
|
||||
func (fq FilterQuery) Or(filter ...FilterQuery) FilterQuery {
|
||||
if fq.None() && len(filter) == 1 {
|
||||
return filter[0]
|
||||
} else if fq.Type == FilterOrOp || fq.None() {
|
||||
fq.Type = FilterOrOp
|
||||
fq.Inner = append(fq.Inner, filter...)
|
||||
return fq
|
||||
}
|
||||
|
||||
inner := append([]FilterQuery{fq}, filter...)
|
||||
return Or(inner...)
|
||||
}
|
||||
|
||||
func (fq FilterQuery) and(other FilterQuery) FilterQuery {
|
||||
if fq.Type == FilterAndOp {
|
||||
fq.Inner = append(fq.Inner, other)
|
||||
return fq
|
||||
}
|
||||
|
||||
return And(fq, other)
|
||||
}
|
||||
|
||||
func (fq FilterQuery) or(other FilterQuery) FilterQuery {
|
||||
if fq.Type == FilterOrOp || fq.None() {
|
||||
fq.Type = FilterOrOp
|
||||
fq.Inner = append(fq.Inner, other)
|
||||
return fq
|
||||
}
|
||||
|
||||
return Or(fq, other)
|
||||
}
|
||||
|
||||
func (fq FilterQuery) applyIndex(index *Index) {
|
||||
index.Filter = fq
|
||||
}
|
||||
|
||||
// AndEq append equal expression using and.
|
||||
func (fq FilterQuery) AndEq(field string, value interface{}) FilterQuery {
|
||||
return fq.and(Eq(field, value))
|
||||
}
|
||||
|
||||
// AndNe append not equal expression using and.
|
||||
func (fq FilterQuery) AndNe(field string, value interface{}) FilterQuery {
|
||||
return fq.and(Ne(field, value))
|
||||
}
|
||||
|
||||
// AndLt append lesser than expression using and.
|
||||
func (fq FilterQuery) AndLt(field string, value interface{}) FilterQuery {
|
||||
return fq.and(Lt(field, value))
|
||||
}
|
||||
|
||||
// AndLte append lesser than or equal expression using and.
|
||||
func (fq FilterQuery) AndLte(field string, value interface{}) FilterQuery {
|
||||
return fq.and(Lte(field, value))
|
||||
}
|
||||
|
||||
// AndGt append greater than expression using and.
|
||||
func (fq FilterQuery) AndGt(field string, value interface{}) FilterQuery {
|
||||
return fq.and(Gt(field, value))
|
||||
}
|
||||
|
||||
// AndGte append greater than or equal expression using and.
|
||||
func (fq FilterQuery) AndGte(field string, value interface{}) FilterQuery {
|
||||
return fq.and(Gte(field, value))
|
||||
}
|
||||
|
||||
// AndNil append is nil expression using and.
|
||||
func (fq FilterQuery) AndNil(field string) FilterQuery {
|
||||
return fq.and(Nil(field))
|
||||
}
|
||||
|
||||
// AndNotNil append is not nil expression using and.
|
||||
func (fq FilterQuery) AndNotNil(field string) FilterQuery {
|
||||
return fq.and(NotNil(field))
|
||||
}
|
||||
|
||||
// AndIn append is in expression using and.
|
||||
func (fq FilterQuery) AndIn(field string, values ...interface{}) FilterQuery {
|
||||
return fq.and(In(field, values...))
|
||||
}
|
||||
|
||||
// AndNin append is not in expression using and.
|
||||
func (fq FilterQuery) AndNin(field string, values ...interface{}) FilterQuery {
|
||||
return fq.and(Nin(field, values...))
|
||||
}
|
||||
|
||||
// AndLike append like expression using and.
|
||||
func (fq FilterQuery) AndLike(field string, pattern string) FilterQuery {
|
||||
return fq.and(Like(field, pattern))
|
||||
}
|
||||
|
||||
// AndNotLike append not like expression using and.
|
||||
func (fq FilterQuery) AndNotLike(field string, pattern string) FilterQuery {
|
||||
return fq.and(NotLike(field, pattern))
|
||||
}
|
||||
|
||||
// AndFragment append fragment using and.
|
||||
func (fq FilterQuery) AndFragment(expr string, values ...interface{}) FilterQuery {
|
||||
return fq.and(FilterFragment(expr, values...))
|
||||
}
|
||||
|
||||
// OrEq append equal expression using or.
|
||||
func (fq FilterQuery) OrEq(field string, value interface{}) FilterQuery {
|
||||
return fq.or(Eq(field, value))
|
||||
}
|
||||
|
||||
// OrNe append not equal expression using or.
|
||||
func (fq FilterQuery) OrNe(field string, value interface{}) FilterQuery {
|
||||
return fq.or(Ne(field, value))
|
||||
}
|
||||
|
||||
// OrLt append lesser than expression using or.
|
||||
func (fq FilterQuery) OrLt(field string, value interface{}) FilterQuery {
|
||||
return fq.or(Lt(field, value))
|
||||
}
|
||||
|
||||
// OrLte append lesser than or equal expression using or.
|
||||
func (fq FilterQuery) OrLte(field string, value interface{}) FilterQuery {
|
||||
return fq.or(Lte(field, value))
|
||||
}
|
||||
|
||||
// OrGt append greater than expression using or.
|
||||
func (fq FilterQuery) OrGt(field string, value interface{}) FilterQuery {
|
||||
return fq.or(Gt(field, value))
|
||||
}
|
||||
|
||||
// OrGte append greater than or equal expression using or.
|
||||
func (fq FilterQuery) OrGte(field string, value interface{}) FilterQuery {
|
||||
return fq.or(Gte(field, value))
|
||||
}
|
||||
|
||||
// OrNil append is nil expression using or.
|
||||
func (fq FilterQuery) OrNil(field string) FilterQuery {
|
||||
return fq.or(Nil(field))
|
||||
}
|
||||
|
||||
// OrNotNil append is not nil expression using or.
|
||||
func (fq FilterQuery) OrNotNil(field string) FilterQuery {
|
||||
return fq.or(NotNil(field))
|
||||
}
|
||||
|
||||
// OrIn append is in expression using or.
|
||||
func (fq FilterQuery) OrIn(field string, values ...interface{}) FilterQuery {
|
||||
return fq.or(In(field, values...))
|
||||
}
|
||||
|
||||
// OrNin append is not in expression using or.
|
||||
func (fq FilterQuery) OrNin(field string, values ...interface{}) FilterQuery {
|
||||
return fq.or(Nin(field, values...))
|
||||
}
|
||||
|
||||
// OrLike append like expression using or.
|
||||
func (fq FilterQuery) OrLike(field string, pattern string) FilterQuery {
|
||||
return fq.or(Like(field, pattern))
|
||||
}
|
||||
|
||||
// OrNotLike append not like expression using or.
|
||||
func (fq FilterQuery) OrNotLike(field string, pattern string) FilterQuery {
|
||||
return fq.or(NotLike(field, pattern))
|
||||
}
|
||||
|
||||
// OrFragment append fragment using or.
|
||||
func (fq FilterQuery) OrFragment(expr string, values ...interface{}) FilterQuery {
|
||||
return fq.or(FilterFragment(expr, values...))
|
||||
}
|
||||
|
||||
// And compares other filters using and.
|
||||
func And(inner ...FilterQuery) FilterQuery {
|
||||
if len(inner) == 1 {
|
||||
return inner[0]
|
||||
}
|
||||
|
||||
return FilterQuery{
|
||||
Type: FilterAndOp,
|
||||
Inner: inner,
|
||||
}
|
||||
}
|
||||
|
||||
// Or compares other filters using or.
|
||||
func Or(inner ...FilterQuery) FilterQuery {
|
||||
if len(inner) == 1 {
|
||||
return inner[0]
|
||||
}
|
||||
|
||||
return FilterQuery{
|
||||
Type: FilterOrOp,
|
||||
Inner: inner,
|
||||
}
|
||||
}
|
||||
|
||||
// Not wraps filters using not.
|
||||
// It'll negate the filter type if possible.
|
||||
func Not(inner ...FilterQuery) FilterQuery {
|
||||
if len(inner) == 1 {
|
||||
fq := inner[0]
|
||||
switch fq.Type {
|
||||
case FilterEqOp:
|
||||
fq.Type = FilterNeOp
|
||||
return fq
|
||||
case FilterLtOp:
|
||||
fq.Type = FilterGteOp
|
||||
case FilterLteOp:
|
||||
fq.Type = FilterGtOp
|
||||
case FilterGtOp:
|
||||
fq.Type = FilterLteOp
|
||||
case FilterGteOp:
|
||||
fq.Type = FilterLtOp
|
||||
case FilterNilOp:
|
||||
fq.Type = FilterNotNilOp
|
||||
case FilterInOp:
|
||||
fq.Type = FilterNinOp
|
||||
case FilterLikeOp:
|
||||
fq.Type = FilterNotLikeOp
|
||||
default:
|
||||
return FilterQuery{
|
||||
Type: FilterNotOp,
|
||||
Inner: inner,
|
||||
}
|
||||
}
|
||||
|
||||
return fq
|
||||
}
|
||||
|
||||
return FilterQuery{
|
||||
Type: FilterNotOp,
|
||||
Inner: inner,
|
||||
}
|
||||
}
|
||||
|
||||
// Eq expression field equal to value.
|
||||
func Eq(field string, value interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterEqOp,
|
||||
Field: field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func lockVersion(version int) FilterQuery {
|
||||
return Eq("lock_version", version)
|
||||
}
|
||||
|
||||
// Ne compares that left value is not equal to right value.
|
||||
func Ne(field string, value interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterNeOp,
|
||||
Field: field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// Lt compares that left value is less than to right value.
|
||||
func Lt(field string, value interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterLtOp,
|
||||
Field: field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// Lte compares that left value is less than or equal to right value.
|
||||
func Lte(field string, value interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterLteOp,
|
||||
Field: field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// Gt compares that left value is greater than to right value.
|
||||
func Gt(field string, value interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterGtOp,
|
||||
Field: field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// Gte compares that left value is greater than or equal to right value.
|
||||
func Gte(field string, value interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterGteOp,
|
||||
Field: field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// Nil check whether field is nil.
|
||||
func Nil(field string) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterNilOp,
|
||||
Field: field,
|
||||
}
|
||||
}
|
||||
|
||||
// NotNil check whether field is not nil.
|
||||
func NotNil(field string) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterNotNilOp,
|
||||
Field: field,
|
||||
}
|
||||
}
|
||||
|
||||
// In check whethers value of the field is included in values.
|
||||
func In(field string, values ...interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterInOp,
|
||||
Field: field,
|
||||
Value: values,
|
||||
}
|
||||
}
|
||||
|
||||
// InInt check whethers integer values of the field is included.
|
||||
func InInt(field string, values []int) FilterQuery {
|
||||
var (
|
||||
ivalues = make([]interface{}, len(values))
|
||||
)
|
||||
|
||||
for i := range values {
|
||||
ivalues[i] = values[i]
|
||||
}
|
||||
|
||||
return In(field, ivalues...)
|
||||
}
|
||||
|
||||
// InUint check whethers unsigned integer values of the field is included.
|
||||
func InUint(field string, values []uint) FilterQuery {
|
||||
var (
|
||||
ivalues = make([]interface{}, len(values))
|
||||
)
|
||||
|
||||
for i := range values {
|
||||
ivalues[i] = values[i]
|
||||
}
|
||||
|
||||
return In(field, ivalues...)
|
||||
}
|
||||
|
||||
// InString check whethers string values of the field is included.
|
||||
func InString(field string, values []string) FilterQuery {
|
||||
var (
|
||||
ivalues = make([]interface{}, len(values))
|
||||
)
|
||||
|
||||
for i := range values {
|
||||
ivalues[i] = values[i]
|
||||
}
|
||||
|
||||
return In(field, ivalues...)
|
||||
}
|
||||
|
||||
// Nin check whethers value of the field is not included in values.
|
||||
func Nin(field string, values ...interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterNinOp,
|
||||
Field: field,
|
||||
Value: values,
|
||||
}
|
||||
}
|
||||
|
||||
// NinInt check whethers integer values of the is not included.
|
||||
func NinInt(field string, values []int) FilterQuery {
|
||||
var (
|
||||
ivalues = make([]interface{}, len(values))
|
||||
)
|
||||
|
||||
for i := range values {
|
||||
ivalues[i] = values[i]
|
||||
}
|
||||
|
||||
return Nin(field, ivalues...)
|
||||
}
|
||||
|
||||
// NinUint check whethers unsigned integer values of the is not included.
|
||||
func NinUint(field string, values []uint) FilterQuery {
|
||||
var (
|
||||
ivalues = make([]interface{}, len(values))
|
||||
)
|
||||
|
||||
for i := range values {
|
||||
ivalues[i] = values[i]
|
||||
}
|
||||
|
||||
return Nin(field, ivalues...)
|
||||
}
|
||||
|
||||
// NinString check whethers string values of the is not included.
|
||||
func NinString(field string, values []string) FilterQuery {
|
||||
var (
|
||||
ivalues = make([]interface{}, len(values))
|
||||
)
|
||||
|
||||
for i := range values {
|
||||
ivalues[i] = values[i]
|
||||
}
|
||||
|
||||
return Nin(field, ivalues...)
|
||||
}
|
||||
|
||||
// Like compares value of field to match string pattern.
|
||||
func Like(field string, pattern string) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterLikeOp,
|
||||
Field: field,
|
||||
Value: pattern,
|
||||
}
|
||||
}
|
||||
|
||||
// NotLike compares value of field to not match string pattern.
|
||||
func NotLike(field string, pattern string) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterNotLikeOp,
|
||||
Field: field,
|
||||
Value: pattern,
|
||||
}
|
||||
}
|
||||
|
||||
// FilterFragment add custom filter.
|
||||
func FilterFragment(expr string, values ...interface{}) FilterQuery {
|
||||
return FilterQuery{
|
||||
Type: FilterFragmentOp,
|
||||
Field: expr,
|
||||
Value: values,
|
||||
}
|
||||
}
|
||||
|
||||
func filterDocument(doc *Document) FilterQuery {
|
||||
var (
|
||||
pFields = doc.PrimaryFields()
|
||||
pValues = doc.PrimaryValues()
|
||||
)
|
||||
|
||||
return filterDocumentPrimary(pFields, pValues, FilterEqOp)
|
||||
}
|
||||
|
||||
func filterDocumentPrimary(pFields []string, pValues []interface{}, op FilterOp) FilterQuery {
|
||||
var filter FilterQuery
|
||||
|
||||
for i := range pFields {
|
||||
filter = filter.And(FilterQuery{
|
||||
Type: op,
|
||||
Field: pFields[i],
|
||||
Value: pValues[i],
|
||||
})
|
||||
}
|
||||
|
||||
return filter
|
||||
|
||||
}
|
||||
|
||||
func filterCollection(col *Collection) FilterQuery {
|
||||
var (
|
||||
pFields = col.PrimaryFields()
|
||||
pValues = col.PrimaryValues()
|
||||
length = col.Len()
|
||||
)
|
||||
|
||||
return filterCollectionPrimary(pFields, pValues, length)
|
||||
}
|
||||
|
||||
func filterCollectionPrimary(pFields []string, pValues []interface{}, length int) FilterQuery {
|
||||
var filter FilterQuery
|
||||
|
||||
if len(pFields) == 1 {
|
||||
filter = In(pFields[0], pValues[0].([]interface{})...)
|
||||
} else {
|
||||
var (
|
||||
andFilters = make([]FilterQuery, length)
|
||||
)
|
||||
|
||||
for i := range pValues {
|
||||
var (
|
||||
values = pValues[i].([]interface{})
|
||||
)
|
||||
|
||||
for j := range values {
|
||||
andFilters[j] = andFilters[j].AndEq(pFields[i], values[j])
|
||||
}
|
||||
}
|
||||
|
||||
filter = Or(andFilters...)
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
func filterBelongsTo(assoc Association) (FilterQuery, error) {
|
||||
var (
|
||||
rValue = assoc.ReferenceValue()
|
||||
fValue = assoc.ForeignValue()
|
||||
filter = Eq(assoc.ForeignField(), fValue)
|
||||
)
|
||||
|
||||
if rValue != fValue {
|
||||
return filter, ConstraintError{
|
||||
Key: assoc.ReferenceField(),
|
||||
Type: ForeignKeyConstraint,
|
||||
Err: errors.New("rel: inconsistent belongs to ref and fk"),
|
||||
}
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func filterHasOne(assoc Association, asssocDoc *Document) (FilterQuery, error) {
|
||||
var (
|
||||
fField = assoc.ForeignField()
|
||||
fValue = assoc.ForeignValue()
|
||||
rValue = assoc.ReferenceValue()
|
||||
filter = filterDocument(asssocDoc).AndEq(fField, rValue)
|
||||
)
|
||||
|
||||
if rValue != fValue {
|
||||
return filter, ConstraintError{
|
||||
Key: fField,
|
||||
Type: ForeignKeyConstraint,
|
||||
Err: errors.New("rel: inconsistent has one ref and fk"),
|
||||
}
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package rel
|
||||
|
||||
// GroupQuery defines group clause of the query.
|
||||
type GroupQuery struct {
|
||||
Fields []string
|
||||
Filter FilterQuery
|
||||
}
|
||||
|
||||
// Build query.
|
||||
func (gq GroupQuery) Build(query *Query) {
|
||||
query.GroupQuery = gq
|
||||
}
|
||||
|
||||
// Having appends filter for group query with and operand.
|
||||
func (gq GroupQuery) Having(filters ...FilterQuery) GroupQuery {
|
||||
gq.Filter = gq.Filter.And(filters...)
|
||||
return gq
|
||||
}
|
||||
|
||||
// OrHaving appends filter for group query with or operand.
|
||||
func (gq GroupQuery) OrHaving(filters ...FilterQuery) GroupQuery {
|
||||
gq.Filter = gq.Filter.Or(And(filters...))
|
||||
return gq
|
||||
}
|
||||
|
||||
// Where is alias for having.
|
||||
func (gq GroupQuery) Where(filters ...FilterQuery) GroupQuery {
|
||||
return gq.Having(filters...)
|
||||
}
|
||||
|
||||
// OrWhere is alias for OrHaving.
|
||||
func (gq GroupQuery) OrWhere(filters ...FilterQuery) GroupQuery {
|
||||
return gq.OrHaving(filters...)
|
||||
}
|
||||
|
||||
// NewGroup query.
|
||||
func NewGroup(fields ...string) GroupQuery {
|
||||
return GroupQuery{
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package rel
|
||||
|
||||
// Index definition.
|
||||
type Index struct {
|
||||
Op SchemaOp
|
||||
Table string
|
||||
Name string
|
||||
Unique bool
|
||||
Columns []string
|
||||
Optional bool
|
||||
Filter FilterQuery
|
||||
Options string
|
||||
}
|
||||
|
||||
func (i Index) description() string {
|
||||
return i.Op.String() + " index " + i.Name + " on " + i.Table
|
||||
}
|
||||
|
||||
func (Index) internalMigration() {}
|
||||
|
||||
func createIndex(table string, name string, columns []string, options []IndexOption) Index {
|
||||
index := Index{
|
||||
Op: SchemaCreate,
|
||||
Table: table,
|
||||
Name: name,
|
||||
Columns: columns,
|
||||
}
|
||||
|
||||
applyIndexOptions(&index, options)
|
||||
return index
|
||||
}
|
||||
|
||||
func createUniqueIndex(table string, name string, columns []string, options []IndexOption) Index {
|
||||
index := createIndex(table, name, columns, options)
|
||||
index.Unique = true
|
||||
return index
|
||||
}
|
||||
|
||||
func dropIndex(table string, name string, options []IndexOption) Index {
|
||||
index := Index{
|
||||
Op: SchemaDrop,
|
||||
Table: table,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
applyIndexOptions(&index, options)
|
||||
return index
|
||||
}
|
||||
|
||||
// IndexOption interface.
|
||||
// Available options are: Comment, Options.
|
||||
type IndexOption interface {
|
||||
applyIndex(index *Index)
|
||||
}
|
||||
|
||||
func applyIndexOptions(index *Index, options []IndexOption) {
|
||||
for i := range options {
|
||||
options[i].applyIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
// Name option for defining custom index name.
|
||||
type Name string
|
||||
|
||||
func (n Name) applyKey(key *Key) {
|
||||
key.Name = string(n)
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Instrumenter defines function type that can be used for instrumetation.
|
||||
// This function should return a function with no argument as a callback for finished execution.
|
||||
type Instrumenter func(ctx context.Context, op string, message string) func(err error)
|
||||
|
||||
// Observe operation.
|
||||
func (i Instrumenter) Observe(ctx context.Context, op string, message string) func(err error) {
|
||||
if i != nil {
|
||||
return i(ctx, op, message)
|
||||
}
|
||||
|
||||
return func(err error) {}
|
||||
}
|
||||
|
||||
// DefaultLogger instrumentation to log queries and rel operation.
|
||||
func DefaultLogger(ctx context.Context, op string, message string) func(err error) {
|
||||
// no op for rel functions.
|
||||
if strings.HasPrefix(op, "rel-") {
|
||||
return func(error) {}
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
|
||||
return func(err error) {
|
||||
duration := time.Since(t)
|
||||
if err != nil {
|
||||
log.Print("[duration: ", duration, " op: ", op, "] ", message, " - ", err)
|
||||
} else {
|
||||
log.Print("[duration: ", duration, " op: ", op, "] ", message)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Iterator allows iterating through all record in database in batch.
|
||||
type Iterator interface {
|
||||
io.Closer
|
||||
Next(record interface{}) error
|
||||
}
|
||||
|
||||
// IteratorOption is used to configure iteration behaviour, such as batch size, start id and finish id.
|
||||
type IteratorOption interface {
|
||||
apply(*iterator)
|
||||
}
|
||||
|
||||
type batchSize int
|
||||
|
||||
func (bs batchSize) apply(i *iterator) {
|
||||
i.batchSize = int(bs)
|
||||
}
|
||||
|
||||
// String representation.
|
||||
func (bs batchSize) String() string {
|
||||
return fmt.Sprintf("rel.BatchSize(%d)", bs)
|
||||
}
|
||||
|
||||
// BatchSize specifies the size of iterator batch. Defaults to 1000.
|
||||
func BatchSize(size int) IteratorOption {
|
||||
return batchSize(size)
|
||||
}
|
||||
|
||||
type start []interface{}
|
||||
|
||||
func (s start) apply(i *iterator) {
|
||||
i.start = s
|
||||
}
|
||||
|
||||
// String representation.
|
||||
func (s start) String() string {
|
||||
return fmt.Sprintf("rel.Start(%s)", fmtifaces(s))
|
||||
}
|
||||
|
||||
// Start specifies the primary value to start from (inclusive).
|
||||
func Start(id ...interface{}) IteratorOption {
|
||||
return start(id)
|
||||
}
|
||||
|
||||
type finish []interface{}
|
||||
|
||||
func (f finish) apply(i *iterator) {
|
||||
i.finish = f
|
||||
}
|
||||
|
||||
// String representation.
|
||||
func (f finish) String() string {
|
||||
return fmt.Sprintf("rel.Finish(%s)", fmtifaces(f))
|
||||
}
|
||||
|
||||
// Finish specifies the primary value to finish at (inclusive).
|
||||
func Finish(id ...interface{}) IteratorOption {
|
||||
return finish(id)
|
||||
}
|
||||
|
||||
type iterator struct {
|
||||
ctx context.Context
|
||||
start []interface{}
|
||||
finish []interface{}
|
||||
batchSize int
|
||||
current int
|
||||
query Query
|
||||
adapter Adapter
|
||||
cursor Cursor
|
||||
fields []string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (i *iterator) Close() error {
|
||||
if !i.closed && i.cursor != nil {
|
||||
i.closed = true
|
||||
return i.cursor.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *iterator) Next(record interface{}) error {
|
||||
if i.current%i.batchSize == 0 {
|
||||
if err := i.fetch(i.ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !i.cursor.Next() {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
var (
|
||||
doc = NewDocument(record)
|
||||
scanners = doc.Scanners(i.fields)
|
||||
)
|
||||
|
||||
i.current++
|
||||
return i.cursor.Scan(scanners...)
|
||||
}
|
||||
|
||||
func (i *iterator) fetch(ctx context.Context, record interface{}) error {
|
||||
if i.current == 0 {
|
||||
i.init(record)
|
||||
} else {
|
||||
i.cursor.Close()
|
||||
}
|
||||
|
||||
i.query = i.query.Limit(i.batchSize).Offset(i.current)
|
||||
|
||||
cursor, err := i.adapter.Query(ctx, i.query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields, err := cursor.Fields()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.cursor = cursor
|
||||
i.fields = fields
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *iterator) init(record interface{}) {
|
||||
var (
|
||||
doc = NewDocument(record)
|
||||
)
|
||||
|
||||
if i.query.Table == "" {
|
||||
i.query.Table = doc.Table()
|
||||
}
|
||||
|
||||
if len(i.start) > 0 {
|
||||
i.query = i.query.Where(filterDocumentPrimary(doc.PrimaryFields(), i.start, FilterGteOp))
|
||||
}
|
||||
|
||||
if len(i.finish) > 0 {
|
||||
i.query = i.query.Where(filterDocumentPrimary(doc.PrimaryFields(), i.finish, FilterLteOp))
|
||||
}
|
||||
|
||||
i.query = i.query.SortAsc(doc.PrimaryFields()...)
|
||||
}
|
||||
|
||||
func newIterator(ctx context.Context, adapter Adapter, query Query, options []IteratorOption) Iterator {
|
||||
it := &iterator{
|
||||
ctx: ctx,
|
||||
batchSize: 1000,
|
||||
query: query,
|
||||
adapter: adapter,
|
||||
}
|
||||
|
||||
for i := range options {
|
||||
options[i].apply(it)
|
||||
}
|
||||
|
||||
return it
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package rel
|
||||
|
||||
// JoinQuery defines join clause in query.
|
||||
type JoinQuery struct {
|
||||
Mode string
|
||||
Table string
|
||||
From string
|
||||
To string
|
||||
Assoc string
|
||||
Filter FilterQuery
|
||||
Arguments []interface{}
|
||||
}
|
||||
|
||||
// Build query.
|
||||
func (jq JoinQuery) Build(query *Query) {
|
||||
query.JoinQuery = append(query.JoinQuery, jq)
|
||||
|
||||
if jq.Assoc != "" {
|
||||
query.AddPopulator(&query.JoinQuery[len(query.JoinQuery)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func (jq *JoinQuery) Populate(query *Query, docMeta DocumentMeta) {
|
||||
var (
|
||||
assocMeta = docMeta.Association(jq.Assoc)
|
||||
assocDocMeta = assocMeta.DocumentMeta()
|
||||
)
|
||||
|
||||
jq.Table = assocDocMeta.Table() + " as " + jq.Assoc
|
||||
jq.To = jq.Assoc + "." + assocMeta.ForeignField()
|
||||
jq.From = docMeta.Table() + "." + assocMeta.ReferenceField()
|
||||
|
||||
// load association if defined and supported
|
||||
if assocMeta.Type() == HasOne || assocMeta.Type() == BelongsTo {
|
||||
var (
|
||||
load = false
|
||||
selectField = jq.Assoc + ".*"
|
||||
)
|
||||
|
||||
for i := range query.SelectQuery.Fields {
|
||||
if load && i > 0 {
|
||||
query.SelectQuery.Fields[i-1] = query.SelectQuery.Fields[i]
|
||||
}
|
||||
if query.SelectQuery.Fields[i] == selectField {
|
||||
load = true
|
||||
}
|
||||
}
|
||||
|
||||
if load {
|
||||
fields := make([]string, len(assocDocMeta.Fields()))
|
||||
for i, f := range assocDocMeta.Fields() {
|
||||
fields[i] = jq.Assoc + "." + f + " as " + jq.Assoc + "." + f
|
||||
}
|
||||
query.SelectQuery.Fields = append(query.SelectQuery.Fields[:(len(query.SelectQuery.Fields)-1)], fields...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewJoinWith query with custom join mode, table, field and additional filters with AND condition.
|
||||
func NewJoinWith(mode string, table string, from string, to string, filter ...FilterQuery) JoinQuery {
|
||||
return JoinQuery{
|
||||
Mode: mode,
|
||||
Table: table,
|
||||
From: from,
|
||||
To: to,
|
||||
Filter: And(filter...),
|
||||
}
|
||||
}
|
||||
|
||||
// NewJoinFragment defines a join clause using raw query.
|
||||
func NewJoinFragment(expr string, args ...interface{}) JoinQuery {
|
||||
if args == nil {
|
||||
// prevent buildJoin to populate From and To variable.
|
||||
args = []interface{}{}
|
||||
}
|
||||
|
||||
return JoinQuery{
|
||||
Mode: expr,
|
||||
Arguments: args,
|
||||
}
|
||||
}
|
||||
|
||||
// NewJoin with given table.
|
||||
func NewJoin(table string, filter ...FilterQuery) JoinQuery {
|
||||
return NewJoinWith("JOIN", table, "", "", filter...)
|
||||
}
|
||||
|
||||
// NewJoinOn table with given field and optional additional filter.
|
||||
func NewJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
|
||||
return NewJoinWith("JOIN", table, from, to, filter...)
|
||||
}
|
||||
|
||||
// NewInnerJoin with given table and optional filter.
|
||||
func NewInnerJoin(table string, filter ...FilterQuery) JoinQuery {
|
||||
return NewInnerJoinOn(table, "", "", filter...)
|
||||
}
|
||||
|
||||
// NewInnerJoinOn table with given field and optional additional filter.
|
||||
func NewInnerJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
|
||||
return NewJoinWith("INNER JOIN", table, from, to, filter...)
|
||||
}
|
||||
|
||||
// NewLeftJoin with given table and optional filter.
|
||||
func NewLeftJoin(table string, filter ...FilterQuery) JoinQuery {
|
||||
return NewLeftJoinOn(table, "", "", filter...)
|
||||
}
|
||||
|
||||
// NewLeftJoinOn table with given field and optional additional filter.
|
||||
func NewLeftJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
|
||||
return NewJoinWith("LEFT JOIN", table, from, to, filter...)
|
||||
}
|
||||
|
||||
// NewRightJoin with given table and optional filter.
|
||||
func NewRightJoin(table string, filter ...FilterQuery) JoinQuery {
|
||||
return NewRightJoinOn(table, "", "", filter...)
|
||||
}
|
||||
|
||||
// NewRightJoinOn table with given field and optional additional filter.
|
||||
func NewRightJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
|
||||
return NewJoinWith("RIGHT JOIN", table, from, to, filter...)
|
||||
}
|
||||
|
||||
// NewFullJoin with given table and optional filter.
|
||||
func NewFullJoin(table string, filter ...FilterQuery) JoinQuery {
|
||||
return NewFullJoinOn(table, "", "", filter...)
|
||||
}
|
||||
|
||||
// NewFullJoinOn table with given field and optional additional filter.
|
||||
func NewFullJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
|
||||
return NewJoinWith("FULL JOIN", table, from, to, filter...)
|
||||
}
|
||||
|
||||
// NewJoinAssocWith with given association field and optional additional filters.
|
||||
func NewJoinAssocWith(mode string, assoc string, filter ...FilterQuery) JoinQuery {
|
||||
return JoinQuery{
|
||||
Mode: mode,
|
||||
Assoc: assoc,
|
||||
Filter: And(filter...),
|
||||
}
|
||||
}
|
||||
|
||||
// NewJoinAssoc with given association field and optional additional filters.
|
||||
func NewJoinAssoc(assoc string, filter ...FilterQuery) JoinQuery {
|
||||
return NewJoinAssocWith("JOIN", assoc, filter...)
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package rel
|
||||
|
||||
// KeyType definition.
|
||||
type KeyType string
|
||||
|
||||
const (
|
||||
// PrimaryKey KeyType.
|
||||
PrimaryKey KeyType = "PRIMARY KEY"
|
||||
// ForeignKey KeyType.
|
||||
ForeignKey KeyType = "FOREIGN KEY"
|
||||
// UniqueKey KeyType.
|
||||
UniqueKey = "UNIQUE"
|
||||
)
|
||||
|
||||
// ForeignKeyReference definition.
|
||||
type ForeignKeyReference struct {
|
||||
Table string
|
||||
Columns []string
|
||||
OnDelete string
|
||||
OnUpdate string
|
||||
}
|
||||
|
||||
// Key definition.
|
||||
type Key struct {
|
||||
Op SchemaOp
|
||||
Name string
|
||||
Type KeyType
|
||||
Columns []string
|
||||
Rename string
|
||||
Reference ForeignKeyReference
|
||||
Options string
|
||||
}
|
||||
|
||||
func (Key) internalTableDefinition() {}
|
||||
|
||||
func createKeys(columns []string, typ KeyType, options []KeyOption) Key {
|
||||
key := Key{
|
||||
Op: SchemaCreate,
|
||||
Columns: columns,
|
||||
Type: typ,
|
||||
}
|
||||
|
||||
applyKeyOptions(&key, options)
|
||||
return key
|
||||
}
|
||||
|
||||
func createPrimaryKeys(columns []string, options []KeyOption) Key {
|
||||
return createKeys(columns, PrimaryKey, options)
|
||||
}
|
||||
|
||||
func createForeignKey(column string, refTable string, refColumn string, options []KeyOption) Key {
|
||||
key := Key{
|
||||
Op: SchemaCreate,
|
||||
Type: ForeignKey,
|
||||
Columns: []string{column},
|
||||
Reference: ForeignKeyReference{
|
||||
Table: refTable,
|
||||
Columns: []string{refColumn},
|
||||
},
|
||||
}
|
||||
|
||||
applyKeyOptions(&key, options)
|
||||
return key
|
||||
}
|
||||
|
||||
// TODO: Rename and Drop, PR welcomed.
|
@ -0,0 +1,162 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Map can be used as mutation for repository insert or update operation.
|
||||
// This allows inserting or updating only on specified field.
|
||||
// Insert/Update of has one or belongs to can be done using other Map as a value.
|
||||
// Insert/Update of has many can be done using slice of Map as a value.
|
||||
// Map is intended to be used internally within application, and not to be exposed directly as an APIs.
|
||||
type Map map[string]interface{}
|
||||
|
||||
// Apply mutation.
|
||||
func (m Map) Apply(doc *Document, mutation *Mutation) {
|
||||
var (
|
||||
pField = doc.PrimaryField()
|
||||
pValue = doc.PrimaryValue()
|
||||
)
|
||||
|
||||
for field, value := range m {
|
||||
switch v := value.(type) {
|
||||
case Map:
|
||||
if !mutation.Cascade {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
assoc = doc.Association(field)
|
||||
)
|
||||
|
||||
if assoc.Type() != HasOne && assoc.Type() != BelongsTo {
|
||||
panic(fmt.Sprint("rel: cannot associate has many", v, "as", field, "into", doc.Table()))
|
||||
}
|
||||
|
||||
var (
|
||||
assocDoc, _ = assoc.Document()
|
||||
assocMutation = Apply(assocDoc, v)
|
||||
)
|
||||
|
||||
mutation.SetAssoc(field, assocMutation)
|
||||
case []Map:
|
||||
if !mutation.Cascade {
|
||||
continue
|
||||
}
|
||||
var (
|
||||
assoc = doc.Association(field)
|
||||
muts, deletedIDs = applyMaps(v, assoc)
|
||||
)
|
||||
|
||||
mutation.SetAssoc(field, muts...)
|
||||
mutation.SetDeletedIDs(field, deletedIDs)
|
||||
default:
|
||||
if field == pField {
|
||||
if v != pValue {
|
||||
panic(fmt.Sprint("rel: replacing primary value (", pValue, " become ", v, ") is not allowed"))
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !doc.SetValue(field, v) {
|
||||
panic(fmt.Sprint("rel: cannot assign ", v, " as ", field, " into ", doc.Table()))
|
||||
}
|
||||
|
||||
mutation.Add(Set(field, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Map) String() string {
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString("rel.Map{")
|
||||
for k, v := range m {
|
||||
if builder.Len() > len("rel.Map{") {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
builder.WriteByte('"')
|
||||
builder.WriteString(k)
|
||||
builder.WriteString("\": ")
|
||||
|
||||
switch im := v.(type) {
|
||||
case Map:
|
||||
builder.WriteString(im.String())
|
||||
case []Map:
|
||||
builder.WriteString("[]rel.Map{")
|
||||
for i := range im {
|
||||
if i > 0 {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
builder.WriteString(im[i].String())
|
||||
}
|
||||
builder.WriteString("}")
|
||||
default:
|
||||
builder.WriteString(fmtiface(v)) // TODO: use compact struct print (reltest.csprint)
|
||||
}
|
||||
}
|
||||
builder.WriteString("}")
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func applyMaps(maps []Map, assoc Association) ([]Mutation, []interface{}) {
|
||||
var (
|
||||
deletedIDs []interface{}
|
||||
muts = make([]Mutation, len(maps))
|
||||
col, _ = assoc.Collection()
|
||||
)
|
||||
|
||||
var (
|
||||
pField = col.PrimaryField()
|
||||
pIndex = make(map[interface{}]int)
|
||||
pValues = col.PrimaryValue().([]interface{})
|
||||
)
|
||||
|
||||
for i, v := range pValues {
|
||||
pIndex[v] = i
|
||||
}
|
||||
|
||||
var (
|
||||
curr = 0
|
||||
inserts []Map
|
||||
)
|
||||
|
||||
for _, m := range maps {
|
||||
if pChange, changed := m[pField]; changed {
|
||||
// update
|
||||
pID, ok := pIndex[pChange]
|
||||
if !ok {
|
||||
panic("rel: cannot update has many assoc that is not loaded or doesn't belong to this record")
|
||||
}
|
||||
|
||||
if pID != curr {
|
||||
col.Swap(pID, curr)
|
||||
pValues[pID], pValues[curr] = pValues[curr], pValues[pID]
|
||||
}
|
||||
|
||||
muts[curr] = Apply(col.Get(curr), m)
|
||||
delete(pIndex, pChange)
|
||||
curr++
|
||||
} else {
|
||||
inserts = append(inserts, m)
|
||||
}
|
||||
}
|
||||
|
||||
// delete stales
|
||||
if curr < col.Len() {
|
||||
deletedIDs = pValues[curr:]
|
||||
col.Truncate(0, curr)
|
||||
} else {
|
||||
deletedIDs = []interface{}{}
|
||||
}
|
||||
|
||||
// inserts remaining
|
||||
for i, m := range inserts {
|
||||
muts[curr+i] = Apply(col.Add(), m)
|
||||
}
|
||||
|
||||
return muts, deletedIDs
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Mutator is interface for a record mutator.
|
||||
type Mutator interface {
|
||||
Apply(doc *Document, mutation *Mutation)
|
||||
}
|
||||
|
||||
// Apply using given mutators.
|
||||
func Apply(doc *Document, mutators ...Mutator) Mutation {
|
||||
return applyMutators(doc, true, true, mutators...)
|
||||
}
|
||||
|
||||
// apply given mutators with customized default values
|
||||
func applyMutators(doc *Document, cascade, applyStructset bool, mutators ...Mutator) Mutation {
|
||||
var (
|
||||
optionsCount int
|
||||
mutation = Mutation{
|
||||
Unscoped: false,
|
||||
Reload: false,
|
||||
Cascade: Cascade(cascade),
|
||||
}
|
||||
)
|
||||
|
||||
for i := range mutators {
|
||||
switch mut := mutators[i].(type) {
|
||||
case Unscoped, Reload, Cascade, OnConflict:
|
||||
optionsCount++
|
||||
mut.Apply(doc, &mutation)
|
||||
default:
|
||||
mut.Apply(doc, &mutation)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to structset.
|
||||
if applyStructset && optionsCount == len(mutators) {
|
||||
newStructset(doc, false).Apply(doc, &mutation)
|
||||
}
|
||||
|
||||
return mutation
|
||||
}
|
||||
|
||||
// AssocMutation represents mutation for association.
|
||||
type AssocMutation struct {
|
||||
Mutations []Mutation
|
||||
DeletedIDs []interface{} // This is array of single id, and doesn't support composite primary key.
|
||||
}
|
||||
|
||||
// Mutation represents value to be inserted or updated to database.
|
||||
// It's not safe to be used multiple time. some operation my alter mutation data.
|
||||
type Mutation struct {
|
||||
Mutates map[string]Mutate
|
||||
Assoc map[string]AssocMutation
|
||||
OnConflict OnConflict
|
||||
Unscoped Unscoped
|
||||
Reload Reload
|
||||
Cascade Cascade
|
||||
ErrorFunc ErrorFunc
|
||||
}
|
||||
|
||||
func (m *Mutation) initMutates() {
|
||||
if m.Mutates == nil {
|
||||
m.Mutates = make(map[string]Mutate)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mutation) initAssoc() {
|
||||
if m.Assoc == nil {
|
||||
m.Assoc = make(map[string]AssocMutation)
|
||||
}
|
||||
}
|
||||
|
||||
// IsEmpty returns true if no mutates operation and assoc's mutation is defined.
|
||||
func (m *Mutation) IsEmpty() bool {
|
||||
return m.IsMutatesEmpty() && m.IsAssocEmpty()
|
||||
}
|
||||
|
||||
// IsMutatesEmpty returns true if no mutates operation is defined.
|
||||
func (m *Mutation) IsMutatesEmpty() bool {
|
||||
return len(m.Mutates) == 0
|
||||
}
|
||||
|
||||
// IsAssocEmpty returns true if no assoc's mutation is defined.
|
||||
func (m *Mutation) IsAssocEmpty() bool {
|
||||
return len(m.Assoc) == 0
|
||||
}
|
||||
|
||||
// Add a mutate.
|
||||
func (m *Mutation) Add(mut Mutate) {
|
||||
m.initMutates()
|
||||
|
||||
m.Mutates[mut.Field] = mut
|
||||
}
|
||||
|
||||
// SetAssoc mutation.
|
||||
func (m *Mutation) SetAssoc(field string, muts ...Mutation) {
|
||||
m.initAssoc()
|
||||
|
||||
assoc := m.Assoc[field]
|
||||
assoc.Mutations = muts
|
||||
m.Assoc[field] = assoc
|
||||
}
|
||||
|
||||
// SetDeletedIDs mutation.
|
||||
// nil slice will clear association.
|
||||
func (m *Mutation) SetDeletedIDs(field string, ids []interface{}) {
|
||||
m.initAssoc()
|
||||
|
||||
assoc := m.Assoc[field]
|
||||
assoc.DeletedIDs = ids
|
||||
m.Assoc[field] = assoc
|
||||
}
|
||||
|
||||
// ChangeOp represents type of mutate operation.
|
||||
type ChangeOp int
|
||||
|
||||
const (
|
||||
// ChangeInvalidOp operation.
|
||||
ChangeInvalidOp ChangeOp = iota
|
||||
// ChangeSetOp operation.
|
||||
ChangeSetOp
|
||||
// ChangeIncOp operation.
|
||||
ChangeIncOp
|
||||
// ChangeFragmentOp operation.
|
||||
ChangeFragmentOp
|
||||
)
|
||||
|
||||
// Mutate stores mutation instruction.
|
||||
type Mutate struct {
|
||||
Type ChangeOp
|
||||
Field string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Apply mutation.
|
||||
func (m Mutate) Apply(doc *Document, mutation *Mutation) {
|
||||
invalid := false
|
||||
|
||||
switch m.Type {
|
||||
case ChangeSetOp:
|
||||
if !doc.SetValue(m.Field, m.Value) {
|
||||
invalid = true
|
||||
}
|
||||
case ChangeFragmentOp:
|
||||
mutation.Reload = true
|
||||
default:
|
||||
if typ, ok := doc.Type(m.Field); ok {
|
||||
kind := typ.Kind()
|
||||
invalid = m.Type == ChangeIncOp && (kind < reflect.Int || kind > reflect.Uint64)
|
||||
} else {
|
||||
invalid = true
|
||||
}
|
||||
mutation.Reload = true
|
||||
}
|
||||
|
||||
if invalid {
|
||||
panic(fmt.Sprint("rel: cannot assign ", m.Value, " as ", m.Field, " into ", doc.Table()))
|
||||
}
|
||||
|
||||
mutation.Add(m)
|
||||
}
|
||||
|
||||
// String representation
|
||||
func (m Mutate) String() string {
|
||||
str := "≤Invalid Mutator>"
|
||||
switch m.Type {
|
||||
case ChangeSetOp:
|
||||
str = fmt.Sprintf("rel.Set(\"%s\", %s)", m.Field, fmtiface(m.Value))
|
||||
case ChangeIncOp:
|
||||
str = fmt.Sprintf("rel.IncBy(\"%s\", %s)", m.Field, fmtiface(m.Value))
|
||||
case ChangeFragmentOp:
|
||||
str = fmt.Sprintf("rel.SetFragment(\"%s\", %s)", m.Field, fmtifaces(m.Value.([]interface{})))
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// Set create a mutate using set operation.
|
||||
func Set(field string, value interface{}) Mutate {
|
||||
return Mutate{
|
||||
Type: ChangeSetOp,
|
||||
Field: field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// Inc create a mutate using increment operation.
|
||||
func Inc(field string) Mutate {
|
||||
return IncBy(field, 1)
|
||||
}
|
||||
|
||||
// IncBy create a mutate using increment operation with custom increment value.
|
||||
func IncBy(field string, n int) Mutate {
|
||||
return Mutate{
|
||||
Type: ChangeIncOp,
|
||||
Field: field,
|
||||
Value: n,
|
||||
}
|
||||
}
|
||||
|
||||
// Dec create a mutate using deccrement operation.
|
||||
func Dec(field string) Mutate {
|
||||
return DecBy(field, 1)
|
||||
}
|
||||
|
||||
// DecBy create a mutate using decrement operation with custom decrement value.
|
||||
func DecBy(field string, n int) Mutate {
|
||||
return Mutate{
|
||||
Type: ChangeIncOp,
|
||||
Field: field,
|
||||
Value: -n,
|
||||
}
|
||||
}
|
||||
|
||||
// SetFragment create a mutate operation using fragment operation.
|
||||
// Only available for Update.
|
||||
func SetFragment(raw string, args ...interface{}) Mutate {
|
||||
return Mutate{
|
||||
Type: ChangeFragmentOp,
|
||||
Field: raw,
|
||||
Value: args,
|
||||
}
|
||||
}
|
||||
|
||||
// Setf is an alias for SetFragment
|
||||
var Setf = SetFragment
|
||||
|
||||
// Reload force reload after insert/update.
|
||||
// Default to false.
|
||||
type Reload bool
|
||||
|
||||
// Apply mutation.
|
||||
func (r Reload) Apply(doc *Document, mutation *Mutation) {
|
||||
mutation.Reload = r
|
||||
}
|
||||
|
||||
// Build query.
|
||||
func (r Reload) Build(query *Query) {
|
||||
query.ReloadQuery = r
|
||||
}
|
||||
|
||||
// Cascade enable or disable updating associations.
|
||||
// Default to true.
|
||||
type Cascade bool
|
||||
|
||||
// Build query.
|
||||
func (c Cascade) Build(query *Query) {
|
||||
query.CascadeQuery = c
|
||||
}
|
||||
|
||||
// Apply mutation.
|
||||
func (c Cascade) Apply(doc *Document, mutation *Mutation) {
|
||||
mutation.Cascade = c
|
||||
}
|
||||
|
||||
func (c Cascade) String() string {
|
||||
return fmt.Sprintf("rel.Cascade(%t)", c)
|
||||
}
|
||||
|
||||
// ErrorFunc allows conversion REL's error to Application custom errors.
|
||||
type ErrorFunc func(error) error
|
||||
|
||||
// Apply mutation.
|
||||
func (ef ErrorFunc) Apply(doc *Document, mutation *Mutation) {
|
||||
mutation.ErrorFunc = ef
|
||||
}
|
||||
|
||||
func (ef ErrorFunc) transform(err error) error {
|
||||
if ef != nil && err != nil {
|
||||
return ef(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type nullable struct {
|
||||
dest interface{}
|
||||
}
|
||||
|
||||
var _ sql.Scanner = (*nullable)(nil)
|
||||
|
||||
func (n nullable) Scan(src interface{}) error {
|
||||
return convertAssign(n.dest, src)
|
||||
}
|
||||
|
||||
// Nullable wrap value as a nullable sql.Scanner.
|
||||
// If value returned from database is nil, nullable scanner will set dest to zero value.
|
||||
func Nullable(dest interface{}) interface{} {
|
||||
if s, ok := dest.(sql.Scanner); ok {
|
||||
return s
|
||||
}
|
||||
|
||||
rt := reflect.TypeOf(dest)
|
||||
if rt.Kind() != reflect.Ptr {
|
||||
panic("rel: destination must be a pointer")
|
||||
}
|
||||
|
||||
if rt.Elem().Kind() == reflect.Ptr {
|
||||
return dest
|
||||
}
|
||||
|
||||
return nullable{
|
||||
dest: dest,
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package rel
|
||||
|
||||
// OnConflict mutation.
|
||||
type OnConflict struct {
|
||||
Keys []string
|
||||
Ignore bool
|
||||
Replace bool
|
||||
Fragment string
|
||||
FragmentArgs []interface{}
|
||||
}
|
||||
|
||||
// Apply mutation.
|
||||
func (ocm OnConflict) Apply(doc *Document, mutation *Mutation) {
|
||||
if ocm.Keys == nil && ocm.Fragment == "" {
|
||||
ocm.Keys = doc.PrimaryFields()
|
||||
}
|
||||
|
||||
mutation.OnConflict = ocm
|
||||
}
|
||||
|
||||
// OnConflictIgnore insertion when conflict happens.
|
||||
func OnConflictIgnore() OnConflict {
|
||||
return OnConflict{Ignore: true}
|
||||
}
|
||||
|
||||
// OnConflictKeyIgnore insertion when conflict happens on specific keys.
|
||||
//
|
||||
// Specifying key is not supported by all database and may be ignored.
|
||||
func OnConflictKeyIgnore(key string) OnConflict {
|
||||
return OnConflictKeysIgnore([]string{key})
|
||||
}
|
||||
|
||||
// OnConflictKeysIgnore insertion when conflict happens on specific keys.
|
||||
//
|
||||
// Specifying key is not supported by all database and may be ignored.
|
||||
func OnConflictKeysIgnore(keys []string) OnConflict {
|
||||
return OnConflict{Keys: keys, Ignore: true}
|
||||
}
|
||||
|
||||
// OnConflictReplace insertion when conflict happens.
|
||||
func OnConflictReplace() OnConflict {
|
||||
return OnConflict{Replace: true}
|
||||
}
|
||||
|
||||
// OnConflictKeyReplace insertion when conflict happens on specific keys.
|
||||
//
|
||||
// Specifying key is not supported by all database and may be ignored.
|
||||
func OnConflictKeyReplace(key string) OnConflict {
|
||||
return OnConflictKeysReplace([]string{key})
|
||||
}
|
||||
|
||||
// OnConflictKeysReplace insertion when conflict happens on specific keys.
|
||||
//
|
||||
// Specifying key is not supported by all database and may be ignored.
|
||||
func OnConflictKeysReplace(keys []string) OnConflict {
|
||||
return OnConflict{Keys: keys, Replace: true}
|
||||
}
|
||||
|
||||
// OnConflictFragment allows to write custom sql for on conflict.
|
||||
//
|
||||
// This will add custom sql after ON CONFLICT, example: ON CONFLICT [FRAGMENT]
|
||||
func OnConflictFragment(sql string, args ...interface{}) OnConflict {
|
||||
return OnConflict{Fragment: sql, FragmentArgs: args}
|
||||
}
|
@ -0,0 +1,574 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Querier interface defines contract to be used for query builder.
|
||||
type Querier interface {
|
||||
Build(*Query)
|
||||
}
|
||||
|
||||
type QueryPopulator interface {
|
||||
Populate(*Query, DocumentMeta)
|
||||
}
|
||||
|
||||
// Build for given table using given queriers.
|
||||
func Build(table string, queriers ...Querier) Query {
|
||||
var (
|
||||
query = newQuery()
|
||||
)
|
||||
|
||||
if len(queriers) > 0 {
|
||||
_, query.empty = queriers[0].(Query)
|
||||
}
|
||||
|
||||
for _, querier := range queriers {
|
||||
// avoid using indirect call to avoid heap allocation
|
||||
switch q := querier.(type) {
|
||||
case Query:
|
||||
q.Build(&query)
|
||||
case JoinQuery:
|
||||
q.Build(&query)
|
||||
case FilterQuery:
|
||||
q.Build(&query)
|
||||
case GroupQuery:
|
||||
q.Build(&query)
|
||||
case SortQuery:
|
||||
q.Build(&query)
|
||||
case Offset:
|
||||
q.Build(&query)
|
||||
case Limit:
|
||||
q.Build(&query)
|
||||
case Lock:
|
||||
q.Build(&query)
|
||||
case Unscoped:
|
||||
q.Build(&query)
|
||||
case Reload:
|
||||
q.Build(&query)
|
||||
case SQLQuery:
|
||||
q.Build(&query)
|
||||
case Preload:
|
||||
q.Build(&query)
|
||||
case Cascade:
|
||||
q.Build(&query)
|
||||
}
|
||||
}
|
||||
|
||||
if query.Table == "" {
|
||||
query.Table = table
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// Query defines information about query generated by query builder.
|
||||
type Query struct {
|
||||
empty bool // TODO: use bitmask to mark what is updated and use it when merging two queries
|
||||
Table string
|
||||
SelectQuery SelectQuery
|
||||
JoinQuery []JoinQuery
|
||||
WhereQuery FilterQuery
|
||||
GroupQuery GroupQuery
|
||||
SortQuery []SortQuery
|
||||
OffsetQuery Offset
|
||||
LimitQuery Limit
|
||||
LockQuery Lock
|
||||
SQLQuery SQLQuery
|
||||
UnscopedQuery Unscoped
|
||||
ReloadQuery Reload
|
||||
CascadeQuery Cascade
|
||||
PreloadQuery []string
|
||||
UsePrimaryDb bool
|
||||
queryPopulators []QueryPopulator
|
||||
}
|
||||
|
||||
// Build query.
|
||||
func (q Query) Build(query *Query) {
|
||||
if query.empty {
|
||||
*query = q
|
||||
} else {
|
||||
// manual merge
|
||||
if q.Table != "" {
|
||||
query.Table = q.Table
|
||||
}
|
||||
|
||||
if q.SelectQuery.Fields != nil {
|
||||
query.SelectQuery = q.SelectQuery
|
||||
}
|
||||
|
||||
query.JoinQuery = append(query.JoinQuery, q.JoinQuery...)
|
||||
|
||||
if !q.WhereQuery.None() {
|
||||
query.WhereQuery = query.WhereQuery.And(q.WhereQuery)
|
||||
}
|
||||
|
||||
if q.GroupQuery.Fields != nil {
|
||||
query.GroupQuery = q.GroupQuery
|
||||
}
|
||||
|
||||
query.SortQuery = append(query.SortQuery, q.SortQuery...)
|
||||
|
||||
if q.OffsetQuery != 0 {
|
||||
query.OffsetQuery = q.OffsetQuery
|
||||
}
|
||||
|
||||
if q.LimitQuery != 0 {
|
||||
query.LimitQuery = q.LimitQuery
|
||||
}
|
||||
|
||||
if q.LockQuery != "" {
|
||||
query.LockQuery = q.LockQuery
|
||||
}
|
||||
|
||||
query.ReloadQuery = query.ReloadQuery || q.ReloadQuery
|
||||
query.CascadeQuery = query.CascadeQuery || q.CascadeQuery
|
||||
query.UsePrimaryDb = query.UsePrimaryDb || q.UsePrimaryDb
|
||||
}
|
||||
}
|
||||
|
||||
func (q Query) Populate(documentMeta DocumentMeta) Query {
|
||||
for i := range q.queryPopulators {
|
||||
q.queryPopulators[i].Populate(&q, documentMeta)
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Query) AddPopulator(populator QueryPopulator) {
|
||||
q.queryPopulators = append(q.queryPopulators, populator)
|
||||
}
|
||||
|
||||
// Select filter fields to be selected from database.
|
||||
func (q Query) Select(fields ...string) Query {
|
||||
q.SelectQuery = NewSelect(fields...)
|
||||
return q
|
||||
}
|
||||
|
||||
// From set the table to be used for query.
|
||||
func (q Query) From(table string) Query {
|
||||
q.Table = table
|
||||
return q
|
||||
}
|
||||
|
||||
// Distinct sets select query to be distinct.
|
||||
func (q Query) Distinct() Query {
|
||||
q.SelectQuery.OnlyDistinct = true
|
||||
return q
|
||||
}
|
||||
|
||||
// Join current table with other table.
|
||||
func (q Query) Join(table string, filter ...FilterQuery) Query {
|
||||
return q.JoinOn(table, "", "", filter...)
|
||||
}
|
||||
|
||||
// JoinOn current table with other table.
|
||||
func (q Query) JoinOn(table string, from string, to string, filter ...FilterQuery) Query {
|
||||
return q.JoinWith("JOIN", table, from, to, filter...)
|
||||
}
|
||||
|
||||
// JoinWith current table with other table with custom join mode.
|
||||
func (q Query) JoinWith(mode string, table string, from string, to string, filter ...FilterQuery) Query {
|
||||
NewJoinWith(mode, table, from, to, filter...).Build(&q) // TODO: ensure this always called last
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// Joinf create join query using a raw query.
|
||||
func (q Query) Joinf(expr string, args ...interface{}) Query {
|
||||
NewJoinFragment(expr, args...).Build(&q) // TODO: ensure this always called last
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// JoinAssoc current table with other table based on association field.
|
||||
func (q Query) JoinAssoc(assoc string, filter ...FilterQuery) Query {
|
||||
return q.JoinAssocWith("JOIN", assoc, filter...)
|
||||
}
|
||||
|
||||
// JoinAssocWith current table with other table based on association field.
|
||||
func (q Query) JoinAssocWith(mode string, assoc string, filter ...FilterQuery) Query {
|
||||
NewJoinAssocWith(mode, assoc, filter...).Build(&q)
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// Where query.
|
||||
func (q Query) Where(filters ...FilterQuery) Query {
|
||||
q.WhereQuery = q.WhereQuery.And(filters...)
|
||||
return q
|
||||
}
|
||||
|
||||
// Wheref create where query using a raw query.
|
||||
func (q Query) Wheref(expr string, args ...interface{}) Query {
|
||||
q.WhereQuery = q.WhereQuery.And(FilterFragment(expr, args...))
|
||||
return q
|
||||
}
|
||||
|
||||
// OrWhere query.
|
||||
func (q Query) OrWhere(filters ...FilterQuery) Query {
|
||||
q.WhereQuery = q.WhereQuery.Or(And(filters...))
|
||||
return q
|
||||
}
|
||||
|
||||
// OrWheref create where query using a raw query.
|
||||
func (q Query) OrWheref(expr string, args ...interface{}) Query {
|
||||
q.WhereQuery = q.WhereQuery.Or(FilterFragment(expr, args...))
|
||||
return q
|
||||
}
|
||||
|
||||
// Group query.
|
||||
func (q Query) Group(fields ...string) Query {
|
||||
q.GroupQuery.Fields = fields
|
||||
return q
|
||||
}
|
||||
|
||||
// Having query.
|
||||
func (q Query) Having(filters ...FilterQuery) Query {
|
||||
q.GroupQuery.Filter = q.GroupQuery.Filter.And(filters...)
|
||||
return q
|
||||
}
|
||||
|
||||
// Havingf create having query using a raw query.
|
||||
func (q Query) Havingf(expr string, args ...interface{}) Query {
|
||||
q.GroupQuery.Filter = q.GroupQuery.Filter.And(FilterFragment(expr, args...))
|
||||
return q
|
||||
}
|
||||
|
||||
// OrHaving query.
|
||||
func (q Query) OrHaving(filters ...FilterQuery) Query {
|
||||
q.GroupQuery.Filter = q.GroupQuery.Filter.Or(And(filters...))
|
||||
return q
|
||||
}
|
||||
|
||||
// OrHavingf create having query using a raw query.
|
||||
func (q Query) OrHavingf(expr string, args ...interface{}) Query {
|
||||
q.GroupQuery.Filter = q.GroupQuery.Filter.Or(FilterFragment(expr, args...))
|
||||
return q
|
||||
}
|
||||
|
||||
// Sort query.
|
||||
func (q Query) Sort(fields ...string) Query {
|
||||
return q.SortAsc(fields...)
|
||||
}
|
||||
|
||||
// SortAsc query.
|
||||
func (q Query) SortAsc(fields ...string) Query {
|
||||
var (
|
||||
offset = len(q.SortQuery)
|
||||
)
|
||||
|
||||
q.SortQuery = append(q.SortQuery, make([]SortQuery, len(fields))...)
|
||||
for i := range fields {
|
||||
q.SortQuery[offset+i] = NewSortAsc(fields[i])
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// SortDesc query.
|
||||
func (q Query) SortDesc(fields ...string) Query {
|
||||
var (
|
||||
offset = len(q.SortQuery)
|
||||
)
|
||||
|
||||
q.SortQuery = append(q.SortQuery, make([]SortQuery, len(fields))...)
|
||||
for i := range fields {
|
||||
q.SortQuery[offset+i] = NewSortDesc(fields[i])
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// Offset the result returned by database.
|
||||
func (q Query) Offset(offset int) Query {
|
||||
q.OffsetQuery = Offset(offset)
|
||||
return q
|
||||
}
|
||||
|
||||
// Limit result returned by database.
|
||||
func (q Query) Limit(limit int) Query {
|
||||
q.LimitQuery = Limit(limit)
|
||||
return q
|
||||
}
|
||||
|
||||
// Lock query expression.
|
||||
func (q Query) Lock(lock string) Query {
|
||||
q.LockQuery = Lock(lock)
|
||||
return q
|
||||
}
|
||||
|
||||
// Unscoped allows soft-delete to be ignored.
|
||||
func (q Query) Unscoped() Query {
|
||||
q.UnscopedQuery = true
|
||||
return q
|
||||
}
|
||||
|
||||
// Reload force reloading association on preload.
|
||||
func (q Query) Reload() Query {
|
||||
q.ReloadQuery = true
|
||||
return q
|
||||
}
|
||||
|
||||
// Cascade enable/disable autoload association on Find and FindAll query.
|
||||
func (q Query) Cascade(c bool) Query {
|
||||
q.CascadeQuery = Cascade(c)
|
||||
return q
|
||||
}
|
||||
|
||||
// Preload field association.
|
||||
func (q Query) Preload(field string) Query {
|
||||
q.PreloadQuery = append(q.PreloadQuery, field)
|
||||
return q
|
||||
}
|
||||
|
||||
// UsePrimary database.
|
||||
func (q Query) UsePrimary() Query {
|
||||
q.UsePrimaryDb = true
|
||||
return q
|
||||
}
|
||||
|
||||
// String describe query as string.
|
||||
func (q Query) String() string {
|
||||
if q.SQLQuery.Statement != "" {
|
||||
return q.SQLQuery.String()
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("rel")
|
||||
|
||||
if q.UsePrimaryDb {
|
||||
builder.WriteString(".UsePrimary()")
|
||||
}
|
||||
|
||||
if q.Table != "" {
|
||||
builder.WriteString(".From(\"")
|
||||
builder.WriteString(q.Table)
|
||||
builder.WriteString("\")")
|
||||
}
|
||||
|
||||
if len(q.SelectQuery.Fields) != 0 {
|
||||
builder.WriteString(".Select(\"")
|
||||
builder.WriteString(strings.Join(q.SelectQuery.Fields, "\", \""))
|
||||
builder.WriteString("\")")
|
||||
}
|
||||
|
||||
if q.SelectQuery.OnlyDistinct {
|
||||
builder.WriteString(".Distinct()")
|
||||
}
|
||||
|
||||
for i := range q.JoinQuery {
|
||||
builder.WriteString(".JoinWith(\"")
|
||||
builder.WriteString(q.JoinQuery[i].Mode)
|
||||
builder.WriteString("\", \"")
|
||||
builder.WriteString(q.JoinQuery[i].Table)
|
||||
builder.WriteString("\", \"")
|
||||
builder.WriteString(q.JoinQuery[i].From)
|
||||
builder.WriteString("\", \"")
|
||||
builder.WriteString(q.JoinQuery[i].To)
|
||||
builder.WriteString("\")")
|
||||
}
|
||||
|
||||
if !q.WhereQuery.None() {
|
||||
builder.WriteString(".Where(")
|
||||
builder.WriteString(q.WhereQuery.String())
|
||||
builder.WriteByte(')')
|
||||
}
|
||||
|
||||
if len(q.GroupQuery.Fields) != 0 {
|
||||
builder.WriteString(".Group(\"")
|
||||
builder.WriteString(strings.Join(q.GroupQuery.Fields, "\", \""))
|
||||
builder.WriteString("\")")
|
||||
|
||||
if !q.GroupQuery.Filter.None() {
|
||||
builder.WriteString(".Having(")
|
||||
builder.WriteString(q.GroupQuery.Filter.String())
|
||||
builder.WriteByte(')')
|
||||
}
|
||||
}
|
||||
|
||||
for _, sq := range q.SortQuery {
|
||||
if sq.Asc() {
|
||||
builder.WriteString(".SortAsc(\"")
|
||||
} else {
|
||||
builder.WriteString(".SortDesc(\"")
|
||||
}
|
||||
builder.WriteString(sq.Field)
|
||||
builder.WriteString("\")")
|
||||
}
|
||||
|
||||
if q.LimitQuery > 0 {
|
||||
builder.WriteString(".Limit(")
|
||||
builder.WriteString(strconv.Itoa(int(q.LimitQuery)))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
if q.OffsetQuery > 0 {
|
||||
builder.WriteString(".Offset(")
|
||||
builder.WriteString(strconv.Itoa(int(q.OffsetQuery)))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
if q.LockQuery != "" {
|
||||
builder.WriteString(".Lock(\"")
|
||||
builder.WriteString(string(q.LockQuery))
|
||||
builder.WriteString("\")")
|
||||
}
|
||||
|
||||
if q.UnscopedQuery {
|
||||
builder.WriteString(".Unscoped()")
|
||||
}
|
||||
|
||||
if q.ReloadQuery {
|
||||
builder.WriteString(".Reload()")
|
||||
}
|
||||
|
||||
if !q.CascadeQuery {
|
||||
builder.WriteString(".Cascade(false)")
|
||||
}
|
||||
|
||||
if len(q.PreloadQuery) != 0 {
|
||||
builder.WriteString(".Preload(\"")
|
||||
builder.WriteString(strings.Join(q.PreloadQuery, "\", \""))
|
||||
builder.WriteString("\")")
|
||||
}
|
||||
|
||||
if str := builder.String(); str != "rel" {
|
||||
return str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func newQuery() Query {
|
||||
return Query{
|
||||
CascadeQuery: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Select query create a query with chainable syntax, using select as the starting point.
|
||||
func Select(fields ...string) Query {
|
||||
query := newQuery()
|
||||
query.SelectQuery.Fields = fields
|
||||
return query
|
||||
}
|
||||
|
||||
// From create a query with chainable syntax, using from as the starting point.
|
||||
func From(table string) Query {
|
||||
query := newQuery()
|
||||
query.Table = table
|
||||
return query
|
||||
}
|
||||
|
||||
// Join create a query with chainable syntax, using join as the starting point.
|
||||
func Join(table string, filter ...FilterQuery) Query {
|
||||
return JoinOn(table, "", "", filter...)
|
||||
}
|
||||
|
||||
// JoinOn create a query with chainable syntax, using join as the starting point.
|
||||
func JoinOn(table string, from string, to string, filter ...FilterQuery) Query {
|
||||
return JoinWith("JOIN", table, from, to, filter...)
|
||||
}
|
||||
|
||||
// JoinWith create a query with chainable syntax, using join as the starting point.
|
||||
func JoinWith(mode string, table string, from string, to string, filter ...FilterQuery) Query {
|
||||
query := newQuery()
|
||||
query.JoinQuery = []JoinQuery{
|
||||
NewJoinWith(mode, table, from, to, filter...),
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// JoinAssoc create a query with chainable syntax, using join as the starting point.
|
||||
func JoinAssoc(assoc string, filter ...FilterQuery) Query {
|
||||
return JoinAssocWith("JOIN", assoc, filter...)
|
||||
}
|
||||
|
||||
// JoinAssocWith create a query with chainable syntax, using join as the starting point.
|
||||
func JoinAssocWith(mode string, assoc string, filter ...FilterQuery) Query {
|
||||
query := newQuery()
|
||||
query.JoinQuery = []JoinQuery{
|
||||
NewJoinAssocWith(mode, assoc, filter...),
|
||||
}
|
||||
query.AddPopulator(&query.JoinQuery[0])
|
||||
return query
|
||||
}
|
||||
|
||||
// Joinf create a query with chainable syntax, using join as the starting point.
|
||||
func Joinf(expr string, args ...interface{}) Query {
|
||||
query := newQuery()
|
||||
query.JoinQuery = []JoinQuery{
|
||||
NewJoinFragment(expr, args...),
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// Where create a query with chainable syntax, using where as the starting point.
|
||||
func Where(filters ...FilterQuery) Query {
|
||||
query := newQuery()
|
||||
query.WhereQuery = And(filters...)
|
||||
return query
|
||||
}
|
||||
|
||||
func UsePrimary() Query {
|
||||
query := newQuery()
|
||||
query.UsePrimaryDb = true
|
||||
return query
|
||||
}
|
||||
|
||||
// Offset Query.
|
||||
type Offset int
|
||||
|
||||
// Build query.
|
||||
func (o Offset) Build(query *Query) {
|
||||
query.OffsetQuery = o
|
||||
}
|
||||
|
||||
// Limit options.
|
||||
// When passed as query, it limits returned result from database.
|
||||
// When passed as column option, it sets the maximum size of the string/text/binary/integer columns.
|
||||
type Limit int
|
||||
|
||||
// Build query.
|
||||
func (l Limit) Build(query *Query) {
|
||||
query.LimitQuery = l
|
||||
}
|
||||
|
||||
func (l Limit) applyColumn(column *Column) {
|
||||
column.Limit = int(l)
|
||||
}
|
||||
|
||||
// Lock query.
|
||||
// This query will be ignored if used outside of transaction.
|
||||
type Lock string
|
||||
|
||||
// Build query.
|
||||
func (l Lock) Build(query *Query) {
|
||||
query.LockQuery = l
|
||||
}
|
||||
|
||||
// ForUpdate lock query.
|
||||
func ForUpdate() Lock {
|
||||
return "FOR UPDATE"
|
||||
}
|
||||
|
||||
// Unscoped query.
|
||||
type Unscoped bool
|
||||
|
||||
// Build query.
|
||||
func (u Unscoped) Build(query *Query) {
|
||||
query.UnscopedQuery = u
|
||||
}
|
||||
|
||||
// Apply mutation.
|
||||
func (u Unscoped) Apply(doc *Document, mutation *Mutation) {
|
||||
mutation.Unscoped = u
|
||||
}
|
||||
|
||||
// Preload query.
|
||||
type Preload string
|
||||
|
||||
// Build query.
|
||||
func (p Preload) Build(query *Query) {
|
||||
query.PreloadQuery = append(query.PreloadQuery, string(p))
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
// Package rel contains all rel primary APIs, such as Repository.
|
||||
package rel
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,150 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SchemaOp type.
|
||||
type SchemaOp uint8
|
||||
|
||||
const (
|
||||
// SchemaCreate operation.
|
||||
SchemaCreate SchemaOp = iota
|
||||
// SchemaAlter operation.
|
||||
SchemaAlter
|
||||
// SchemaRename operation.
|
||||
SchemaRename
|
||||
// SchemaDrop operation.
|
||||
SchemaDrop
|
||||
)
|
||||
|
||||
func (s SchemaOp) String() string {
|
||||
return [...]string{"create", "alter", "rename", "drop"}[s]
|
||||
}
|
||||
|
||||
// Migration definition.
|
||||
type Migration interface {
|
||||
internalMigration()
|
||||
description() string
|
||||
}
|
||||
|
||||
// Schema builder.
|
||||
type Schema struct {
|
||||
Migrations []Migration
|
||||
}
|
||||
|
||||
func (s *Schema) add(migration Migration) {
|
||||
s.Migrations = append(s.Migrations, migration)
|
||||
}
|
||||
|
||||
// CreateTable with name and its definition.
|
||||
func (s *Schema) CreateTable(name string, fn func(t *Table), options ...TableOption) {
|
||||
table := createTable(name, options)
|
||||
fn(&table)
|
||||
s.add(table)
|
||||
}
|
||||
|
||||
// CreateTableIfNotExists with name and its definition.
|
||||
func (s *Schema) CreateTableIfNotExists(name string, fn func(t *Table), options ...TableOption) {
|
||||
table := createTableIfNotExists(name, options)
|
||||
fn(&table)
|
||||
s.add(table)
|
||||
}
|
||||
|
||||
// AlterTable with name and its definition.
|
||||
func (s *Schema) AlterTable(name string, fn func(t *AlterTable), options ...TableOption) {
|
||||
table := alterTable(name, options)
|
||||
fn(&table)
|
||||
s.add(table.Table)
|
||||
}
|
||||
|
||||
// RenameTable by name.
|
||||
func (s *Schema) RenameTable(name string, newName string, options ...TableOption) {
|
||||
s.add(renameTable(name, newName, options))
|
||||
}
|
||||
|
||||
// DropTable by name.
|
||||
func (s *Schema) DropTable(name string, options ...TableOption) {
|
||||
s.add(dropTable(name, options))
|
||||
}
|
||||
|
||||
// DropTableIfExists by name.
|
||||
func (s *Schema) DropTableIfExists(name string, options ...TableOption) {
|
||||
s.add(dropTableIfExists(name, options))
|
||||
}
|
||||
|
||||
// AddColumn with name and type.
|
||||
func (s *Schema) AddColumn(table string, name string, typ ColumnType, options ...ColumnOption) {
|
||||
at := alterTable(table, nil)
|
||||
at.Column(name, typ, options...)
|
||||
s.add(at.Table)
|
||||
}
|
||||
|
||||
// RenameColumn by name.
|
||||
func (s *Schema) RenameColumn(table string, name string, newName string, options ...ColumnOption) {
|
||||
at := alterTable(table, nil)
|
||||
at.RenameColumn(name, newName, options...)
|
||||
s.add(at.Table)
|
||||
}
|
||||
|
||||
// DropColumn by name.
|
||||
func (s *Schema) DropColumn(table string, name string, options ...ColumnOption) {
|
||||
at := alterTable(table, nil)
|
||||
at.DropColumn(name, options...)
|
||||
s.add(at.Table)
|
||||
}
|
||||
|
||||
// CreateIndex for columns on a table.
|
||||
func (s *Schema) CreateIndex(table string, name string, column []string, options ...IndexOption) {
|
||||
s.add(createIndex(table, name, column, options))
|
||||
}
|
||||
|
||||
// CreateUniqueIndex for columns on a table.
|
||||
func (s *Schema) CreateUniqueIndex(table string, name string, column []string, options ...IndexOption) {
|
||||
s.add(createUniqueIndex(table, name, column, options))
|
||||
}
|
||||
|
||||
// DropIndex by name.
|
||||
func (s *Schema) DropIndex(table string, name string, options ...IndexOption) {
|
||||
s.add(dropIndex(table, name, options))
|
||||
}
|
||||
|
||||
// Exec queries.
|
||||
func (s *Schema) Exec(raw Raw) {
|
||||
s.add(raw)
|
||||
}
|
||||
|
||||
// Do migration using golang codes.
|
||||
func (s *Schema) Do(fn Do) {
|
||||
s.add(fn)
|
||||
}
|
||||
|
||||
// String returns schema operation.
|
||||
func (s Schema) String() string {
|
||||
descs := make([]string, len(s.Migrations))
|
||||
for i := range descs {
|
||||
descs[i] = s.Migrations[i].description()
|
||||
}
|
||||
|
||||
return strings.Join(descs, ", ")
|
||||
}
|
||||
|
||||
// Raw string
|
||||
type Raw string
|
||||
|
||||
func (r Raw) description() string {
|
||||
return "execute raw command"
|
||||
}
|
||||
|
||||
func (r Raw) internalMigration() {}
|
||||
func (r Raw) internalTableDefinition() {}
|
||||
|
||||
// Do used internally for schema migration.
|
||||
type Do func(context.Context, Repository) error
|
||||
|
||||
func (d Do) description() string {
|
||||
return "run go code"
|
||||
}
|
||||
|
||||
func (d Do) internalMigration() {}
|
@ -0,0 +1,142 @@
|
||||
package rel
|
||||
|
||||
// TableOption interface.
|
||||
// Available options are: Comment, Options.
|
||||
type TableOption interface {
|
||||
applyTable(table *Table)
|
||||
}
|
||||
|
||||
func applyTableOptions(table *Table, options []TableOption) {
|
||||
for i := range options {
|
||||
options[i].applyTable(table)
|
||||
}
|
||||
}
|
||||
|
||||
// ColumnOption interface.
|
||||
// Available options are: Nil, Unsigned, Limit, Precision, Scale, Default, Comment, Options.
|
||||
type ColumnOption interface {
|
||||
applyColumn(column *Column)
|
||||
}
|
||||
|
||||
func applyColumnOptions(column *Column, options []ColumnOption) {
|
||||
for i := range options {
|
||||
options[i].applyColumn(column)
|
||||
}
|
||||
}
|
||||
|
||||
// KeyOption interface.
|
||||
// Available options are: Comment, Options.
|
||||
type KeyOption interface {
|
||||
applyKey(key *Key)
|
||||
}
|
||||
|
||||
func applyKeyOptions(key *Key, options []KeyOption) {
|
||||
for i := range options {
|
||||
options[i].applyKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Primary set column as primary.
|
||||
type Primary bool
|
||||
|
||||
func (r Primary) applyColumn(column *Column) {
|
||||
column.Primary = bool(r)
|
||||
}
|
||||
|
||||
// Unique set column as unique.
|
||||
type Unique bool
|
||||
|
||||
func (r Unique) applyColumn(column *Column) {
|
||||
column.Unique = bool(r)
|
||||
}
|
||||
|
||||
func (r Unique) applyIndex(index *Index) {
|
||||
index.Unique = bool(r)
|
||||
}
|
||||
|
||||
// Required disallows nil values in the column.
|
||||
type Required bool
|
||||
|
||||
func (r Required) applyColumn(column *Column) {
|
||||
column.Required = bool(r)
|
||||
}
|
||||
|
||||
// Unsigned sets integer column to be unsigned.
|
||||
type Unsigned bool
|
||||
|
||||
func (u Unsigned) applyColumn(column *Column) {
|
||||
column.Unsigned = bool(u)
|
||||
}
|
||||
|
||||
// Precision defines the precision for the decimal fields, representing the total number of digits in the number.
|
||||
type Precision int
|
||||
|
||||
func (p Precision) applyColumn(column *Column) {
|
||||
column.Precision = int(p)
|
||||
}
|
||||
|
||||
// Scale Defines the scale for the decimal fields, representing the number of digits after the decimal point.
|
||||
type Scale int
|
||||
|
||||
func (s Scale) applyColumn(column *Column) {
|
||||
column.Scale = int(s)
|
||||
}
|
||||
|
||||
type defaultValue struct {
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (d defaultValue) applyColumn(column *Column) {
|
||||
column.Default = d.value
|
||||
}
|
||||
|
||||
// Default allows to set a default value on the column.).
|
||||
func Default(def interface{}) ColumnOption {
|
||||
return defaultValue{value: def}
|
||||
}
|
||||
|
||||
// OnDelete option for foreign key.
|
||||
type OnDelete string
|
||||
|
||||
func (od OnDelete) applyKey(key *Key) {
|
||||
key.Reference.OnDelete = string(od)
|
||||
}
|
||||
|
||||
// OnUpdate option for foreign key.
|
||||
type OnUpdate string
|
||||
|
||||
func (ou OnUpdate) applyKey(key *Key) {
|
||||
key.Reference.OnUpdate = string(ou)
|
||||
}
|
||||
|
||||
// Options options for table, column and index.
|
||||
type Options string
|
||||
|
||||
func (o Options) applyTable(table *Table) {
|
||||
table.Options = string(o)
|
||||
}
|
||||
|
||||
func (o Options) applyColumn(column *Column) {
|
||||
column.Options = string(o)
|
||||
}
|
||||
|
||||
func (o Options) applyIndex(index *Index) {
|
||||
index.Options = string(o)
|
||||
}
|
||||
|
||||
func (o Options) applyKey(key *Key) {
|
||||
key.Options = string(o)
|
||||
}
|
||||
|
||||
// Optional option.
|
||||
// when used with create table, will create table only if it's not exists.
|
||||
// when used with drop table, will drop table only if it's exists.
|
||||
type Optional bool
|
||||
|
||||
func (o Optional) applyTable(table *Table) {
|
||||
table.Optional = bool(o)
|
||||
}
|
||||
|
||||
func (o Optional) applyIndex(index *Index) {
|
||||
index.Optional = bool(o)
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package rel
|
||||
|
||||
// SelectQuery defines select clause of the query.
|
||||
type SelectQuery struct {
|
||||
OnlyDistinct bool
|
||||
Fields []string
|
||||
}
|
||||
|
||||
// Distinct select query.
|
||||
func (sq SelectQuery) Distinct() SelectQuery {
|
||||
sq.OnlyDistinct = true
|
||||
return sq
|
||||
}
|
||||
|
||||
// NewSelect query.
|
||||
//
|
||||
// Deprecated: use Select instead
|
||||
func NewSelect(fields ...string) SelectQuery {
|
||||
return SelectQuery{
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package rel
|
||||
|
||||
// SortQuery defines sort information of query.
|
||||
type SortQuery struct {
|
||||
Field string
|
||||
Sort int
|
||||
}
|
||||
|
||||
// Build sort query.
|
||||
func (sq SortQuery) Build(query *Query) {
|
||||
query.SortQuery = append(query.SortQuery, sq)
|
||||
}
|
||||
|
||||
// Asc returns true if sort is ascending.
|
||||
func (sq SortQuery) Asc() bool {
|
||||
return sq.Sort >= 0
|
||||
}
|
||||
|
||||
// Desc returns true if s is descending.
|
||||
func (sq SortQuery) Desc() bool {
|
||||
return sq.Sort < 0
|
||||
}
|
||||
|
||||
// SortAsc sorts field with ascending sort.
|
||||
func SortAsc(field string) SortQuery {
|
||||
return SortQuery{
|
||||
Field: field,
|
||||
Sort: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSortDesc sorts field with descending sort.
|
||||
func SortDesc(field string) SortQuery {
|
||||
return SortQuery{
|
||||
Field: field,
|
||||
Sort: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// NewSortAsc sorts field with ascending sort.
|
||||
//
|
||||
// Deprecated: use SortAsc instead
|
||||
NewSortAsc = SortAsc
|
||||
|
||||
// NewSortDesc sorts field with descending sort.
|
||||
//
|
||||
// Deprecated: use SortDesc instead
|
||||
NewSortDesc = SortDesc
|
||||
)
|
@ -0,0 +1,37 @@
|
||||
package rel
|
||||
|
||||
import "strings"
|
||||
|
||||
// SQLQuery allows querying using native query supported by database.
|
||||
type SQLQuery struct {
|
||||
Statement string
|
||||
Values []interface{}
|
||||
}
|
||||
|
||||
// Build Raw Query.
|
||||
func (sq SQLQuery) Build(query *Query) {
|
||||
query.SQLQuery = sq
|
||||
}
|
||||
|
||||
func (sq SQLQuery) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("rel.SQL(\"")
|
||||
builder.WriteString(sq.Statement)
|
||||
builder.WriteString("\"")
|
||||
|
||||
if len(sq.Values) != 0 {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString(fmtifaces(sq.Values))
|
||||
}
|
||||
|
||||
builder.WriteString(")")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// SQL Query.
|
||||
func SQL(statement string, values ...interface{}) SQLQuery {
|
||||
return SQLQuery{
|
||||
Statement: statement,
|
||||
Values: values,
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Now NowFunc = func() time.Time {
|
||||
return time.Now().Truncate(time.Second)
|
||||
}
|
||||
)
|
||||
|
||||
// NowFunc is the type of function that returns the current time.
|
||||
type NowFunc func() time.Time
|
||||
|
||||
// Structset can be used as mutation for repository insert or update operation.
|
||||
// This will save every field in struct and it's association as long as it's loaded.
|
||||
// This is the default mutator used by repository.
|
||||
type Structset struct {
|
||||
doc *Document
|
||||
skipZero bool
|
||||
}
|
||||
|
||||
// Apply mutation.
|
||||
func (s Structset) Apply(doc *Document, mut *Mutation) {
|
||||
var (
|
||||
pFields = s.doc.PrimaryFields()
|
||||
t = Now()
|
||||
)
|
||||
|
||||
for _, field := range s.doc.Fields() {
|
||||
switch field {
|
||||
case "created_at", "inserted_at":
|
||||
if doc.Flag(HasCreatedAt) {
|
||||
if value, ok := doc.Value(field); ok && value.(time.Time).IsZero() {
|
||||
s.set(doc, mut, field, t, true)
|
||||
continue
|
||||
}
|
||||
}
|
||||
case "updated_at":
|
||||
if doc.Flag(HasUpdatedAt) {
|
||||
s.set(doc, mut, field, t, true)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(pFields) == 1 && pFields[0] == field {
|
||||
// allow setting primary key as long as it's not zero.
|
||||
s.applyValue(doc, mut, field, true)
|
||||
} else {
|
||||
s.applyValue(doc, mut, field, s.skipZero)
|
||||
}
|
||||
}
|
||||
|
||||
if mut.Cascade {
|
||||
s.applyAssoc(mut)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Structset) set(doc *Document, mut *Mutation, field string, value interface{}, force bool) {
|
||||
if (force || doc.v != s.doc.v) && !doc.SetValue(field, value) {
|
||||
panic(fmt.Sprint("rel: cannot assign ", value, " as ", field, " into ", doc.Table()))
|
||||
}
|
||||
|
||||
mut.Add(Set(field, value))
|
||||
}
|
||||
|
||||
func (s Structset) applyValue(doc *Document, mut *Mutation, field string, skipZero bool) {
|
||||
if value, ok := s.doc.Value(field); ok {
|
||||
if skipZero && isZero(value) {
|
||||
return
|
||||
}
|
||||
|
||||
s.set(doc, mut, field, value, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Structset) applyAssoc(mut *Mutation) {
|
||||
for _, field := range s.doc.BelongsTo() {
|
||||
s.buildAssoc(field, mut)
|
||||
}
|
||||
|
||||
for _, field := range s.doc.HasOne() {
|
||||
s.buildAssoc(field, mut)
|
||||
}
|
||||
|
||||
for _, field := range s.doc.HasMany() {
|
||||
s.buildAssocMany(field, mut)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Structset) buildAssoc(field string, mut *Mutation) {
|
||||
assoc := s.doc.Association(field)
|
||||
if assoc.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
doc, _ = assoc.Document()
|
||||
)
|
||||
|
||||
mut.SetAssoc(field, Apply(doc, newStructset(doc, s.skipZero)))
|
||||
}
|
||||
|
||||
func (s Structset) buildAssocMany(field string, mut *Mutation) {
|
||||
assoc := s.doc.Association(field)
|
||||
if assoc.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
col, _ = assoc.Collection()
|
||||
muts = make([]Mutation, col.Len())
|
||||
)
|
||||
|
||||
for i := range muts {
|
||||
var (
|
||||
doc = col.Get(i)
|
||||
)
|
||||
|
||||
muts[i] = Apply(doc, newStructset(doc, s.skipZero))
|
||||
}
|
||||
|
||||
mut.SetAssoc(field, muts...)
|
||||
}
|
||||
|
||||
func newStructset(doc *Document, skipZero bool) Structset {
|
||||
return Structset{
|
||||
doc: doc,
|
||||
skipZero: skipZero,
|
||||
}
|
||||
}
|
||||
|
||||
// NewStructset from a struct.
|
||||
func NewStructset(record interface{}, skipZero bool) Structset {
|
||||
return newStructset(NewDocument(record), skipZero)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package rel
|
||||
|
||||
// SubQuery warps a query into: Prefix (Query)
|
||||
type SubQuery struct {
|
||||
Prefix string
|
||||
Query Query
|
||||
}
|
||||
|
||||
// All warp a query into ALL(sub-query)
|
||||
//
|
||||
// Some database may not support this keyword, please consult to your database documentation.
|
||||
func All(sub Query) SubQuery {
|
||||
return SubQuery{
|
||||
Prefix: "ALL",
|
||||
Query: sub,
|
||||
}
|
||||
}
|
||||
|
||||
// Any warp a query into ANY(sub-query)
|
||||
//
|
||||
// Some database may not support this keyword, please consult to your database documentation.
|
||||
func Any(sub Query) SubQuery {
|
||||
return SubQuery{
|
||||
Prefix: "ANY",
|
||||
Query: sub,
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
package rel
|
||||
|
||||
// TableDefinition interface.
|
||||
type TableDefinition interface {
|
||||
internalTableDefinition()
|
||||
}
|
||||
|
||||
// Table definition.
|
||||
type Table struct {
|
||||
Op SchemaOp
|
||||
Name string
|
||||
Rename string
|
||||
Definitions []TableDefinition
|
||||
Optional bool
|
||||
Options string
|
||||
}
|
||||
|
||||
// Column defines a column with name and type.
|
||||
func (t *Table) Column(name string, typ ColumnType, options ...ColumnOption) {
|
||||
if typ == BigID || typ == ID {
|
||||
options = append([]ColumnOption{Primary(true)}, options...)
|
||||
}
|
||||
t.Definitions = append(t.Definitions, createColumn(name, typ, options))
|
||||
}
|
||||
|
||||
// ID defines a column with name and ID type.
|
||||
// the resulting database type will depends on database.
|
||||
func (t *Table) ID(name string, options ...ColumnOption) {
|
||||
t.Column(name, ID, options...)
|
||||
}
|
||||
|
||||
// BigID defines a column with name and Big ID type.
|
||||
// the resulting database type will depends on database.
|
||||
func (t *Table) BigID(name string, options ...ColumnOption) {
|
||||
t.Column(name, BigID, options...)
|
||||
}
|
||||
|
||||
// Bool defines a column with name and Bool type.
|
||||
func (t *Table) Bool(name string, options ...ColumnOption) {
|
||||
t.Column(name, Bool, options...)
|
||||
}
|
||||
|
||||
// SmallInt defines a column with name and Small type.
|
||||
func (t *Table) SmallInt(name string, options ...ColumnOption) {
|
||||
t.Column(name, SmallInt, options...)
|
||||
}
|
||||
|
||||
// Int defines a column with name and Int type.
|
||||
func (t *Table) Int(name string, options ...ColumnOption) {
|
||||
t.Column(name, Int, options...)
|
||||
}
|
||||
|
||||
// BigInt defines a column with name and BigInt type.
|
||||
func (t *Table) BigInt(name string, options ...ColumnOption) {
|
||||
t.Column(name, BigInt, options...)
|
||||
}
|
||||
|
||||
// Float defines a column with name and Float type.
|
||||
func (t *Table) Float(name string, options ...ColumnOption) {
|
||||
t.Column(name, Float, options...)
|
||||
}
|
||||
|
||||
// Decimal defines a column with name and Decimal type.
|
||||
func (t *Table) Decimal(name string, options ...ColumnOption) {
|
||||
t.Column(name, Decimal, options...)
|
||||
}
|
||||
|
||||
// String defines a column with name and String type.
|
||||
func (t *Table) String(name string, options ...ColumnOption) {
|
||||
t.Column(name, String, options...)
|
||||
}
|
||||
|
||||
// Text defines a column with name and Text type.
|
||||
func (t *Table) Text(name string, options ...ColumnOption) {
|
||||
t.Column(name, Text, options...)
|
||||
}
|
||||
|
||||
// JSON defines a column with name and JSON type.
|
||||
func (t *Table) JSON(name string, options ...ColumnOption) {
|
||||
t.Column(name, JSON, options...)
|
||||
}
|
||||
|
||||
// Date defines a column with name and Date type.
|
||||
func (t *Table) Date(name string, options ...ColumnOption) {
|
||||
t.Column(name, Date, options...)
|
||||
}
|
||||
|
||||
// DateTime defines a column with name and DateTime type.
|
||||
func (t *Table) DateTime(name string, options ...ColumnOption) {
|
||||
t.Column(name, DateTime, options...)
|
||||
}
|
||||
|
||||
// Time defines a column with name and Time type.
|
||||
func (t *Table) Time(name string, options ...ColumnOption) {
|
||||
t.Column(name, Time, options...)
|
||||
}
|
||||
|
||||
// PrimaryKey defines a primary key for table.
|
||||
func (t *Table) PrimaryKey(column string, options ...KeyOption) {
|
||||
t.PrimaryKeys([]string{column}, options...)
|
||||
}
|
||||
|
||||
// PrimaryKeys defines composite primary keys for table.
|
||||
func (t *Table) PrimaryKeys(columns []string, options ...KeyOption) {
|
||||
t.Definitions = append(t.Definitions, createPrimaryKeys(columns, options))
|
||||
}
|
||||
|
||||
// ForeignKey defines foreign key index.
|
||||
func (t *Table) ForeignKey(column string, refTable string, refColumn string, options ...KeyOption) {
|
||||
t.Definitions = append(t.Definitions, createForeignKey(column, refTable, refColumn, options))
|
||||
}
|
||||
|
||||
// Unique defines an unique key for columns.
|
||||
func (t *Table) Unique(columns []string, options ...KeyOption) {
|
||||
t.Definitions = append(t.Definitions, createKeys(columns, UniqueKey, options))
|
||||
}
|
||||
|
||||
// Fragment defines anything using sql fragment.
|
||||
func (t *Table) Fragment(fragment string) {
|
||||
t.Definitions = append(t.Definitions, Raw(fragment))
|
||||
}
|
||||
|
||||
func (t Table) description() string {
|
||||
return t.Op.String() + " table " + t.Name
|
||||
}
|
||||
|
||||
func (t Table) internalMigration() {}
|
||||
|
||||
// AlterTable Migrator.
|
||||
type AlterTable struct {
|
||||
Table
|
||||
}
|
||||
|
||||
// RenameColumn to a new name.
|
||||
func (at *AlterTable) RenameColumn(name string, newName string, options ...ColumnOption) {
|
||||
at.Definitions = append(at.Definitions, renameColumn(name, newName, options))
|
||||
}
|
||||
|
||||
// DropColumn from this table.
|
||||
func (at *AlterTable) DropColumn(name string, options ...ColumnOption) {
|
||||
at.Definitions = append(at.Definitions, dropColumn(name, options))
|
||||
}
|
||||
|
||||
func createTable(name string, options []TableOption) Table {
|
||||
table := Table{
|
||||
Op: SchemaCreate,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
applyTableOptions(&table, options)
|
||||
return table
|
||||
}
|
||||
|
||||
func createTableIfNotExists(name string, options []TableOption) Table {
|
||||
table := createTable(name, options)
|
||||
table.Optional = true
|
||||
return table
|
||||
}
|
||||
|
||||
func alterTable(name string, options []TableOption) AlterTable {
|
||||
table := Table{
|
||||
Op: SchemaAlter,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
applyTableOptions(&table, options)
|
||||
return AlterTable{Table: table}
|
||||
}
|
||||
|
||||
func renameTable(name string, newName string, options []TableOption) Table {
|
||||
table := Table{
|
||||
Op: SchemaRename,
|
||||
Name: name,
|
||||
Rename: newName,
|
||||
}
|
||||
|
||||
applyTableOptions(&table, options)
|
||||
return table
|
||||
}
|
||||
|
||||
func dropTable(name string, options []TableOption) Table {
|
||||
table := Table{
|
||||
Op: SchemaDrop,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
applyTableOptions(&table, options)
|
||||
return table
|
||||
}
|
||||
|
||||
func dropTableIfExists(name string, options []TableOption) Table {
|
||||
table := dropTable(name, options)
|
||||
table.Optional = true
|
||||
return table
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package rel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func indirectInterface(rv reflect.Value) interface{} {
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
if rv.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
return rv.Interface()
|
||||
}
|
||||
|
||||
func indirectReflectType(rt reflect.Type) reflect.Type {
|
||||
if rt.Kind() == reflect.Ptr {
|
||||
return rt.Elem()
|
||||
}
|
||||
|
||||
return rt
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type isZeroer interface {
|
||||
IsZero() bool
|
||||
}
|
||||
|
||||
// isZero shallowly check wether a field in struct is zero or not
|
||||
func isZero(value interface{}) bool {
|
||||
var (
|
||||
zero bool
|
||||
)
|
||||
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
zero = true
|
||||
case bool:
|
||||
zero = !v
|
||||
case string:
|
||||
zero = v == ""
|
||||
case int:
|
||||
zero = v == 0
|
||||
case int8:
|
||||
zero = v == 0
|
||||
case int16:
|
||||
zero = v == 0
|
||||
case int32:
|
||||
zero = v == 0
|
||||
case int64:
|
||||
zero = v == 0
|
||||
case uint:
|
||||
zero = v == 0
|
||||
case uint8:
|
||||
zero = v == 0
|
||||
case uint16:
|
||||
zero = v == 0
|
||||
case uint32:
|
||||
zero = v == 0
|
||||
case uint64:
|
||||
zero = v == 0
|
||||
case uintptr:
|
||||
zero = v == 0
|
||||
case float32:
|
||||
zero = v == 0
|
||||
case float64:
|
||||
zero = v == 0
|
||||
case isZeroer:
|
||||
zero = v.IsZero()
|
||||
default:
|
||||
zero = isDeepZero(reflect.ValueOf(value), 0)
|
||||
}
|
||||
|
||||
return zero
|
||||
}
|
||||
|
||||
// modified from https://golang.org/src/reflect/value.go?s=33807:33835#L1077
|
||||
func isDeepZero(rv reflect.Value, depth int) bool {
|
||||
if depth < 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Bool:
|
||||
return !rv.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return rv.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return rv.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return math.Float64bits(rv.Float()) == 0
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
c := rv.Complex()
|
||||
return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0
|
||||
case reflect.Array:
|
||||
// check one level deeper if it's an uuid ([16]byte)
|
||||
if rv.Type().Elem().Kind() == reflect.Uint8 && rv.Len() == 16 {
|
||||
depth += 1
|
||||
}
|
||||
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if !isDeepZero(rv.Index(i), depth-1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.UnsafePointer:
|
||||
return rv.IsNil()
|
||||
case reflect.Slice:
|
||||
return rv.IsNil() || rv.Len() == 0
|
||||
case reflect.String:
|
||||
return rv.Len() == 0
|
||||
case reflect.Struct:
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
if !isDeepZero(rv.Field(i), depth-1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func setPointerValue(ft reflect.Type, fv reflect.Value, rt reflect.Type, rv reflect.Value) bool {
|
||||
if ft.Elem() != rt && !rt.AssignableTo(ft.Elem()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if fv.IsNil() {
|
||||
fv.Set(reflect.New(ft.Elem()))
|
||||
}
|
||||
fv.Elem().Set(rv)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func setConvertValue(ft reflect.Type, fv reflect.Value, rt reflect.Type, rv reflect.Value) bool {
|
||||
var (
|
||||
rk = rt.Kind()
|
||||
fk = ft.Kind()
|
||||
)
|
||||
|
||||
// prevents unintentional conversion
|
||||
if (rk >= reflect.Int || rk <= reflect.Uint64) && fk == reflect.String {
|
||||
return false
|
||||
}
|
||||
|
||||
fv.Set(rv.Convert(ft))
|
||||
return true
|
||||
}
|
||||
|
||||
func fmtiface(v interface{}) string {
|
||||
if str, ok := v.(string); ok {
|
||||
return "\"" + str + "\""
|
||||
}
|
||||
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
func fmtifaces(v []interface{}) string {
|
||||
var str strings.Builder
|
||||
for i := range v {
|
||||
if i > 0 {
|
||||
str.WriteString(", ")
|
||||
}
|
||||
str.WriteString(fmtiface(v[i]))
|
||||
}
|
||||
|
||||
return str.String()
|
||||
}
|
||||
|
||||
// Encode index slice into single string
|
||||
func encodeIndices(indices []int) string {
|
||||
var sb strings.Builder
|
||||
for _, index := range indices {
|
||||
sb.WriteString("/")
|
||||
sb.WriteString(strconv.Itoa(index))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Get field by index and init pointers on path if flag is true
|
||||
// modified from: https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/reflect/value.go;l=1228-1245;bpv
|
||||
func reflectValueFieldByIndex(rv reflect.Value, index []int, init bool) reflect.Value {
|
||||
if len(index) == 1 {
|
||||
return rv.Field(index[0])
|
||||
}
|
||||
|
||||
for depth := 0; depth < len(index)-1; depth += 1 {
|
||||
field := rv.Field(index[depth])
|
||||
|
||||
if field.Kind() != reflect.Ptr {
|
||||
rv = field
|
||||
continue
|
||||
}
|
||||
|
||||
if field.IsNil() {
|
||||
if !init {
|
||||
targetType := field.Type().Elem().FieldByIndex(index[depth+1:]).Type
|
||||
return reflect.Zero(reflect.PtrTo(targetType))
|
||||
}
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
|
||||
rv = field.Elem()
|
||||
}
|
||||
return rv.Field(index[len(index)-1])
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "go"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
import_root = "github.com/go-rel/sql"
|
||||
|
||||
[[transformers]]
|
||||
name = "gofmt"
|
||||
enabled = true
|
@ -0,0 +1,8 @@
|
||||
vendor
|
||||
.tool-versions
|
||||
*.db
|
||||
.vscode/
|
||||
debug.test
|
||||
.idea/
|
||||
*.out
|
||||
*.test
|
@ -0,0 +1,8 @@
|
||||
builds:
|
||||
- skip: true
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 REL
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,9 @@
|
||||
# sql
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/go-rel/sql?status.svg)](https://pkg.go.dev/github.com/go-rel/sql)
|
||||
[![Test](https://github.com/go-rel/sql/actions/workflows/test.yml/badge.svg)](https://github.com/go-rel/sql/actions/workflows/test.yml)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-rel/sql)](https://goreportcard.com/report/github.com/go-rel/sql)
|
||||
[![codecov](https://codecov.io/gh/go-rel/sql/branch/main/graph/badge.svg?token=67b87tbq5M)](https://codecov.io/gh/go-rel/sql)
|
||||
[![Gitter chat](https://badges.gitter.im/go-rel/rel.png)](https://gitter.im/go-rel/rel)
|
||||
|
||||
Base SQL adapter for REL.
|
@ -0,0 +1,33 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
type QueryBuilder interface {
|
||||
Build(query rel.Query) (string, []interface{})
|
||||
}
|
||||
|
||||
type InsertBuilder interface {
|
||||
Build(table string, primaryField string, mutates map[string]rel.Mutate, onConflict rel.OnConflict) (string, []interface{})
|
||||
}
|
||||
|
||||
type InsertAllBuilder interface {
|
||||
Build(table string, primaryField string, fields []string, bulkMutates []map[string]rel.Mutate, onConflict rel.OnConflict) (string, []interface{})
|
||||
}
|
||||
|
||||
type UpdateBuilder interface {
|
||||
Build(table string, primaryField string, mutates map[string]rel.Mutate, filter rel.FilterQuery) (string, []interface{})
|
||||
}
|
||||
|
||||
type DeleteBuilder interface {
|
||||
Build(table string, filter rel.FilterQuery) (string, []interface{})
|
||||
}
|
||||
|
||||
type TableBuilder interface {
|
||||
Build(table rel.Table) string
|
||||
}
|
||||
|
||||
type IndexBuilder interface {
|
||||
Build(index rel.Index) string
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-rel/sql"
|
||||
)
|
||||
|
||||
// UnescapeCharacter disable field escaping when it starts with this character.
|
||||
var UnescapeCharacter byte = '^'
|
||||
|
||||
var escapeCache sync.Map
|
||||
|
||||
type escapeCacheKey struct {
|
||||
table string
|
||||
value string
|
||||
quoter Quoter
|
||||
}
|
||||
|
||||
// Buffer is used to build query string.
|
||||
type Buffer struct {
|
||||
strings.Builder
|
||||
Quoter Quoter
|
||||
ValueConverter driver.ValueConverter
|
||||
ArgumentPlaceholder string
|
||||
ArgumentOrdinal bool
|
||||
InlineValues bool
|
||||
BoolTrueValue string
|
||||
BoolFalseValue string
|
||||
valueCount int
|
||||
arguments []interface{}
|
||||
}
|
||||
|
||||
// WriteValue query placeholder and append value to argument.
|
||||
func (b *Buffer) WriteValue(value interface{}) {
|
||||
if !b.InlineValues {
|
||||
b.WritePlaceholder()
|
||||
b.arguments = append(b.arguments, value)
|
||||
return
|
||||
}
|
||||
|
||||
// Detect float bits to not lose precision after converting to float64
|
||||
var floatBits = 64
|
||||
if value != nil && reflect.TypeOf(value).Kind() == reflect.Float32 {
|
||||
floatBits = 32
|
||||
}
|
||||
|
||||
if v, err := b.ValueConverter.ConvertValue(value); err != nil {
|
||||
log.Printf("[WARN] unsupported inline value %v: %v", value, err)
|
||||
} else {
|
||||
value = v
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
b.WriteString("NULL")
|
||||
return
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
b.WriteString(b.Quoter.Value(v))
|
||||
return
|
||||
case []byte:
|
||||
b.WriteString(b.Quoter.Value(string(v)))
|
||||
return
|
||||
case time.Time:
|
||||
b.WriteString(b.Quoter.Value(v.Format(sql.DefaultTimeLayout)))
|
||||
return
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(value)
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
b.WriteString(strconv.FormatInt(rv.Int(), 10))
|
||||
return
|
||||
case reflect.Float32, reflect.Float64:
|
||||
b.WriteString(strconv.FormatFloat(rv.Float(), 'g', -1, floatBits))
|
||||
return
|
||||
case reflect.Bool:
|
||||
if rv.Bool() {
|
||||
b.WriteString(b.BoolTrueValue)
|
||||
} else {
|
||||
b.WriteString(b.BoolFalseValue)
|
||||
}
|
||||
return
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%v", value))
|
||||
}
|
||||
|
||||
// WritePlaceholder without adding argument.
|
||||
// argument can be added later using AddArguments function.
|
||||
func (b *Buffer) WritePlaceholder() {
|
||||
b.valueCount++
|
||||
b.WriteString(b.ArgumentPlaceholder)
|
||||
if b.ArgumentOrdinal {
|
||||
b.WriteString(strconv.Itoa(b.valueCount))
|
||||
}
|
||||
}
|
||||
|
||||
// WriteField writes table and field name.
|
||||
func (b *Buffer) WriteField(table, field string) {
|
||||
b.WriteString(b.escape(table, field))
|
||||
}
|
||||
|
||||
// WriteEscape string.
|
||||
func (b *Buffer) WriteEscape(value string) {
|
||||
b.WriteString(b.escape("", value))
|
||||
}
|
||||
|
||||
func (b Buffer) escape(table, value string) string {
|
||||
if table == "" && value == "*" {
|
||||
return value
|
||||
}
|
||||
|
||||
key := escapeCacheKey{table: table, value: value, quoter: b.Quoter}
|
||||
escapedValue, ok := escapeCache.Load(key)
|
||||
if ok {
|
||||
return escapedValue.(string)
|
||||
}
|
||||
|
||||
var escaped_table string
|
||||
if table != "" {
|
||||
if strings.IndexByte(table, '.') >= 0 {
|
||||
parts := strings.Split(table, ".")
|
||||
for i, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
parts[i] = b.Quoter.ID(part)
|
||||
}
|
||||
escaped_table = strings.Join(parts, ".")
|
||||
} else {
|
||||
escaped_table = b.Quoter.ID(table)
|
||||
}
|
||||
}
|
||||
|
||||
if value == "*" {
|
||||
escapedValue = escaped_table + ".*"
|
||||
} else if len(value) > 0 && value[0] == UnescapeCharacter {
|
||||
escapedValue = value[1:]
|
||||
} else if _, err := strconv.Atoi(value); err == nil {
|
||||
escapedValue = value
|
||||
} else if i := strings.Index(strings.ToLower(value), " as "); i > -1 {
|
||||
escapedValue = b.escape(table, value[:i]) + " AS " + b.Quoter.ID(value[i+4:])
|
||||
} else if start, end := strings.IndexRune(value, '('), strings.IndexRune(value, ')'); start >= 0 && end >= 0 && end > start {
|
||||
escapedValue = value[:start+1] + b.escape(table, value[start+1:end]) + value[end:]
|
||||
} else {
|
||||
parts := strings.Split(value, ".")
|
||||
for i, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "*" && i == len(parts)-1 {
|
||||
break
|
||||
}
|
||||
parts[i] = b.Quoter.ID(part)
|
||||
}
|
||||
result := strings.Join(parts, ".")
|
||||
if len(parts) == 1 && table != "" {
|
||||
result = escaped_table + "." + result
|
||||
}
|
||||
escapedValue = result
|
||||
}
|
||||
|
||||
escapeCache.Store(key, escapedValue)
|
||||
return escapedValue.(string)
|
||||
}
|
||||
|
||||
// AddArguments appends multiple arguments without writing placeholder query..
|
||||
func (b *Buffer) AddArguments(args ...interface{}) {
|
||||
if b.arguments == nil {
|
||||
b.arguments = args
|
||||
} else {
|
||||
b.arguments = append(b.arguments, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (b Buffer) Arguments() []interface{} {
|
||||
return b.arguments
|
||||
}
|
||||
|
||||
// Reset buffer.
|
||||
func (b *Buffer) Reset() {
|
||||
b.Builder.Reset()
|
||||
b.valueCount = 0
|
||||
b.arguments = nil
|
||||
}
|
||||
|
||||
// BufferFactory is used to create buffer based on shared settings.
|
||||
type BufferFactory struct {
|
||||
Quoter Quoter
|
||||
ValueConverter driver.ValueConverter
|
||||
ArgumentPlaceholder string
|
||||
ArgumentOrdinal bool
|
||||
InlineValues bool
|
||||
BoolTrueValue string
|
||||
BoolFalseValue string
|
||||
}
|
||||
|
||||
func (bf BufferFactory) Create() Buffer {
|
||||
conv := bf.ValueConverter
|
||||
if conv == nil {
|
||||
conv = driver.DefaultParameterConverter
|
||||
}
|
||||
return Buffer{
|
||||
Quoter: bf.Quoter,
|
||||
ValueConverter: conv,
|
||||
ArgumentPlaceholder: bf.ArgumentPlaceholder,
|
||||
ArgumentOrdinal: bf.ArgumentOrdinal,
|
||||
InlineValues: bf.InlineValues,
|
||||
BoolTrueValue: bf.BoolTrueValue,
|
||||
BoolFalseValue: bf.BoolFalseValue,
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
// Delete builder.
|
||||
type Delete struct {
|
||||
BufferFactory BufferFactory
|
||||
Query QueryWriter
|
||||
Filter Filter
|
||||
}
|
||||
|
||||
// Build SQL query and its arguments.
|
||||
func (ds Delete) Build(table string, filter rel.FilterQuery) (string, []interface{}) {
|
||||
var (
|
||||
buffer = ds.BufferFactory.Create()
|
||||
)
|
||||
|
||||
buffer.WriteString("DELETE FROM ")
|
||||
buffer.WriteEscape(table)
|
||||
|
||||
if !filter.None() {
|
||||
buffer.WriteString(" WHERE ")
|
||||
ds.Filter.Write(&buffer, table, filter, ds.Query)
|
||||
}
|
||||
|
||||
buffer.WriteString(";")
|
||||
|
||||
return buffer.String(), buffer.Arguments()
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
// Filter builder.
|
||||
type Filter struct{}
|
||||
|
||||
// Write SQL to buffer.
|
||||
func (f Filter) Write(buffer *Buffer, table string, filter rel.FilterQuery, queryWriter QueryWriter) {
|
||||
switch filter.Type {
|
||||
case rel.FilterAndOp:
|
||||
f.WriteLogical(buffer, table, "AND", filter.Inner, queryWriter)
|
||||
case rel.FilterOrOp:
|
||||
f.WriteLogical(buffer, table, "OR", filter.Inner, queryWriter)
|
||||
case rel.FilterNotOp:
|
||||
buffer.WriteString("NOT ")
|
||||
f.WriteLogical(buffer, table, "AND", filter.Inner, queryWriter)
|
||||
case rel.FilterEqOp,
|
||||
rel.FilterNeOp,
|
||||
rel.FilterLtOp,
|
||||
rel.FilterLteOp,
|
||||
rel.FilterGtOp,
|
||||
rel.FilterGteOp:
|
||||
f.WriteComparison(buffer, table, filter, queryWriter)
|
||||
case rel.FilterNilOp:
|
||||
buffer.WriteField(table, filter.Field)
|
||||
buffer.WriteString(" IS NULL")
|
||||
case rel.FilterNotNilOp:
|
||||
buffer.WriteField(table, filter.Field)
|
||||
buffer.WriteString(" IS NOT NULL")
|
||||
case rel.FilterInOp,
|
||||
rel.FilterNinOp:
|
||||
f.WriteInclusion(buffer, table, filter, queryWriter)
|
||||
case rel.FilterLikeOp:
|
||||
buffer.WriteField(table, filter.Field)
|
||||
buffer.WriteString(" LIKE ")
|
||||
buffer.WriteValue(filter.Value)
|
||||
case rel.FilterNotLikeOp:
|
||||
buffer.WriteField(table, filter.Field)
|
||||
buffer.WriteString(" NOT LIKE ")
|
||||
buffer.WriteValue(filter.Value)
|
||||
case rel.FilterFragmentOp:
|
||||
buffer.WriteString(filter.Field)
|
||||
if !buffer.InlineValues {
|
||||
buffer.AddArguments(filter.Value.([]interface{})...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteLogical SQL to buffer.
|
||||
func (f Filter) WriteLogical(buffer *Buffer, table, op string, inner []rel.FilterQuery, queryWriter QueryWriter) {
|
||||
var (
|
||||
length = len(inner)
|
||||
)
|
||||
|
||||
if length > 1 {
|
||||
buffer.WriteByte('(')
|
||||
}
|
||||
|
||||
for i, c := range inner {
|
||||
f.Write(buffer, table, c, queryWriter)
|
||||
|
||||
if i < length-1 {
|
||||
buffer.WriteByte(' ')
|
||||
buffer.WriteString(op)
|
||||
buffer.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
|
||||
if length > 1 {
|
||||
buffer.WriteByte(')')
|
||||
}
|
||||
}
|
||||
|
||||
// WriteComparison SQL to buffer.
|
||||
func (f Filter) WriteComparison(buffer *Buffer, table string, filter rel.FilterQuery, queryWriter QueryWriter) {
|
||||
buffer.WriteField(table, filter.Field)
|
||||
|
||||
switch filter.Type {
|
||||
case rel.FilterEqOp:
|
||||
buffer.WriteByte('=')
|
||||
case rel.FilterNeOp:
|
||||
buffer.WriteString("<>")
|
||||
case rel.FilterLtOp:
|
||||
buffer.WriteByte('<')
|
||||
case rel.FilterLteOp:
|
||||
buffer.WriteString("<=")
|
||||
case rel.FilterGtOp:
|
||||
buffer.WriteByte('>')
|
||||
case rel.FilterGteOp:
|
||||
buffer.WriteString(">=")
|
||||
}
|
||||
|
||||
switch v := filter.Value.(type) {
|
||||
case rel.SubQuery:
|
||||
// For warped sub-queries
|
||||
f.WriteSubQuery(buffer, v, queryWriter)
|
||||
case rel.Query:
|
||||
// For sub-queries without warp
|
||||
f.WriteSubQuery(buffer, rel.SubQuery{Query: v}, queryWriter)
|
||||
default:
|
||||
// For simple values
|
||||
buffer.WriteValue(filter.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteInclusion SQL to buffer.
|
||||
func (f Filter) WriteInclusion(buffer *Buffer, table string, filter rel.FilterQuery, queryWriter QueryWriter) {
|
||||
var (
|
||||
values = filter.Value.([]interface{})
|
||||
)
|
||||
|
||||
if len(values) == 0 {
|
||||
if filter.Type == rel.FilterInOp {
|
||||
buffer.WriteString("1=0")
|
||||
} else {
|
||||
buffer.WriteString("1=1")
|
||||
}
|
||||
} else {
|
||||
buffer.WriteField(table, filter.Field)
|
||||
|
||||
if filter.Type == rel.FilterInOp {
|
||||
buffer.WriteString(" IN ")
|
||||
} else {
|
||||
buffer.WriteString(" NOT IN ")
|
||||
}
|
||||
|
||||
f.WriteInclusionValues(buffer, values, queryWriter)
|
||||
}
|
||||
}
|
||||
|
||||
func (f Filter) WriteInclusionValues(buffer *Buffer, values []interface{}, queryWriter QueryWriter) {
|
||||
if len(values) == 1 {
|
||||
if value, ok := values[0].(rel.Query); ok {
|
||||
f.WriteSubQuery(buffer, rel.SubQuery{Query: value}, queryWriter)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteByte('(')
|
||||
for i := 0; i < len(values); i++ {
|
||||
if i > 0 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
buffer.WriteValue(values[i])
|
||||
}
|
||||
buffer.WriteByte(')')
|
||||
}
|
||||
|
||||
func (f Filter) WriteSubQuery(buffer *Buffer, sub rel.SubQuery, queryWriter QueryWriter) {
|
||||
buffer.WriteString(sub.Prefix)
|
||||
buffer.WriteByte('(')
|
||||
queryWriter.Write(buffer, sub.Query)
|
||||
buffer.WriteByte(')')
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
// Index builder.
|
||||
type Index struct {
|
||||
BufferFactory BufferFactory
|
||||
Query QueryWriter
|
||||
Filter Filter
|
||||
DropIndexOnTable bool
|
||||
SupportFilter bool
|
||||
}
|
||||
|
||||
// Build sql query for index.
|
||||
func (i Index) Build(index rel.Index) string {
|
||||
buffer := i.BufferFactory.Create()
|
||||
|
||||
switch index.Op {
|
||||
case rel.SchemaCreate:
|
||||
i.WriteCreateIndex(&buffer, index)
|
||||
case rel.SchemaDrop:
|
||||
i.WriteDropIndex(&buffer, index)
|
||||
}
|
||||
|
||||
i.WriteOptions(&buffer, index.Options)
|
||||
buffer.WriteByte(';')
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// WriteCreateIndex to buffer
|
||||
func (i Index) WriteCreateIndex(buffer *Buffer, index rel.Index) {
|
||||
buffer.WriteString("CREATE ")
|
||||
if index.Unique {
|
||||
buffer.WriteString("UNIQUE ")
|
||||
}
|
||||
buffer.WriteString("INDEX ")
|
||||
|
||||
if index.Optional {
|
||||
buffer.WriteString("IF NOT EXISTS ")
|
||||
}
|
||||
|
||||
buffer.WriteEscape(index.Name)
|
||||
buffer.WriteString(" ON ")
|
||||
buffer.WriteEscape(index.Table)
|
||||
|
||||
buffer.WriteString(" (")
|
||||
for n, col := range index.Columns {
|
||||
if n > 0 {
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
buffer.WriteEscape(col)
|
||||
}
|
||||
buffer.WriteString(")")
|
||||
if !index.Filter.None() {
|
||||
if !i.SupportFilter {
|
||||
log.Print("[REL] Adapter does not support filtered/partial indexes")
|
||||
return
|
||||
}
|
||||
buffer.WriteString(" WHERE ")
|
||||
i.Filter.Write(buffer, "", index.Filter, i.Query)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteDropIndex to buffer
|
||||
func (i Index) WriteDropIndex(buffer *Buffer, index rel.Index) {
|
||||
buffer.WriteString("DROP INDEX ")
|
||||
|
||||
if index.Optional {
|
||||
buffer.WriteString("IF EXISTS ")
|
||||
}
|
||||
|
||||
buffer.WriteEscape(index.Name)
|
||||
|
||||
if i.DropIndexOnTable {
|
||||
buffer.WriteString(" ON ")
|
||||
buffer.WriteEscape(index.Table)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteOptions sql to buffer.
|
||||
func (i Index) WriteOptions(buffer *Buffer, options string) {
|
||||
if options == "" {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.WriteByte(' ')
|
||||
buffer.WriteString(options)
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
// Insert builder.
|
||||
type Insert struct {
|
||||
BufferFactory BufferFactory
|
||||
ReturningPrimaryValue bool
|
||||
InsertDefaultValues bool
|
||||
OnConflict OnConflict
|
||||
}
|
||||
|
||||
// Build sql query and its arguments.
|
||||
func (i Insert) Build(table string, primaryField string, mutates map[string]rel.Mutate, onConflict rel.OnConflict) (string, []interface{}) {
|
||||
var (
|
||||
buffer = i.BufferFactory.Create()
|
||||
)
|
||||
|
||||
i.WriteInsertInto(&buffer, table)
|
||||
i.WriteValues(&buffer, mutates)
|
||||
i.OnConflict.WriteMutates(&buffer, mutates, onConflict)
|
||||
i.WriteReturning(&buffer, primaryField)
|
||||
|
||||
buffer.WriteString(";")
|
||||
|
||||
return buffer.String(), buffer.Arguments()
|
||||
}
|
||||
|
||||
func (i Insert) WriteInsertInto(buffer *Buffer, table string) {
|
||||
buffer.WriteString("INSERT INTO ")
|
||||
buffer.WriteEscape(table)
|
||||
}
|
||||
|
||||
func (i Insert) WriteValues(buffer *Buffer, mutates map[string]rel.Mutate) {
|
||||
var (
|
||||
count = len(mutates)
|
||||
)
|
||||
|
||||
if count == 0 && i.InsertDefaultValues {
|
||||
buffer.WriteString(" DEFAULT VALUES")
|
||||
} else {
|
||||
buffer.WriteString(" (")
|
||||
|
||||
var (
|
||||
n = 0
|
||||
arguments = make([]interface{}, 0, count)
|
||||
)
|
||||
|
||||
for field, mut := range mutates {
|
||||
if mut.Type == rel.ChangeSetOp {
|
||||
if n > 0 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
|
||||
buffer.WriteEscape(field)
|
||||
arguments = append(arguments, mut.Value)
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteString(") VALUES (")
|
||||
|
||||
for i := range arguments {
|
||||
if i > 0 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
|
||||
buffer.WritePlaceholder()
|
||||
}
|
||||
buffer.AddArguments(arguments...)
|
||||
buffer.WriteByte(')')
|
||||
}
|
||||
}
|
||||
|
||||
func (i Insert) WriteReturning(buffer *Buffer, primaryField string) {
|
||||
if i.ReturningPrimaryValue && primaryField != "" {
|
||||
buffer.WriteString(" RETURNING ")
|
||||
buffer.WriteEscape(primaryField)
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
// InsertAll builder.
|
||||
type InsertAll struct {
|
||||
BufferFactory BufferFactory
|
||||
ReturningPrimaryValue bool
|
||||
OnConflict OnConflict
|
||||
}
|
||||
|
||||
// Build SQL string and its arguments.
|
||||
func (ia InsertAll) Build(table string, primaryField string, fields []string, bulkMutates []map[string]rel.Mutate, onConflict rel.OnConflict) (string, []interface{}) {
|
||||
var (
|
||||
buffer = ia.BufferFactory.Create()
|
||||
)
|
||||
|
||||
ia.WriteInsertInto(&buffer, table)
|
||||
ia.WriteValues(&buffer, fields, bulkMutates)
|
||||
ia.OnConflict.Write(&buffer, fields, onConflict)
|
||||
ia.WriteReturning(&buffer, primaryField)
|
||||
buffer.WriteString(";")
|
||||
|
||||
return buffer.String(), buffer.Arguments()
|
||||
}
|
||||
|
||||
func (ia InsertAll) WriteInsertInto(buffer *Buffer, table string) {
|
||||
buffer.WriteString("INSERT INTO ")
|
||||
buffer.WriteEscape(table)
|
||||
}
|
||||
|
||||
func (ia InsertAll) WriteValues(buffer *Buffer, fields []string, bulkMutates []map[string]rel.Mutate) {
|
||||
var (
|
||||
fieldsCount = len(fields)
|
||||
mutatesCount = len(bulkMutates)
|
||||
)
|
||||
|
||||
buffer.WriteString(" (")
|
||||
|
||||
for i := range fields {
|
||||
buffer.WriteEscape(fields[i])
|
||||
|
||||
if i < fieldsCount-1 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteString(") VALUES ")
|
||||
|
||||
for i, mutates := range bulkMutates {
|
||||
buffer.WriteByte('(')
|
||||
|
||||
for j, field := range fields {
|
||||
if mut, ok := mutates[field]; ok && mut.Type == rel.ChangeSetOp {
|
||||
buffer.WriteValue(mut.Value)
|
||||
} else {
|
||||
buffer.WriteString("DEFAULT")
|
||||
}
|
||||
|
||||
if j < fieldsCount-1 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
}
|
||||
|
||||
if i < mutatesCount-1 {
|
||||
buffer.WriteString("),")
|
||||
} else {
|
||||
buffer.WriteByte(')')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ia InsertAll) WriteReturning(buffer *Buffer, primaryField string) {
|
||||
if ia.ReturningPrimaryValue && primaryField != "" {
|
||||
buffer.WriteString(" RETURNING ")
|
||||
buffer.WriteEscape(primaryField)
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
type OnConflict struct {
|
||||
Statement string
|
||||
IgnoreStatement string
|
||||
UpdateStatement string
|
||||
TableQualifier string
|
||||
SupportKey bool
|
||||
UseValues bool
|
||||
}
|
||||
|
||||
func (oc OnConflict) Write(buffer *Buffer, fields []string, onConflict rel.OnConflict) {
|
||||
if onConflict.Keys == nil && onConflict.Fragment == "" {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.WriteByte(' ')
|
||||
buffer.WriteString(oc.Statement)
|
||||
oc.WriteKeys(buffer, onConflict)
|
||||
|
||||
buffer.WriteByte(' ')
|
||||
switch {
|
||||
case onConflict.Ignore:
|
||||
oc.WriteIgnore(buffer, fields)
|
||||
case onConflict.Replace:
|
||||
oc.WriteReplace(buffer, fields)
|
||||
case onConflict.Fragment != "":
|
||||
buffer.WriteString(onConflict.Fragment)
|
||||
buffer.AddArguments(onConflict.FragmentArgs...)
|
||||
}
|
||||
}
|
||||
|
||||
func (oc OnConflict) WriteMutates(buffer *Buffer, mutates map[string]rel.Mutate, onConflict rel.OnConflict) {
|
||||
var fields []string
|
||||
if onConflict.Replace || (onConflict.Ignore && oc.IgnoreStatement == "") {
|
||||
fields = make([]string, len(mutates))
|
||||
i := 0
|
||||
for field := range mutates {
|
||||
fields[i] = field
|
||||
i++
|
||||
}
|
||||
}
|
||||
oc.Write(buffer, fields, onConflict)
|
||||
}
|
||||
|
||||
func (oc OnConflict) WriteKeys(buffer *Buffer, onConflict rel.OnConflict) {
|
||||
if !oc.SupportKey || len(onConflict.Keys) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.WriteByte('(')
|
||||
for i := range onConflict.Keys {
|
||||
if i > 0 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
buffer.WriteEscape(onConflict.Keys[i])
|
||||
}
|
||||
buffer.WriteByte(')')
|
||||
}
|
||||
|
||||
func (oc OnConflict) WriteIgnore(buffer *Buffer, fields []string) {
|
||||
if oc.IgnoreStatement == "" && len(fields) != 0 {
|
||||
// mysql specific
|
||||
buffer.WriteString(oc.UpdateStatement)
|
||||
buffer.WriteByte(' ')
|
||||
|
||||
buffer.WriteEscape(fields[0])
|
||||
buffer.WriteByte('=')
|
||||
buffer.WriteEscape(fields[0])
|
||||
} else {
|
||||
buffer.WriteString(oc.IgnoreStatement)
|
||||
}
|
||||
}
|
||||
|
||||
func (oc OnConflict) WriteReplace(buffer *Buffer, fields []string) {
|
||||
buffer.WriteString(oc.UpdateStatement)
|
||||
buffer.WriteByte(' ')
|
||||
|
||||
for i, field := range fields {
|
||||
if i > 0 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
|
||||
buffer.WriteEscape(field)
|
||||
buffer.WriteByte('=')
|
||||
|
||||
if oc.UseValues {
|
||||
buffer.WriteString("VALUES(")
|
||||
buffer.WriteEscape(field)
|
||||
buffer.WriteByte(')')
|
||||
} else {
|
||||
buffer.WriteField(oc.TableQualifier, field)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
type QueryWriter interface {
|
||||
Write(buffer *Buffer, query rel.Query)
|
||||
}
|
||||
|
||||
// Query builder.
|
||||
type Query struct {
|
||||
BufferFactory BufferFactory
|
||||
Filter Filter
|
||||
}
|
||||
|
||||
// Build SQL string and it arguments.
|
||||
func (q Query) Build(query rel.Query) (string, []interface{}) {
|
||||
var (
|
||||
buffer = q.BufferFactory.Create()
|
||||
)
|
||||
|
||||
q.Write(&buffer, query)
|
||||
|
||||
return buffer.String(), buffer.Arguments()
|
||||
}
|
||||
|
||||
// Write SQL to buffer.
|
||||
func (q Query) Write(buffer *Buffer, query rel.Query) {
|
||||
if query.SQLQuery.Statement != "" {
|
||||
buffer.WriteString(query.SQLQuery.Statement)
|
||||
buffer.AddArguments(query.SQLQuery.Values...)
|
||||
return
|
||||
}
|
||||
|
||||
rootQuery := buffer.Len() == 0
|
||||
|
||||
q.WriteSelect(buffer, query.Table, query.SelectQuery)
|
||||
q.WriteQuery(buffer, query)
|
||||
|
||||
if rootQuery {
|
||||
buffer.WriteByte(';')
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSelect SQL to buffer.
|
||||
func (q Query) WriteSelect(buffer *Buffer, table string, selectQuery rel.SelectQuery) {
|
||||
if len(selectQuery.Fields) == 0 {
|
||||
buffer.WriteString("SELECT ")
|
||||
if selectQuery.OnlyDistinct {
|
||||
buffer.WriteString("DISTINCT ")
|
||||
}
|
||||
buffer.WriteField(table, "*")
|
||||
return
|
||||
}
|
||||
|
||||
buffer.WriteString("SELECT ")
|
||||
|
||||
if selectQuery.OnlyDistinct {
|
||||
buffer.WriteString("DISTINCT ")
|
||||
}
|
||||
|
||||
l := len(selectQuery.Fields) - 1
|
||||
for i, f := range selectQuery.Fields {
|
||||
buffer.WriteField(table, f)
|
||||
|
||||
if i < l {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteQuery SQL to buffer.
|
||||
func (q Query) WriteQuery(buffer *Buffer, query rel.Query) {
|
||||
q.WriteFrom(buffer, query.Table)
|
||||
q.WriteJoin(buffer, query.Table, query.JoinQuery)
|
||||
q.WriteWhere(buffer, query.Table, query.WhereQuery)
|
||||
|
||||
if len(query.GroupQuery.Fields) > 0 {
|
||||
q.WriteGroupBy(buffer, query.Table, query.GroupQuery.Fields)
|
||||
q.WriteHaving(buffer, query.Table, query.GroupQuery.Filter)
|
||||
}
|
||||
|
||||
q.WriteOrderBy(buffer, query.Table, query.SortQuery)
|
||||
q.WriteLimitOffet(buffer, query.LimitQuery, query.OffsetQuery)
|
||||
|
||||
if query.LockQuery != "" {
|
||||
buffer.WriteByte(' ')
|
||||
buffer.WriteString(string(query.LockQuery))
|
||||
}
|
||||
}
|
||||
|
||||
// WriteFrom SQL to buffer.
|
||||
func (q Query) WriteFrom(buffer *Buffer, table string) {
|
||||
buffer.WriteString(" FROM ")
|
||||
buffer.WriteEscape(table)
|
||||
}
|
||||
|
||||
// WriteJoin SQL to buffer.
|
||||
func (q Query) WriteJoin(buffer *Buffer, table string, joins []rel.JoinQuery) {
|
||||
if len(joins) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, join := range joins {
|
||||
var (
|
||||
from = join.From
|
||||
to = join.To
|
||||
)
|
||||
|
||||
// TODO: move this to core functionality, and infer join condition using assoc data.
|
||||
if join.Arguments == nil && (join.From == "" || join.To == "") {
|
||||
from = table + "." + strings.TrimSuffix(join.Table, "s") + "_id"
|
||||
to = join.Table + ".id"
|
||||
}
|
||||
|
||||
buffer.WriteByte(' ')
|
||||
buffer.WriteString(join.Mode)
|
||||
buffer.WriteByte(' ')
|
||||
|
||||
if join.Table != "" {
|
||||
buffer.WriteEscape(join.Table)
|
||||
buffer.WriteString(" ON ")
|
||||
buffer.WriteEscape(from)
|
||||
buffer.WriteString("=")
|
||||
buffer.WriteEscape(to)
|
||||
if !join.Filter.None() {
|
||||
buffer.WriteString(" AND ")
|
||||
q.Filter.Write(buffer, join.Table, join.Filter, q)
|
||||
}
|
||||
}
|
||||
|
||||
buffer.AddArguments(join.Arguments...)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteWhere SQL to buffer.
|
||||
func (q Query) WriteWhere(buffer *Buffer, table string, filter rel.FilterQuery) {
|
||||
if filter.None() {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.WriteString(" WHERE ")
|
||||
q.Filter.Write(buffer, table, filter, q)
|
||||
}
|
||||
|
||||
// WriteGroupBy SQL to buffer.
|
||||
func (q Query) WriteGroupBy(buffer *Buffer, table string, fields []string) {
|
||||
buffer.WriteString(" GROUP BY ")
|
||||
|
||||
l := len(fields) - 1
|
||||
for i, f := range fields {
|
||||
buffer.WriteField(table, f)
|
||||
|
||||
if i < l {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteHaving SQL to buffer.
|
||||
func (q Query) WriteHaving(buffer *Buffer, table string, filter rel.FilterQuery) {
|
||||
if filter.None() {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.WriteString(" HAVING ")
|
||||
q.Filter.Write(buffer, table, filter, q)
|
||||
}
|
||||
|
||||
// WriteOrderBy SQL to buffer.
|
||||
func (q Query) WriteOrderBy(buffer *Buffer, table string, orders []rel.SortQuery) {
|
||||
var (
|
||||
length = len(orders)
|
||||
)
|
||||
|
||||
if length == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.WriteString(" ORDER BY ")
|
||||
for i, order := range orders {
|
||||
if i > 0 {
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
|
||||
buffer.WriteField(table, order.Field)
|
||||
|
||||
if order.Asc() {
|
||||
buffer.WriteString(" ASC")
|
||||
} else {
|
||||
buffer.WriteString(" DESC")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteLimitOffet SQL to buffer.
|
||||
func (q Query) WriteLimitOffet(buffer *Buffer, limit rel.Limit, offset rel.Offset) {
|
||||
if limit > 0 {
|
||||
buffer.WriteString(" LIMIT ")
|
||||
buffer.WriteString(strconv.Itoa(int(limit)))
|
||||
|
||||
if offset > 0 {
|
||||
buffer.WriteString(" OFFSET ")
|
||||
buffer.WriteString(strconv.Itoa(int(offset)))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Quoter returns safe and valid SQL strings to use when building a SQL text.
|
||||
type Quoter interface {
|
||||
// ID quotes identifiers such as schema, table, or column names.
|
||||
// ID does not operate on multipart identifiers such as "public.Table",
|
||||
// it only operates on single identifiers such as "public" and "Table".
|
||||
ID(name string) string
|
||||
|
||||
// Value quotes database values such as string or []byte types as strings
|
||||
// that are suitable and safe to embed in SQL text. The returned value
|
||||
// of a string will include all surrounding quotes.
|
||||
//
|
||||
// If a value type is not supported it must panic.
|
||||
Value(v interface{}) string
|
||||
}
|
||||
|
||||
// Quote is default implementation of Quoter interface.
|
||||
type Quote struct {
|
||||
IDPrefix string
|
||||
IDSuffix string
|
||||
IDSuffixEscapeChar string
|
||||
ValueQuote string
|
||||
ValueQuoteEscapeChar string
|
||||
}
|
||||
|
||||
func (q Quote) ID(name string) string {
|
||||
return q.IDPrefix + strings.ReplaceAll(name, q.IDSuffix, q.IDSuffixEscapeChar+q.IDSuffix) + q.IDSuffix
|
||||
}
|
||||
|
||||
func (q Quote) Value(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
default:
|
||||
panic("unsupported value")
|
||||
case string:
|
||||
return q.ValueQuote + strings.ReplaceAll(v, q.ValueQuote, q.ValueQuoteEscapeChar+q.ValueQuote) + q.ValueQuote
|
||||
}
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
type ColumnMapper func(*rel.Column) (string, int, int)
|
||||
|
||||
// Table builder.
|
||||
type Table struct {
|
||||
BufferFactory BufferFactory
|
||||
ColumnMapper ColumnMapper
|
||||
}
|
||||
|
||||
// Build SQL query for table creation and modification.
|
||||
func (t Table) Build(table rel.Table) string {
|
||||
var (
|
||||
buffer = t.BufferFactory.Create()
|
||||
)
|
||||
|
||||
switch table.Op {
|
||||
case rel.SchemaCreate:
|
||||
t.WriteCreateTable(&buffer, table)
|
||||
case rel.SchemaAlter:
|
||||
t.WriteAlterTable(&buffer, table)
|
||||
case rel.SchemaRename:
|
||||
t.WriteRenameTable(&buffer, table)
|
||||
case rel.SchemaDrop:
|
||||
t.WriteDropTable(&buffer, table)
|
||||
}
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// WriteCreateTable query to buffer.
|
||||
func (t Table) WriteCreateTable(buffer *Buffer, table rel.Table) {
|
||||
buffer.WriteString("CREATE TABLE ")
|
||||
|
||||
if table.Optional {
|
||||
buffer.WriteString("IF NOT EXISTS ")
|
||||
}
|
||||
|
||||
buffer.WriteEscape(table.Name)
|
||||
if len(table.Definitions) > 0 {
|
||||
buffer.WriteString(" (")
|
||||
|
||||
for i, def := range table.Definitions {
|
||||
if i > 0 {
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
switch v := def.(type) {
|
||||
case rel.Column:
|
||||
t.WriteColumn(buffer, v)
|
||||
case rel.Key:
|
||||
t.WriteKey(buffer, v)
|
||||
case rel.Raw:
|
||||
buffer.WriteString(string(v))
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteByte(')')
|
||||
}
|
||||
t.WriteOptions(buffer, table.Options)
|
||||
buffer.WriteByte(';')
|
||||
}
|
||||
|
||||
// WriteAlterTable query to buffer.
|
||||
func (t Table) WriteAlterTable(buffer *Buffer, table rel.Table) {
|
||||
for _, def := range table.Definitions {
|
||||
buffer.WriteString("ALTER TABLE ")
|
||||
buffer.WriteEscape(table.Name)
|
||||
buffer.WriteByte(' ')
|
||||
|
||||
switch v := def.(type) {
|
||||
case rel.Column:
|
||||
switch v.Op {
|
||||
case rel.SchemaCreate:
|
||||
buffer.WriteString("ADD COLUMN ")
|
||||
t.WriteColumn(buffer, v)
|
||||
case rel.SchemaRename:
|
||||
// Add Change
|
||||
buffer.WriteString("RENAME COLUMN ")
|
||||
buffer.WriteEscape(v.Name)
|
||||
buffer.WriteString(" TO ")
|
||||
buffer.WriteEscape(v.Rename)
|
||||
case rel.SchemaDrop:
|
||||
buffer.WriteString("DROP COLUMN ")
|
||||
buffer.WriteEscape(v.Name)
|
||||
}
|
||||
case rel.Key:
|
||||
// TODO: Rename and Drop, PR welcomed.
|
||||
switch v.Op {
|
||||
case rel.SchemaCreate:
|
||||
buffer.WriteString("ADD ")
|
||||
t.WriteKey(buffer, v)
|
||||
}
|
||||
}
|
||||
|
||||
t.WriteOptions(buffer, table.Options)
|
||||
buffer.WriteByte(';')
|
||||
}
|
||||
}
|
||||
|
||||
// WriteRenameTable query to buffer.
|
||||
func (t Table) WriteRenameTable(buffer *Buffer, table rel.Table) {
|
||||
buffer.WriteString("ALTER TABLE ")
|
||||
buffer.WriteEscape(table.Name)
|
||||
buffer.WriteString(" RENAME TO ")
|
||||
buffer.WriteEscape(table.Rename)
|
||||
buffer.WriteByte(';')
|
||||
}
|
||||
|
||||
// WriteDropTable query to buffer.
|
||||
func (t Table) WriteDropTable(buffer *Buffer, table rel.Table) {
|
||||
buffer.WriteString("DROP TABLE ")
|
||||
|
||||
if table.Optional {
|
||||
buffer.WriteString("IF EXISTS ")
|
||||
}
|
||||
|
||||
buffer.WriteEscape(table.Name)
|
||||
buffer.WriteByte(';')
|
||||
}
|
||||
|
||||
// WriteColumn definition to buffer.
|
||||
func (t Table) WriteColumn(buffer *Buffer, column rel.Column) {
|
||||
var (
|
||||
typ, m, n = t.ColumnMapper(&column)
|
||||
)
|
||||
|
||||
buffer.WriteEscape(column.Name)
|
||||
buffer.WriteByte(' ')
|
||||
buffer.WriteString(typ)
|
||||
|
||||
if m != 0 {
|
||||
buffer.WriteByte('(')
|
||||
buffer.WriteString(strconv.Itoa(m))
|
||||
|
||||
if n != 0 {
|
||||
buffer.WriteByte(',')
|
||||
buffer.WriteString(strconv.Itoa(n))
|
||||
}
|
||||
|
||||
buffer.WriteByte(')')
|
||||
}
|
||||
|
||||
if column.Unsigned {
|
||||
buffer.WriteString(" UNSIGNED")
|
||||
}
|
||||
|
||||
if column.Unique {
|
||||
buffer.WriteString(" UNIQUE")
|
||||
}
|
||||
|
||||
if column.Required {
|
||||
buffer.WriteString(" NOT NULL")
|
||||
}
|
||||
|
||||
if column.Primary {
|
||||
buffer.WriteString(" PRIMARY KEY")
|
||||
}
|
||||
|
||||
if column.Default != nil {
|
||||
buffer.WriteString(" DEFAULT ")
|
||||
buffer.WriteValue(column.Default)
|
||||
}
|
||||
|
||||
t.WriteOptions(buffer, column.Options)
|
||||
}
|
||||
|
||||
// WriteKey definition to buffer.
|
||||
func (t Table) WriteKey(buffer *Buffer, key rel.Key) {
|
||||
var (
|
||||
typ = string(key.Type)
|
||||
)
|
||||
|
||||
buffer.WriteString(typ)
|
||||
|
||||
if key.Name != "" {
|
||||
buffer.WriteByte(' ')
|
||||
buffer.WriteEscape(key.Name)
|
||||
}
|
||||
|
||||
buffer.WriteString(" (")
|
||||
for i, col := range key.Columns {
|
||||
if i > 0 {
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
buffer.WriteEscape(col)
|
||||
}
|
||||
buffer.WriteString(")")
|
||||
|
||||
if key.Type == rel.ForeignKey {
|
||||
buffer.WriteString(" REFERENCES ")
|
||||
buffer.WriteEscape(key.Reference.Table)
|
||||
|
||||
buffer.WriteString(" (")
|
||||
for i, col := range key.Reference.Columns {
|
||||
if i > 0 {
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
buffer.WriteEscape(col)
|
||||
}
|
||||
buffer.WriteString(")")
|
||||
|
||||
if onDelete := key.Reference.OnDelete; onDelete != "" {
|
||||
buffer.WriteString(" ON DELETE ")
|
||||
buffer.WriteString(onDelete)
|
||||
}
|
||||
|
||||
if onUpdate := key.Reference.OnUpdate; onUpdate != "" {
|
||||
buffer.WriteString(" ON UPDATE ")
|
||||
buffer.WriteString(onUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
t.WriteOptions(buffer, key.Options)
|
||||
}
|
||||
|
||||
// WriteOptions sql to buffer.
|
||||
func (t Table) WriteOptions(buffer *Buffer, options string) {
|
||||
if options == "" {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.WriteByte(' ')
|
||||
buffer.WriteString(options)
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
// Update builder.
|
||||
type Update struct {
|
||||
BufferFactory BufferFactory
|
||||
Query QueryWriter
|
||||
Filter Filter
|
||||
}
|
||||
|
||||
// Build SQL string and it arguments.
|
||||
func (u Update) Build(table string, primaryField string, mutates map[string]rel.Mutate, filter rel.FilterQuery) (string, []interface{}) {
|
||||
var (
|
||||
buffer = u.BufferFactory.Create()
|
||||
)
|
||||
|
||||
buffer.WriteString("UPDATE ")
|
||||
buffer.WriteEscape(table)
|
||||
buffer.WriteString(" SET ")
|
||||
|
||||
i := 0
|
||||
for field, mut := range mutates {
|
||||
if field == primaryField {
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
i++
|
||||
|
||||
switch mut.Type {
|
||||
case rel.ChangeSetOp:
|
||||
buffer.WriteEscape(field)
|
||||
buffer.WriteByte('=')
|
||||
buffer.WriteValue(mut.Value)
|
||||
case rel.ChangeIncOp:
|
||||
buffer.WriteEscape(field)
|
||||
buffer.WriteByte('=')
|
||||
buffer.WriteEscape(field)
|
||||
buffer.WriteByte('+')
|
||||
buffer.WriteValue(mut.Value)
|
||||
case rel.ChangeFragmentOp:
|
||||
buffer.WriteString(field)
|
||||
buffer.AddArguments(mut.Value.([]interface{})...)
|
||||
}
|
||||
}
|
||||
|
||||
if !filter.None() {
|
||||
buffer.WriteString(" WHERE ")
|
||||
u.Filter.Write(&buffer, table, filter, u.Query)
|
||||
}
|
||||
|
||||
buffer.WriteString(";")
|
||||
|
||||
return buffer.String(), buffer.Arguments()
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// Cursor used for retrieving result.
|
||||
type Cursor struct {
|
||||
*sql.Rows
|
||||
}
|
||||
|
||||
// Fields returned in the result.
|
||||
func (c *Cursor) Fields() ([]string, error) {
|
||||
return c.Columns()
|
||||
}
|
||||
|
||||
// NopScanner for this adapter.
|
||||
func (c *Cursor) NopScanner() interface{} {
|
||||
return &sql.RawBytes{}
|
||||
}
|
@ -0,0 +1,302 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
// ErrorMapper function.
|
||||
type ErrorMapper func(error) error
|
||||
|
||||
// IncrementFunc function.
|
||||
type IncrementFunc func(SQL) int
|
||||
|
||||
// SQL base adapter.
|
||||
type SQL struct {
|
||||
QueryBuilder QueryBuilder
|
||||
InsertBuilder InsertBuilder
|
||||
InsertAllBuilder InsertAllBuilder
|
||||
UpdateBuilder UpdateBuilder
|
||||
DeleteBuilder DeleteBuilder
|
||||
TableBuilder TableBuilder
|
||||
IndexBuilder IndexBuilder
|
||||
IncrementFunc IncrementFunc
|
||||
ErrorMapper ErrorMapper
|
||||
DB *sql.DB
|
||||
Tx *sql.Tx
|
||||
Savepoint int
|
||||
Instrumenter rel.Instrumenter
|
||||
}
|
||||
|
||||
// Instrumentation set instrumenter for this adapter.
|
||||
func (s *SQL) Instrumentation(instrumenter rel.Instrumenter) {
|
||||
s.Instrumenter = instrumenter
|
||||
}
|
||||
|
||||
// DoExec using active database connection.
|
||||
func (s SQL) DoExec(ctx context.Context, statement string, args []interface{}) (sql.Result, error) {
|
||||
var (
|
||||
err error
|
||||
result sql.Result
|
||||
finish = s.Instrumenter.Observe(ctx, "adapter-exec", statement)
|
||||
)
|
||||
|
||||
if s.Tx != nil {
|
||||
result, err = s.Tx.ExecContext(ctx, statement, args...)
|
||||
} else {
|
||||
result, err = s.DB.ExecContext(ctx, statement, args...)
|
||||
}
|
||||
|
||||
finish(err)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// DoQuery using active database connection.
|
||||
func (s SQL) DoQuery(ctx context.Context, statement string, args []interface{}) (*sql.Rows, error) {
|
||||
var (
|
||||
err error
|
||||
rows *sql.Rows
|
||||
)
|
||||
|
||||
finish := s.Instrumenter.Observe(ctx, "adapter-query", statement)
|
||||
if s.Tx != nil {
|
||||
rows, err = s.Tx.QueryContext(ctx, statement, args...)
|
||||
} else {
|
||||
rows, err = s.DB.QueryContext(ctx, statement, args...)
|
||||
}
|
||||
finish(err)
|
||||
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// Begin begins a new transaction.
|
||||
func (s SQL) Begin(ctx context.Context) (rel.Adapter, error) {
|
||||
var (
|
||||
tx *sql.Tx
|
||||
savepoint int
|
||||
err error
|
||||
)
|
||||
|
||||
finish := s.Instrumenter.Observe(ctx, "adapter-begin", "begin transaction")
|
||||
|
||||
if s.Tx != nil {
|
||||
tx = s.Tx
|
||||
savepoint = s.Savepoint + 1
|
||||
_, err = s.Tx.ExecContext(ctx, "SAVEPOINT s"+strconv.Itoa(savepoint)+";")
|
||||
} else {
|
||||
tx, err = s.DB.BeginTx(ctx, nil)
|
||||
}
|
||||
|
||||
finish(err)
|
||||
|
||||
return &SQL{
|
||||
QueryBuilder: s.QueryBuilder,
|
||||
InsertBuilder: s.InsertBuilder,
|
||||
InsertAllBuilder: s.InsertAllBuilder,
|
||||
UpdateBuilder: s.UpdateBuilder,
|
||||
DeleteBuilder: s.DeleteBuilder,
|
||||
TableBuilder: s.TableBuilder,
|
||||
IndexBuilder: s.IndexBuilder,
|
||||
IncrementFunc: s.IncrementFunc,
|
||||
ErrorMapper: s.ErrorMapper,
|
||||
Tx: tx,
|
||||
Savepoint: savepoint,
|
||||
Instrumenter: s.Instrumenter,
|
||||
}, s.ErrorMapper(err)
|
||||
}
|
||||
|
||||
// Commit commits current transaction.
|
||||
func (s SQL) Commit(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
finish := s.Instrumenter.Observe(ctx, "adapter-commit", "commit transaction")
|
||||
|
||||
if s.Tx == nil {
|
||||
err = errors.New("unable to commit outside transaction")
|
||||
} else if s.Savepoint > 0 {
|
||||
_, err = s.Tx.ExecContext(ctx, "RELEASE SAVEPOINT s"+strconv.Itoa(s.Savepoint)+";")
|
||||
} else {
|
||||
err = s.Tx.Commit()
|
||||
}
|
||||
|
||||
finish(err)
|
||||
|
||||
return s.ErrorMapper(err)
|
||||
}
|
||||
|
||||
// Rollback revert current transaction.
|
||||
func (s SQL) Rollback(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
finish := s.Instrumenter.Observe(ctx, "adapter-rollback", "rollback transaction")
|
||||
|
||||
if s.Tx == nil {
|
||||
err = errors.New("unable to rollback outside transaction")
|
||||
} else if s.Savepoint > 0 {
|
||||
_, err = s.Tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT s"+strconv.Itoa(s.Savepoint)+";")
|
||||
} else {
|
||||
err = s.Tx.Rollback()
|
||||
}
|
||||
|
||||
finish(err)
|
||||
|
||||
return s.ErrorMapper(err)
|
||||
}
|
||||
|
||||
// Ping database.
|
||||
func (s SQL) Ping(ctx context.Context) error {
|
||||
return s.DB.PingContext(ctx)
|
||||
}
|
||||
|
||||
// Close database connection.
|
||||
//
|
||||
// TODO: add closer to adapter interface
|
||||
func (s SQL) Close() error {
|
||||
return s.DB.Close()
|
||||
}
|
||||
|
||||
// Query performs query operation.
|
||||
func (s SQL) Query(ctx context.Context, query rel.Query) (rel.Cursor, error) {
|
||||
var (
|
||||
statement, args = s.QueryBuilder.Build(query)
|
||||
rows, err = s.DoQuery(ctx, statement, args)
|
||||
)
|
||||
|
||||
return &Cursor{Rows: rows}, s.ErrorMapper(err)
|
||||
}
|
||||
|
||||
// Exec performs exec operation.
|
||||
func (s SQL) Exec(ctx context.Context, statement string, args []interface{}) (int64, int64, error) {
|
||||
var (
|
||||
res, err = s.DoExec(ctx, statement, args)
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, s.ErrorMapper(err)
|
||||
}
|
||||
|
||||
lastID, _ := res.LastInsertId()
|
||||
rowCount, _ := res.RowsAffected()
|
||||
|
||||
return lastID, rowCount, nil
|
||||
}
|
||||
|
||||
// Aggregate record using given query.
|
||||
func (s SQL) Aggregate(ctx context.Context, query rel.Query, mode string, field string) (int, error) {
|
||||
var (
|
||||
out sql.NullInt64
|
||||
aggregateField = "^" + mode + "(" + field + ") AS result"
|
||||
aggregateQuery = query.Select(append([]string{aggregateField}, query.GroupQuery.Fields...)...)
|
||||
statement, args = s.QueryBuilder.Build(aggregateQuery)
|
||||
rows, err = s.DoQuery(ctx, statement, args)
|
||||
)
|
||||
|
||||
defer rows.Close()
|
||||
if err == nil && rows.Next() {
|
||||
rows.Scan(&out)
|
||||
}
|
||||
|
||||
return int(out.Int64), s.ErrorMapper(err)
|
||||
}
|
||||
|
||||
// Insert inserts a record to database and returns its id.
|
||||
func (s SQL) Insert(ctx context.Context, query rel.Query, primaryField string, mutates map[string]rel.Mutate, onConflict rel.OnConflict) (interface{}, error) {
|
||||
var (
|
||||
statement, args = s.InsertBuilder.Build(query.Table, primaryField, mutates, onConflict)
|
||||
id, _, err = s.Exec(ctx, statement, args)
|
||||
)
|
||||
|
||||
return id, err
|
||||
}
|
||||
|
||||
// InsertAll inserts multiple records to database and returns its ids.
|
||||
func (s SQL) InsertAll(ctx context.Context, query rel.Query, primaryField string, fields []string, bulkMutates []map[string]rel.Mutate, onConflict rel.OnConflict) ([]interface{}, error) {
|
||||
var (
|
||||
statement, args = s.InsertAllBuilder.Build(query.Table, primaryField, fields, bulkMutates, onConflict)
|
||||
id, _, err = s.Exec(ctx, statement, args)
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
ids = make([]interface{}, len(bulkMutates))
|
||||
inc = 1
|
||||
)
|
||||
|
||||
if s.IncrementFunc != nil {
|
||||
inc = s.IncrementFunc(s)
|
||||
}
|
||||
|
||||
if inc < 0 {
|
||||
id = id + int64((len(bulkMutates)-1)*inc)
|
||||
inc *= -1
|
||||
}
|
||||
|
||||
if primaryField != "" {
|
||||
counter := 0
|
||||
for i := range ids {
|
||||
if mut, ok := bulkMutates[i][primaryField]; ok {
|
||||
ids[i] = mut.Value
|
||||
id = toInt64(ids[i])
|
||||
counter = 1
|
||||
} else {
|
||||
ids[i] = id + int64(counter*inc)
|
||||
counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// Update updates a record in database.
|
||||
func (s SQL) Update(ctx context.Context, query rel.Query, primaryField string, mutates map[string]rel.Mutate) (int, error) {
|
||||
var (
|
||||
statement, args = s.UpdateBuilder.Build(query.Table, primaryField, mutates, query.WhereQuery)
|
||||
_, updatedCount, err = s.Exec(ctx, statement, args)
|
||||
)
|
||||
|
||||
return int(updatedCount), err
|
||||
}
|
||||
|
||||
// Delete deletes all results that match the query.
|
||||
func (s SQL) Delete(ctx context.Context, query rel.Query) (int, error) {
|
||||
var (
|
||||
statement, args = s.DeleteBuilder.Build(query.Table, query.WhereQuery)
|
||||
_, deletedCount, err = s.Exec(ctx, statement, args)
|
||||
)
|
||||
|
||||
return int(deletedCount), err
|
||||
}
|
||||
|
||||
// SchemaApply performs migration to database.
|
||||
func (s SQL) SchemaApply(ctx context.Context, migration rel.Migration) error {
|
||||
var (
|
||||
statement string
|
||||
)
|
||||
|
||||
switch v := migration.(type) {
|
||||
case rel.Table:
|
||||
statement = s.TableBuilder.Build(v)
|
||||
case rel.Index:
|
||||
statement = s.IndexBuilder.Build(v)
|
||||
case rel.Raw:
|
||||
statement = string(v)
|
||||
}
|
||||
|
||||
_, _, err := s.Exec(ctx, statement, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply performs migration to database.
|
||||
//
|
||||
// Deprecated: Use Schema Apply instead.
|
||||
func (s SQL) Apply(ctx context.Context, migration rel.Migration) error {
|
||||
return s.SchemaApply(ctx, migration)
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rel/rel"
|
||||
)
|
||||
|
||||
// DefaultTimeLayout default time layout.
|
||||
const DefaultTimeLayout = "2006-01-02 15:04:05"
|
||||
|
||||
// ColumnMapper function.
|
||||
func ColumnMapper(column *rel.Column) (string, int, int) {
|
||||
var (
|
||||
typ string
|
||||
m, n int
|
||||
timeLayout = DefaultTimeLayout
|
||||
)
|
||||
|
||||
switch column.Type {
|
||||
case rel.ID:
|
||||
typ = "INT UNSIGNED AUTO_INCREMENT"
|
||||
case rel.BigID:
|
||||
typ = "BIGINT UNSIGNED AUTO_INCREMENT"
|
||||
case rel.Bool:
|
||||
typ = "BOOL"
|
||||
case rel.Int:
|
||||
typ = "INT"
|
||||
m = column.Limit
|
||||
case rel.BigInt:
|
||||
typ = "BIGINT"
|
||||
m = column.Limit
|
||||
case rel.Float:
|
||||
typ = "FLOAT"
|
||||
m = column.Precision
|
||||
case rel.Decimal:
|
||||
typ = "DECIMAL"
|
||||
m = column.Precision
|
||||
n = column.Scale
|
||||
case rel.String:
|
||||
typ = "VARCHAR"
|
||||
m = column.Limit
|
||||
if m == 0 {
|
||||
m = 255
|
||||
}
|
||||
case rel.Text:
|
||||
typ = "TEXT"
|
||||
m = column.Limit
|
||||
case rel.JSON:
|
||||
typ = "TEXT"
|
||||
case rel.Date:
|
||||
typ = "DATE"
|
||||
timeLayout = "2006-01-02"
|
||||
case rel.DateTime:
|
||||
typ = "DATETIME"
|
||||
case rel.Time:
|
||||
typ = "TIME"
|
||||
timeLayout = "15:04:05"
|
||||
default:
|
||||
typ = string(column.Type)
|
||||
}
|
||||
|
||||
if t, ok := column.Default.(time.Time); ok {
|
||||
column.Default = t.Format(timeLayout)
|
||||
}
|
||||
|
||||
return typ, m, n
|
||||
}
|
||||
|
||||
// ExtractString between two string.
|
||||
func ExtractString(s, left, right string) string {
|
||||
var (
|
||||
start = strings.Index(s, left)
|
||||
end = strings.LastIndex(s, right)
|
||||
)
|
||||
|
||||
if start < 0 || end < 0 || start+len(left) >= end {
|
||||
return s
|
||||
}
|
||||
|
||||
return s[start+len(left) : end]
|
||||
}
|
||||
|
||||
func toInt64(i interface{}) int64 {
|
||||
var result int64
|
||||
|
||||
switch s := i.(type) {
|
||||
case int:
|
||||
result = int64(s)
|
||||
case int64:
|
||||
result = s
|
||||
case int32:
|
||||
result = int64(s)
|
||||
case int16:
|
||||
result = int64(s)
|
||||
case int8:
|
||||
result = int64(s)
|
||||
case uint:
|
||||
result = int64(s)
|
||||
case uint64:
|
||||
result = int64(s)
|
||||
case uint32:
|
||||
result = int64(s)
|
||||
case uint16:
|
||||
result = int64(s)
|
||||
case uint8:
|
||||
result = int64(s)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
language: go
|
||||
arch:
|
||||
- amd64
|
||||
- ppc64le
|
||||
go:
|
||||
- 1.8
|
||||
- 1.9
|
||||
- tip
|
||||
# Disable version go:1.8
|
||||
jobs:
|
||||
exclude:
|
||||
- arch: amd64
|
||||
go: 1.8
|
||||
- arch: ppc64le
|
||||
go: 1.8
|
||||
|
||||
install: go get -t -d -v ./... && go build -v ./...
|
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2015 Serenize UG (haftungsbeschränkt)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,25 @@
|
||||
# snaker
|
||||
|
||||
[![Build Status](https://travis-ci.org/serenize/snaker.svg?branch=master)](https://travis-ci.org/serenize/snaker)
|
||||
[![GoDoc](https://godoc.org/github.com/serenize/snaker?status.svg)](https://godoc.org/github.com/serenize/snaker)
|
||||
|
||||
This is a small utility to convert camel cased strings to snake case and back, except some defined words.
|
||||
|
||||
## QBS Usage
|
||||
|
||||
To replace the original toSnake and back algorithms for [https://github.com/coocood/qbs](https://github.com/coocood/qbs)
|
||||
you can easily use snaker:
|
||||
|
||||
Import snaker
|
||||
```go
|
||||
import (
|
||||
github.com/coocood/qbs
|
||||
github.com/serenize/snaker
|
||||
)
|
||||
```
|
||||
|
||||
Register the snaker methods to qbs
|
||||
```go
|
||||
qbs.ColumnNameToFieldName = snaker.SnakeToCamel
|
||||
qbs.FieldNameToColumnName = snaker.CamelToSnake
|
||||
```
|
@ -0,0 +1,150 @@
|
||||
// Package snaker provides methods to convert CamelCase names to snake_case and back.
|
||||
// It considers the list of allowed initialsms used by github.com/golang/lint/golint (e.g. ID or HTTP)
|
||||
package snaker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// CamelToSnake converts a given string to snake case
|
||||
func CamelToSnake(s string) string {
|
||||
var result string
|
||||
var words []string
|
||||
var lastPos int
|
||||
rs := []rune(s)
|
||||
|
||||
for i := 0; i < len(rs); i++ {
|
||||
if i > 0 && unicode.IsUpper(rs[i]) {
|
||||
if initialism := startsWithInitialism(s[lastPos:]); initialism != "" {
|
||||
words = append(words, initialism)
|
||||
|
||||
i += len(initialism) - 1
|
||||
lastPos = i
|
||||
continue
|
||||
}
|
||||
|
||||
words = append(words, s[lastPos:i])
|
||||
lastPos = i
|
||||
}
|
||||
}
|
||||
|
||||
// append the last word
|
||||
if s[lastPos:] != "" {
|
||||
words = append(words, s[lastPos:])
|
||||
}
|
||||
|
||||
for k, word := range words {
|
||||
if k > 0 {
|
||||
result += "_"
|
||||
}
|
||||
|
||||
result += strings.ToLower(word)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func snakeToCamel(s string, upperCase bool) string {
|
||||
var result string
|
||||
|
||||
words := strings.Split(s, "_")
|
||||
|
||||
for i, word := range words {
|
||||
if exception := snakeToCamelExceptions[word]; len(exception) > 0 {
|
||||
result += exception
|
||||
continue
|
||||
}
|
||||
|
||||
if upperCase || i > 0 {
|
||||
if upper := strings.ToUpper(word); commonInitialisms[upper] {
|
||||
result += upper
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (upperCase || i > 0) && len(word) > 0 {
|
||||
w := []rune(word)
|
||||
w[0] = unicode.ToUpper(w[0])
|
||||
result += string(w)
|
||||
} else {
|
||||
result += word
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SnakeToCamel returns a string converted from snake case to uppercase
|
||||
func SnakeToCamel(s string) string {
|
||||
return snakeToCamel(s, true)
|
||||
}
|
||||
|
||||
// SnakeToCamelLower returns a string converted from snake case to lowercase
|
||||
func SnakeToCamelLower(s string) string {
|
||||
return snakeToCamel(s, false)
|
||||
}
|
||||
|
||||
// startsWithInitialism returns the initialism if the given string begins with it
|
||||
func startsWithInitialism(s string) string {
|
||||
var initialism string
|
||||
// the longest initialism is 5 char, the shortest 2
|
||||
for i := 1; i <= 5; i++ {
|
||||
if len(s) > i-1 && commonInitialisms[s[:i]] {
|
||||
initialism = s[:i]
|
||||
}
|
||||
}
|
||||
return initialism
|
||||
}
|
||||
|
||||
// commonInitialisms, taken from
|
||||
// https://github.com/golang/lint/blob/206c0f020eba0f7fbcfbc467a5eb808037df2ed6/lint.go#L731
|
||||
var commonInitialisms = map[string]bool{
|
||||
"ACL": true,
|
||||
"API": true,
|
||||
"ASCII": true,
|
||||
"CPU": true,
|
||||
"CSS": true,
|
||||
"DNS": true,
|
||||
"EOF": true,
|
||||
"ETA": true,
|
||||
"GPU": true,
|
||||
"GUID": true,
|
||||
"HTML": true,
|
||||
"HTTP": true,
|
||||
"HTTPS": true,
|
||||
"ID": true,
|
||||
"IP": true,
|
||||
"JSON": true,
|
||||
"LHS": true,
|
||||
"OS": true,
|
||||
"QPS": true,
|
||||
"RAM": true,
|
||||
"RHS": true,
|
||||
"RPC": true,
|
||||
"SLA": true,
|
||||
"SMTP": true,
|
||||
"SQL": true,
|
||||
"SSH": true,
|
||||
"TCP": true,
|
||||
"TLS": true,
|
||||
"TTL": true,
|
||||
"UDP": true,
|
||||
"UI": true,
|
||||
"UID": true,
|
||||
"UUID": true,
|
||||
"URI": true,
|
||||
"URL": true,
|
||||
"UTF8": true,
|
||||
"VM": true,
|
||||
"XML": true,
|
||||
"XMPP": true,
|
||||
"XSRF": true,
|
||||
"XSS": true,
|
||||
"OAuth": true,
|
||||
}
|
||||
|
||||
// add exceptions here for things that are not automatically convertable
|
||||
var snakeToCamelExceptions = map[string]string{
|
||||
"oauth": "OAuth",
|
||||
}
|
Loading…
Reference in new issue