PR xorm master

This commit is contained in:
hsfish 2023-07-03 18:13:23 +08:00
parent 141ce23b80
commit 0b266873f1
39 changed files with 1230 additions and 792 deletions

View File

@ -1,437 +0,0 @@
---
kind: pipeline
name: test-mysql
environment:
GO111MODULE: "on"
GOPROXY: "https://goproxy.io"
CGO_ENABLED: 1
trigger:
ref:
- refs/heads/master
- refs/pull/*/head
steps:
- name: test-vet
image: golang:1.17
pull: always
volumes:
- name: cache
path: /go/pkg/mod
commands:
- make vet
- name: test-sqlite3
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
depends_on:
- test-vet
commands:
- make fmt-check
- make test
- make test-sqlite3
- TEST_CACHE_ENABLE=true make test-sqlite3
- name: test-sqlite
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
depends_on:
- test-vet
commands:
- make test-sqlite
- TEST_QUOTE_POLICY=reserved make test-sqlite
- name: test-mysql
image: golang:1.17
pull: never
volumes:
- name: cache
path: /go/pkg/mod
depends_on:
- test-vet
environment:
TEST_MYSQL_HOST: mysql
TEST_MYSQL_CHARSET: utf8
TEST_MYSQL_DBNAME: xorm_test
TEST_MYSQL_USERNAME: root
TEST_MYSQL_PASSWORD:
commands:
- TEST_CACHE_ENABLE=true make test-mysql
- name: test-mysql-utf8mb4
image: golang:1.17
pull: never
volumes:
- name: cache
path: /go/pkg/mod
depends_on:
- test-mysql
environment:
TEST_MYSQL_HOST: mysql
TEST_MYSQL_CHARSET: utf8mb4
TEST_MYSQL_DBNAME: xorm_test
TEST_MYSQL_USERNAME: root
TEST_MYSQL_PASSWORD:
commands:
- make test-mysql
- TEST_QUOTE_POLICY=reserved make test-mysql-tls
volumes:
- name: cache
host:
path: /tmp/cache
services:
- name: mysql
image: mysql:5.7
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: xorm_test
---
kind: pipeline
name: test-mysql8
depends_on:
- test-mysql
trigger:
ref:
- refs/heads/master
- refs/pull/*/head
steps:
- name: test-mysql8
image: golang:1.17
pull: never
volumes:
- name: cache
path: /go/pkg/mod
environment:
TEST_MYSQL_HOST: mysql8
TEST_MYSQL_CHARSET: utf8mb4
TEST_MYSQL_DBNAME: xorm_test
TEST_MYSQL_USERNAME: root
TEST_MYSQL_PASSWORD:
commands:
- make test-mysql
- TEST_CACHE_ENABLE=true make test-mysql
volumes:
- name: cache
host:
path: /tmp/cache
services:
- name: mysql8
image: mysql:8.0
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: xorm_test
---
kind: pipeline
name: test-mariadb
depends_on:
- test-mysql8
trigger:
ref:
- refs/heads/master
- refs/pull/*/head
steps:
- name: test-mariadb
image: golang:1.17
pull: never
volumes:
- name: cache
path: /go/pkg/mod
environment:
TEST_MYSQL_HOST: mariadb
TEST_MYSQL_CHARSET: utf8mb4
TEST_MYSQL_DBNAME: xorm_test
TEST_MYSQL_USERNAME: root
TEST_MYSQL_PASSWORD:
commands:
- make test-mysql
- TEST_QUOTE_POLICY=reserved make test-mysql
volumes:
- name: cache
host:
path: /tmp/cache
services:
- name: mariadb
image: mariadb:10.4
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: xorm_test
---
kind: pipeline
name: test-postgres
depends_on:
- test-mariadb
trigger:
ref:
- refs/heads/master
- refs/pull/*/head
steps:
- name: test-postgres
pull: never
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
environment:
TEST_PGSQL_HOST: pgsql
TEST_PGSQL_DBNAME: xorm_test
TEST_PGSQL_USERNAME: postgres
TEST_PGSQL_PASSWORD: postgres
commands:
- make test-postgres
- TEST_CACHE_ENABLE=true make test-postgres
- name: test-postgres-schema
pull: never
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
depends_on:
- test-postgres
environment:
TEST_PGSQL_HOST: pgsql
TEST_PGSQL_SCHEMA: xorm
TEST_PGSQL_DBNAME: xorm_test
TEST_PGSQL_USERNAME: postgres
TEST_PGSQL_PASSWORD: postgres
commands:
- TEST_QUOTE_POLICY=reserved make test-postgres
- name: test-pgx
pull: never
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
depends_on:
- test-postgres-schema
environment:
TEST_PGSQL_HOST: pgsql
TEST_PGSQL_DBNAME: xorm_test
TEST_PGSQL_USERNAME: postgres
TEST_PGSQL_PASSWORD: postgres
commands:
- make test-pgx
- TEST_CACHE_ENABLE=true make test-pgx
- TEST_QUOTE_POLICY=reserved make test-pgx
- name: test-pgx-schema
pull: never
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
depends_on:
- test-pgx
environment:
TEST_PGSQL_HOST: pgsql
TEST_PGSQL_SCHEMA: xorm
TEST_PGSQL_DBNAME: xorm_test
TEST_PGSQL_USERNAME: postgres
TEST_PGSQL_PASSWORD: postgres
commands:
- make test-pgx
- TEST_CACHE_ENABLE=true make test-pgx
- TEST_QUOTE_POLICY=reserved make test-pgx
volumes:
- name: cache
host:
path: /tmp/cache
services:
- name: pgsql
image: postgres:9.5
environment:
POSTGRES_DB: xorm_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
---
kind: pipeline
name: test-mssql
depends_on:
- test-postgres
trigger:
ref:
- refs/heads/master
- refs/pull/*/head
steps:
- name: test-mssql
pull: never
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
environment:
TEST_MSSQL_HOST: mssql
TEST_MSSQL_DBNAME: xorm_test
TEST_MSSQL_USERNAME: sa
TEST_MSSQL_PASSWORD: "yourStrong(!)Password"
commands:
- make test-mssql
- TEST_MSSQL_DEFAULT_VARCHAR=NVARCHAR TEST_MSSQL_DEFAULT_CHAR=NCHAR make test-mssql
volumes:
- name: cache
host:
path: /tmp/cache
services:
- name: mssql
pull: always
image: mcr.microsoft.com/mssql/server:latest
environment:
ACCEPT_EULA: Y
SA_PASSWORD: yourStrong(!)Password
MSSQL_PID: Standard
---
kind: pipeline
name: test-tidb
depends_on:
- test-mssql
trigger:
ref:
- refs/heads/master
- refs/pull/*/head
steps:
- name: test-tidb
pull: never
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
environment:
TEST_TIDB_HOST: "tidb:4000"
TEST_TIDB_DBNAME: xorm_test
TEST_TIDB_USERNAME: root
TEST_TIDB_PASSWORD:
commands:
- make test-tidb
volumes:
- name: cache
host:
path: /tmp/cache
services:
- name: tidb
image: pingcap/tidb:v3.0.3
---
kind: pipeline
name: test-cockroach
depends_on:
- test-tidb
trigger:
ref:
- refs/heads/master
- refs/pull/*/head
steps:
- name: test-cockroach
pull: never
image: golang:1.17
volumes:
- name: cache
path: /go/pkg/mod
environment:
TEST_COCKROACH_HOST: "cockroach:26257"
TEST_COCKROACH_DBNAME: xorm_test
TEST_COCKROACH_USERNAME: root
TEST_COCKROACH_PASSWORD:
commands:
- sleep 10
- make test-cockroach
volumes:
- name: cache
host:
path: /tmp/cache
services:
- name: cockroach
image: cockroachdb/cockroach:v19.2.4
commands:
- /cockroach/cockroach start --insecure
# ---
# kind: pipeline
# name: test-dameng
# depends_on:
# - test-cockroach
# trigger:
# ref:
# - refs/heads/master
# - refs/pull/*/head
# steps:
# - name: test-dameng
# pull: never
# image: golang:1.17
# volumes:
# - name: cache
# path: /go/pkg/mod
# environment:
# TEST_DAMENG_HOST: "dameng:5236"
# TEST_DAMENG_USERNAME: SYSDBA
# TEST_DAMENG_PASSWORD: SYSDBA
# commands:
# - sleep 30
# - make test-dameng
# volumes:
# - name: cache
# host:
# path: /tmp/cache
# services:
# - name: dameng
# image: lunny/dm:v1.0
# commands:
# - /bin/bash /startDm.sh
---
kind: pipeline
name: merge_coverage
depends_on:
- test-mysql
- test-mysql8
- test-mariadb
- test-postgres
- test-mssql
- test-tidb
- test-cockroach
#- test-dameng
trigger:
ref:
- refs/heads/master
- refs/pull/*/head
steps:
- name: merge_coverage
image: golang:1.17
commands:
- make coverage
---
kind: pipeline
name: release-tag
trigger:
event:
- tag
steps:
- name: release-tag-gitea
pull: always
image: plugins/gitea-release:latest
settings:
base_url: https://gitea.com
title: '${DRONE_TAG} is released'
api_key:
from_secret: gitea_token

View File

@ -0,0 +1,23 @@
name: release
on:
push:
tags:
- '*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: setup go
uses: https://github.com/actions/setup-go@v4
with:
go-version: '>=1.20.1'
- name: Use Go Action
id: use-go-action
uses: actions/release-action@main
with:
api_key: '${{secrets.RELEASE_TOKEN}}'

View File

@ -0,0 +1,55 @@
name: test cockroach
on:
push:
branches:
- master
pull_request:
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
test-cockroach:
name: test cockroach
runs-on: ubuntu-latest
steps:
# - name: cache go path
# id: cache-go-path
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_path
# key: go_path-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_path-${{ github.repository }}-
# go_path-
# - name: cache go cache
# id: cache-go-cache
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_cache
# key: go_cache-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_cache-${{ github.repository }}-
# go_cache-
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: https://github.com/actions/checkout@v3
- name: test cockroach
env:
TEST_COCKROACH_HOST: "cockroach:26257"
TEST_COCKROACH_DBNAME: xorm_test
TEST_COCKROACH_USERNAME: root
TEST_COCKROACH_PASSWORD:
run: sleep 20 && make test-cockroach
services:
cockroach:
image: cockroachdb/cockroach:v19.2.4
ports:
- 26257:26257
cmd:
- 'start'
- '--insecure'

View File

@ -0,0 +1,56 @@
name: test mariadb
on:
push:
branches:
- master
pull_request:
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
lint:
name: test mariadb
runs-on: ubuntu-latest
steps:
# - name: cache go path
# id: cache-go-path
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_path
# key: go_path-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_path-${{ github.repository }}-
# go_path-
# - name: cache go cache
# id: cache-go-cache
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_cache
# key: go_cache-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_cache-${{ github.repository }}-
# go_cache-
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: https://github.com/actions/checkout@v3
- name: test mariadb
env:
TEST_MYSQL_HOST: mariadb
TEST_MYSQL_CHARSET: utf8mb4
TEST_MYSQL_DBNAME: xorm_test
TEST_MYSQL_USERNAME: root
TEST_MYSQL_PASSWORD:
run: TEST_QUOTE_POLICY=reserved make test-mysql
services:
mariadb:
image: mariadb:10.4
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: xorm_test
ports:
- 3306:3306

View File

@ -0,0 +1,56 @@
name: test mssql
on:
push:
branches:
- master
pull_request:
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
test-mssql:
name: test mssql
runs-on: ubuntu-latest
steps:
# - name: cache go path
# id: cache-go-path
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_path
# key: go_path-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_path-${{ github.repository }}-
# go_path-
# - name: cache go cache
# id: cache-go-cache
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_cache
# key: go_cache-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_cache-${{ github.repository }}-
# go_cache-
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: https://github.com/actions/checkout@v3
- name: test mssql
env:
TEST_MSSQL_HOST: mssql
TEST_MSSQL_DBNAME: xorm_test
TEST_MSSQL_USERNAME: sa
TEST_MSSQL_PASSWORD: "yourStrong(!)Password"
run: TEST_MSSQL_DEFAULT_VARCHAR=NVARCHAR TEST_MSSQL_DEFAULT_CHAR=NCHAR make test-mssql
services:
mssql:
image: mcr.microsoft.com/mssql/server:latest
env:
ACCEPT_EULA: Y
SA_PASSWORD: yourStrong(!)Password
MSSQL_PID: Standard
ports:
- 1433:1433

View File

@ -0,0 +1,56 @@
name: test mysql
on:
push:
branches:
- master
pull_request:
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
test-mysql:
name: test mysql
runs-on: ubuntu-latest
steps:
# - name: cache go path
# id: cache-go-path
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_path
# key: go_path-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_path-${{ github.repository }}-
# go_path-
# - name: cache go cache
# id: cache-go-cache
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_cache
# key: go_cache-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_cache-${{ github.repository }}-
# go_cache-
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: https://github.com/actions/checkout@v3
- name: test mysql utf8mb4
env:
TEST_MYSQL_HOST: mysql
TEST_MYSQL_CHARSET: utf8mb4
TEST_MYSQL_DBNAME: xorm_test
TEST_MYSQL_USERNAME: root
TEST_MYSQL_PASSWORD:
run: TEST_QUOTE_POLICY=reserved make test-mysql-tls
services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: xorm_test
ports:
- 3306:3306

View File

@ -0,0 +1,56 @@
name: test mysql8
on:
push:
branches:
- master
pull_request:
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
lint:
name: test mysql8
runs-on: ubuntu-latest
steps:
# - name: cache go path
# id: cache-go-path
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_path
# key: go_path-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_path-${{ github.repository }}-
# go_path-
# - name: cache go cache
# id: cache-go-cache
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_cache
# key: go_cache-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_cache-${{ github.repository }}-
# go_cache-
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: https://github.com/actions/checkout@v3
- name: test mysql8
env:
TEST_MYSQL_HOST: mysql8
TEST_MYSQL_CHARSET: utf8mb4
TEST_MYSQL_DBNAME: xorm_test
TEST_MYSQL_USERNAME: root
TEST_MYSQL_PASSWORD:
run: TEST_CACHE_ENABLE=true make test-mysql
services:
mysql8:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: xorm_test
ports:
- 3306:3306

View File

@ -0,0 +1,79 @@
name: test postgres
on:
push:
branches:
- master
pull_request:
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
lint:
name: test postgres
runs-on: ubuntu-latest
steps:
# - name: cache go path
# id: cache-go-path
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_path
# key: go_path-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_path-${{ github.repository }}-
# go_path-
# - name: cache go cache
# id: cache-go-cache
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_cache
# key: go_cache-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_cache-${{ github.repository }}-
# go_cache-
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: https://github.com/actions/checkout@v3
- name: test postgres
env:
TEST_PGSQL_HOST: pgsql
TEST_PGSQL_DBNAME: xorm_test
TEST_PGSQL_USERNAME: postgres
TEST_PGSQL_PASSWORD: postgres
run: TEST_CACHE_ENABLE=true make test-postgres
- name: test postgres with schema
env:
TEST_PGSQL_HOST: pgsql
TEST_PGSQL_SCHEMA: xorm
TEST_PGSQL_DBNAME: xorm_test
TEST_PGSQL_USERNAME: postgres
TEST_PGSQL_PASSWORD: postgres
run: TEST_QUOTE_POLICY=reserved make test-postgres
- name: test pgx
env:
TEST_PGSQL_HOST: pgsql
TEST_PGSQL_DBNAME: xorm_test
TEST_PGSQL_USERNAME: postgres
TEST_PGSQL_PASSWORD: postgres
run: TEST_CACHE_ENABLE=true make test-pgx
- name: test pgx with schema
env:
TEST_PGSQL_HOST: pgsql
TEST_PGSQL_SCHEMA: xorm
TEST_PGSQL_DBNAME: xorm_test
TEST_PGSQL_USERNAME: postgres
TEST_PGSQL_PASSWORD: postgres
run: TEST_QUOTE_POLICY=reserved make test-pgx
services:
pgsql:
image: postgres:9.5
env:
POSTGRES_DB: xorm_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432

View File

@ -0,0 +1,49 @@
name: test sqlite
on:
push:
branches:
- master
pull_request:
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
test-sqlite:
name: unit test & test sqlite
runs-on: ubuntu-latest
steps:
# - name: cache go path
# id: cache-go-path
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_path
# key: go_path-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_path-${{ github.repository }}-
# go_path-
# - name: cache go cache
# id: cache-go-cache
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_cache
# key: go_cache-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_cache-${{ github.repository }}-
# go_cache-
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: https://github.com/actions/checkout@v3
- name: vet
run: make vet
- name: format check
run: make fmt-check
- name: unit test
run: make test
- name: test sqlite3
run: make test-sqlite3
- name: test sqlite3 with cache
run: TEST_CACHE_ENABLE=true make test-sqlite3

View File

@ -0,0 +1,52 @@
name: test tidb
on:
push:
branches:
- master
pull_request:
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
test-tidb:
name: test tidb
runs-on: ubuntu-latest
steps:
# - name: cache go path
# id: cache-go-path
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_path
# key: go_path-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_path-${{ github.repository }}-
# go_path-
# - name: cache go cache
# id: cache-go-cache
# uses: https://github.com/actions/cache@v3
# with:
# path: /go_cache
# key: go_cache-${{ github.repository }}-${{ github.ref_name }}
# restore-keys: |
# go_cache-${{ github.repository }}-
# go_cache-
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: https://github.com/actions/checkout@v3
- name: test tidb
env:
TEST_TIDB_HOST: "tidb:4000"
TEST_TIDB_DBNAME: xorm_test
TEST_TIDB_USERNAME: root
TEST_TIDB_PASSWORD:
run: make test-tidb
services:
tidb:
image: pingcap/tidb:v3.0.3
ports:
- 4000:4000

View File

@ -23,10 +23,11 @@ func Interface2Interface(userLocation *time.Location, v interface{}) (interface{
case *sql.NullString: case *sql.NullString:
return vv.String, nil return vv.String, nil
case *sql.RawBytes: case *sql.RawBytes:
if len(*vv) > 0 { if len([]byte(*vv)) > 0 {
val := make([]byte, len(*vv)) src := []byte(*vv)
copy(val, *vv) dest := make([]byte, len(src))
return val, nil copy(dest, src)
return dest, nil
} }
return nil, nil return nil, nil
case *sql.NullInt32: case *sql.NullInt32:

View File

@ -40,6 +40,13 @@ func String2Time(s string, originalLocation *time.Location, convertedLocation *t
} }
dt = dt.In(convertedLocation) dt = dt.In(convertedLocation)
return &dt, nil return &dt, nil
} else if len(s) >= 21 && s[10] == 'T' && s[19] == '.' {
dt, err := time.Parse(time.RFC3339, s)
if err != nil {
return nil, err
}
dt = dt.In(convertedLocation)
return &dt, nil
} else if len(s) >= 21 && s[19] == '.' { } else if len(s) >= 21 && s[19] == '.' {
var layout = "2006-01-02 15:04:05." + strings.Repeat("0", len(s)-20) var layout = "2006-01-02 15:04:05." + strings.Repeat("0", len(s)-20)
dt, err := time.ParseInLocation(layout, s, originalLocation) dt, err := time.ParseInLocation(layout, s, originalLocation)

View File

@ -17,9 +17,18 @@ func TestString2Time(t *testing.T) {
var kases = map[string]time.Time{ var kases = map[string]time.Time{
"2021-08-10": time.Date(2021, 8, 10, 8, 0, 0, 0, expectedLoc), "2021-08-10": time.Date(2021, 8, 10, 8, 0, 0, 0, expectedLoc),
"2021-06-06T22:58:20+08:00": time.Date(2021, 6, 6, 22, 58, 20, 0, expectedLoc),
"2021-07-11 10:44:00": time.Date(2021, 7, 11, 18, 44, 0, 0, expectedLoc), "2021-07-11 10:44:00": time.Date(2021, 7, 11, 18, 44, 0, 0, expectedLoc),
"2021-07-11 10:44:00.999": time.Date(2021, 7, 11, 18, 44, 0, 999000000, expectedLoc),
"2021-07-11 10:44:00.999999": time.Date(2021, 7, 11, 18, 44, 0, 999999000, expectedLoc),
"2021-07-11 10:44:00.999999999": time.Date(2021, 7, 11, 18, 44, 0, 999999999, expectedLoc),
"2021-06-06T22:58:20+08:00": time.Date(2021, 6, 6, 22, 58, 20, 0, expectedLoc),
"2021-06-06T22:58:20.999+08:00": time.Date(2021, 6, 6, 22, 58, 20, 999000000, expectedLoc),
"2021-06-06T22:58:20.999999+08:00": time.Date(2021, 6, 6, 22, 58, 20, 999999000, expectedLoc),
"2021-06-06T22:58:20.999999999+08:00": time.Date(2021, 6, 6, 22, 58, 20, 999999999, expectedLoc),
"2021-08-10T10:33:04Z": time.Date(2021, 8, 10, 18, 33, 04, 0, expectedLoc), "2021-08-10T10:33:04Z": time.Date(2021, 8, 10, 18, 33, 04, 0, expectedLoc),
"2021-08-10T10:33:04.999Z": time.Date(2021, 8, 10, 18, 33, 04, 999000000, expectedLoc),
"2021-08-10T10:33:04.999999Z": time.Date(2021, 8, 10, 18, 33, 04, 999999000, expectedLoc),
"2021-08-10T10:33:04.999999999Z": time.Date(2021, 8, 10, 18, 33, 04, 999999999, expectedLoc),
} }
for layout, tm := range kases { for layout, tm := range kases {
t.Run(layout, func(t *testing.T) { t.Run(layout, func(t *testing.T) {

View File

@ -659,7 +659,7 @@ func (db *dameng) DropTableSQL(tableName string) (string, bool) {
// ModifyColumnSQL returns a SQL to modify SQL // ModifyColumnSQL returns a SQL to modify SQL
func (db *dameng) ModifyColumnSQL(tableName string, col *schemas.Column) string { func (db *dameng) ModifyColumnSQL(tableName string, col *schemas.Column) string {
s, _ := ColumnString(db.dialect, col, false) s, _ := ColumnString(db.dialect, col, false, false)
return fmt.Sprintf("ALTER TABLE %s MODIFY %s", db.quoter.Quote(tableName), s) return fmt.Sprintf("ALTER TABLE %s MODIFY %s", db.quoter.Quote(tableName), s)
} }
@ -692,7 +692,7 @@ func (db *dameng) CreateTableSQL(ctx context.Context, queryer core.Queryer, tabl
} }
} }
s, _ := ColumnString(db, col, false) s, _ := ColumnString(db, col, false, false)
if _, err := b.WriteString(s); err != nil { if _, err := b.WriteString(s); err != nil {
return "", false, err return "", false, err
} }
@ -709,7 +709,13 @@ func (db *dameng) CreateTableSQL(ctx context.Context, queryer core.Queryer, tabl
return "", false, err return "", false, err
} }
} }
if _, err := b.WriteString(fmt.Sprintf("CONSTRAINT PK_%s PRIMARY KEY (", tableName)); err != nil { if _, err := b.WriteString("CONSTRAINT PK_"); err != nil {
return "", false, err
}
if _, err := b.WriteString(tableName); err != nil {
return "", false, err
}
if _, err := b.WriteString(" PRIMARY KEY ("); err != nil {
return "", false, err return "", false, err
} }
if err := quoter.JoinWrite(&b, pkList, ","); err != nil { if err := quoter.JoinWrite(&b, pkList, ","); err != nil {
@ -837,7 +843,11 @@ func addSingleQuote(name string) string {
if name[0] == '\'' && name[len(name)-1] == '\'' { if name[0] == '\'' && name[len(name)-1] == '\'' {
return name return name
} }
return fmt.Sprintf("'%s'", name) var b strings.Builder
b.WriteRune('\'')
b.WriteString(name)
b.WriteRune('\'')
return b.String()
} }
func (db *dameng) GetColumns(queryer core.Queryer, ctx context.Context, tableName string) ([]string, map[string]*schemas.Column, error) { func (db *dameng) GetColumns(queryer core.Queryer, ctx context.Context, tableName string) ([]string, map[string]*schemas.Column, error) {

View File

@ -135,7 +135,7 @@ func (db *Base) CreateTableSQL(ctx context.Context, queryer core.Queryer, table
for i, colName := range table.ColumnsSeq() { for i, colName := range table.ColumnsSeq() {
col := table.GetColumn(colName) col := table.GetColumn(colName)
s, _ := ColumnString(db.dialect, col, col.IsPrimaryKey && len(table.PrimaryKeys) == 1) s, _ := ColumnString(db.dialect, col, col.IsPrimaryKey && len(table.PrimaryKeys) == 1, false)
b.WriteString(s) b.WriteString(s)
if i != len(table.ColumnsSeq())-1 { if i != len(table.ColumnsSeq())-1 {
@ -209,7 +209,7 @@ func (db *Base) IsColumnExist(queryer core.Queryer, ctx context.Context, tableNa
// AddColumnSQL returns a SQL to add a column // AddColumnSQL returns a SQL to add a column
func (db *Base) AddColumnSQL(tableName string, col *schemas.Column) string { func (db *Base) AddColumnSQL(tableName string, col *schemas.Column) string {
s, _ := ColumnString(db.dialect, col, true) s, _ := ColumnString(db.dialect, col, true, false)
return fmt.Sprintf("ALTER TABLE %s ADD %s", db.dialect.Quoter().Quote(tableName), s) return fmt.Sprintf("ALTER TABLE %s ADD %s", db.dialect.Quoter().Quote(tableName), s)
} }
@ -241,7 +241,7 @@ func (db *Base) DropIndexSQL(tableName string, index *schemas.Index) string {
// ModifyColumnSQL returns a SQL to modify SQL // ModifyColumnSQL returns a SQL to modify SQL
func (db *Base) ModifyColumnSQL(tableName string, col *schemas.Column) string { func (db *Base) ModifyColumnSQL(tableName string, col *schemas.Column) string {
s, _ := ColumnString(db.dialect, col, false) s, _ := ColumnString(db.dialect, col, false, false)
return fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s", db.quoter.Quote(tableName), s) return fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s", db.quoter.Quote(tableName), s)
} }
@ -254,9 +254,7 @@ func (db *Base) ForUpdateSQL(query string) string {
func (db *Base) SetParams(params map[string]string) { func (db *Base) SetParams(params map[string]string) {
} }
var ( var dialects = map[string]func() Dialect{}
dialects = map[string]func() Dialect{}
)
// RegisterDialect register database dialect // RegisterDialect register database dialect
func RegisterDialect(dbName schemas.DBType, dialectFunc func() Dialect) { func RegisterDialect(dbName schemas.DBType, dialectFunc func() Dialect) {
@ -307,7 +305,7 @@ func init() {
} }
// ColumnString generate column description string according dialect // ColumnString generate column description string according dialect
func ColumnString(dialect Dialect, col *schemas.Column, includePrimaryKey bool) (string, error) { func ColumnString(dialect Dialect, col *schemas.Column, includePrimaryKey, supportCollation bool) (string, error) {
bd := strings.Builder{} bd := strings.Builder{}
if err := dialect.Quoter().QuoteTo(&bd, col.Name); err != nil { if err := dialect.Quoter().QuoteTo(&bd, col.Name); err != nil {
@ -322,6 +320,15 @@ func ColumnString(dialect Dialect, col *schemas.Column, includePrimaryKey bool)
return "", err return "", err
} }
if supportCollation && col.Collation != "" {
if _, err := bd.WriteString(" COLLATE "); err != nil {
return "", err
}
if _, err := bd.WriteString(col.Collation); err != nil {
return "", err
}
}
if includePrimaryKey && col.IsPrimaryKey { if includePrimaryKey && col.IsPrimaryKey {
if _, err := bd.WriteString(" PRIMARY KEY"); err != nil { if _, err := bd.WriteString(" PRIMARY KEY"); err != nil {
return "", err return "", err

View File

@ -5,13 +5,14 @@
package dialects package dialects
import ( import (
"fmt" "context"
"strconv"
"strings" "strings"
) )
// Filter is an interface to filter SQL // Filter is an interface to filter SQL
type Filter interface { type Filter interface {
Do(sql string) string Do(ctx context.Context, sql string) string
} }
// SeqFilter filter SQL replace ?, ? ... to $1, $2 ... // SeqFilter filter SQL replace ?, ? ... to $1, $2 ...
@ -28,10 +29,11 @@ func convertQuestionMark(sql, prefix string, start int) string {
var isMaybeLineComment bool var isMaybeLineComment bool
var isMaybeComment bool var isMaybeComment bool
var isMaybeCommentEnd bool var isMaybeCommentEnd bool
var index = start index := start
for _, c := range sql { for _, c := range sql {
if !beginSingleQuote && !isLineComment && !isComment && c == '?' { if !beginSingleQuote && !isLineComment && !isComment && c == '?' {
buf.WriteString(fmt.Sprintf("%s%v", prefix, index)) buf.WriteString(prefix)
buf.WriteString(strconv.Itoa(index))
index++ index++
} else { } else {
if isMaybeLineComment { if isMaybeLineComment {
@ -71,6 +73,6 @@ func convertQuestionMark(sql, prefix string, start int) string {
} }
// Do implements Filter // Do implements Filter
func (s *SeqFilter) Do(sql string) string { func (s *SeqFilter) Do(ctx context.Context, sql string) string {
return convertQuestionMark(sql, s.Prefix, s.Start) return convertQuestionMark(sql, s.Prefix, s.Start)
} }

View File

@ -428,7 +428,7 @@ func (db *mssql) DropTableSQL(tableName string) (string, bool) {
} }
func (db *mssql) ModifyColumnSQL(tableName string, col *schemas.Column) string { func (db *mssql) ModifyColumnSQL(tableName string, col *schemas.Column) string {
s, _ := ColumnString(db.dialect, col, false) s, _ := ColumnString(db.dialect, col, false, true)
return fmt.Sprintf("ALTER TABLE %s ALTER COLUMN %s", db.quoter.Quote(tableName), s) return fmt.Sprintf("ALTER TABLE %s ALTER COLUMN %s", db.quoter.Quote(tableName), s)
} }
@ -454,7 +454,7 @@ func (db *mssql) GetColumns(queryer core.Queryer, ctx context.Context, tableName
s := `select a.name as name, b.name as ctype,a.max_length,a.precision,a.scale,a.is_nullable as nullable, s := `select a.name as name, b.name as ctype,a.max_length,a.precision,a.scale,a.is_nullable as nullable,
"default_is_null" = (CASE WHEN c.text is null THEN 1 ELSE 0 END), "default_is_null" = (CASE WHEN c.text is null THEN 1 ELSE 0 END),
replace(replace(isnull(c.text,''),'(',''),')','') as vdefault, replace(replace(isnull(c.text,''),'(',''),')','') as vdefault,
ISNULL(p.is_primary_key, 0), a.is_identity as is_identity ISNULL(p.is_primary_key, 0), a.is_identity as is_identity, a.collation_name
from sys.columns a from sys.columns a
left join sys.types b on a.user_type_id=b.user_type_id left join sys.types b on a.user_type_id=b.user_type_id
left join sys.syscomments c on a.default_object_id=c.id left join sys.syscomments c on a.default_object_id=c.id
@ -475,9 +475,10 @@ func (db *mssql) GetColumns(queryer core.Queryer, ctx context.Context, tableName
colSeq := make([]string, 0) colSeq := make([]string, 0)
for rows.Next() { for rows.Next() {
var name, ctype, vdefault string var name, ctype, vdefault string
var collation *string
var maxLen, precision, scale int64 var maxLen, precision, scale int64
var nullable, isPK, defaultIsNull, isIncrement bool var nullable, isPK, defaultIsNull, isIncrement bool
err = rows.Scan(&name, &ctype, &maxLen, &precision, &scale, &nullable, &defaultIsNull, &vdefault, &isPK, &isIncrement) err = rows.Scan(&name, &ctype, &maxLen, &precision, &scale, &nullable, &defaultIsNull, &vdefault, &isPK, &isIncrement, &collation)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -499,6 +500,9 @@ func (db *mssql) GetColumns(queryer core.Queryer, ctx context.Context, tableName
} else { } else {
col.Length = maxLen col.Length = maxLen
} }
if collation != nil {
col.Collation = *collation
}
switch ct { switch ct {
case "DATETIMEOFFSET": case "DATETIMEOFFSET":
col.SQLType = schemas.SQLType{Name: schemas.TimeStampz, DefaultLength: 0, DefaultLength2: 0} col.SQLType = schemas.SQLType{Name: schemas.TimeStampz, DefaultLength: 0, DefaultLength2: 0}
@ -646,7 +650,7 @@ func (db *mssql) CreateTableSQL(ctx context.Context, queryer core.Queryer, table
for i, colName := range table.ColumnsSeq() { for i, colName := range table.ColumnsSeq() {
col := table.GetColumn(colName) col := table.GetColumn(colName)
s, _ := ColumnString(db.dialect, col, col.IsPrimaryKey && len(table.PrimaryKeys) == 1) s, _ := ColumnString(db.dialect, col, col.IsPrimaryKey && len(table.PrimaryKeys) == 1, true)
b.WriteString(s) b.WriteString(s)
if i != len(table.ColumnsSeq())-1 { if i != len(table.ColumnsSeq())-1 {

View File

@ -380,12 +380,24 @@ func (db *mysql) IsTableExist(queryer core.Queryer, ctx context.Context, tableNa
func (db *mysql) AddColumnSQL(tableName string, col *schemas.Column) string { func (db *mysql) AddColumnSQL(tableName string, col *schemas.Column) string {
quoter := db.dialect.Quoter() quoter := db.dialect.Quoter()
s, _ := ColumnString(db, col, true) s, _ := ColumnString(db, col, true, true)
sql := fmt.Sprintf("ALTER TABLE %v ADD %v", quoter.Quote(tableName), s) var b strings.Builder
b.WriteString("ALTER TABLE ")
quoter.QuoteTo(&b, tableName)
b.WriteString(" ADD ")
b.WriteString(s)
if len(col.Comment) > 0 { if len(col.Comment) > 0 {
sql += " COMMENT '" + col.Comment + "'" b.WriteString(" COMMENT '")
b.WriteString(col.Comment)
b.WriteString("'")
} }
return sql return b.String()
}
// ModifyColumnSQL returns a SQL to modify SQL
func (db *mysql) ModifyColumnSQL(tableName string, col *schemas.Column) string {
s, _ := ColumnString(db.dialect, col, false, true)
return fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s", db.quoter.Quote(tableName), s)
} }
func (db *mysql) GetColumns(queryer core.Queryer, ctx context.Context, tableName string) ([]string, map[string]*schemas.Column, error) { func (db *mysql) GetColumns(queryer core.Queryer, ctx context.Context, tableName string) ([]string, map[string]*schemas.Column, error) {
@ -398,7 +410,7 @@ func (db *mysql) GetColumns(queryer core.Queryer, ctx context.Context, tableName
"SUBSTRING_INDEX(SUBSTRING(VERSION(), 6), '-', 1) >= 7)))))" "SUBSTRING_INDEX(SUBSTRING(VERSION(), 6), '-', 1) >= 7)))))"
s := "SELECT `COLUMN_NAME`, `IS_NULLABLE`, `COLUMN_DEFAULT`, `COLUMN_TYPE`," + s := "SELECT `COLUMN_NAME`, `IS_NULLABLE`, `COLUMN_DEFAULT`, `COLUMN_TYPE`," +
" `COLUMN_KEY`, `EXTRA`, `COLUMN_COMMENT`, `CHARACTER_MAXIMUM_LENGTH`, " + " `COLUMN_KEY`, `EXTRA`, `COLUMN_COMMENT`, `CHARACTER_MAXIMUM_LENGTH`, " +
alreadyQuoted + " AS NEEDS_QUOTE " + alreadyQuoted + " AS NEEDS_QUOTE, `COLLATION_NAME` " +
"FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?" + "FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?" +
" ORDER BY `COLUMNS`.ORDINAL_POSITION ASC" " ORDER BY `COLUMNS`.ORDINAL_POSITION ASC"
@ -416,8 +428,8 @@ func (db *mysql) GetColumns(queryer core.Queryer, ctx context.Context, tableName
var columnName, nullableStr, colType, colKey, extra, comment string var columnName, nullableStr, colType, colKey, extra, comment string
var alreadyQuoted, isUnsigned bool var alreadyQuoted, isUnsigned bool
var colDefault, maxLength *string var colDefault, maxLength, collation *string
err = rows.Scan(&columnName, &nullableStr, &colDefault, &colType, &colKey, &extra, &comment, &maxLength, &alreadyQuoted) err = rows.Scan(&columnName, &nullableStr, &colDefault, &colType, &colKey, &extra, &comment, &maxLength, &alreadyQuoted, &collation)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -433,6 +445,9 @@ func (db *mysql) GetColumns(queryer core.Queryer, ctx context.Context, tableName
} else { } else {
col.DefaultIsEmpty = true col.DefaultIsEmpty = true
} }
if collation != nil {
col.Collation = *collation
}
fields := strings.Fields(colType) fields := strings.Fields(colType)
if len(fields) == 2 && fields[1] == "unsigned" { if len(fields) == 2 && fields[1] == "unsigned" {
@ -525,7 +540,7 @@ func (db *mysql) GetColumns(queryer core.Queryer, ctx context.Context, tableName
func (db *mysql) GetTables(queryer core.Queryer, ctx context.Context) ([]*schemas.Table, error) { func (db *mysql) GetTables(queryer core.Queryer, ctx context.Context) ([]*schemas.Table, error) {
args := []interface{}{db.uri.DBName} args := []interface{}{db.uri.DBName}
s := "SELECT `TABLE_NAME`, `ENGINE`, `AUTO_INCREMENT`, `TABLE_COMMENT` from " + s := "SELECT `TABLE_NAME`, `ENGINE`, `AUTO_INCREMENT`, `TABLE_COMMENT`, `TABLE_COLLATION` from " +
"`INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? AND (`ENGINE`='MyISAM' OR `ENGINE` = 'InnoDB' OR `ENGINE` = 'TokuDB')" "`INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? AND (`ENGINE`='MyISAM' OR `ENGINE` = 'InnoDB' OR `ENGINE` = 'TokuDB')"
rows, err := queryer.QueryContext(ctx, s, args...) rows, err := queryer.QueryContext(ctx, s, args...)
@ -537,9 +552,9 @@ func (db *mysql) GetTables(queryer core.Queryer, ctx context.Context) ([]*schema
tables := make([]*schemas.Table, 0) tables := make([]*schemas.Table, 0)
for rows.Next() { for rows.Next() {
table := schemas.NewEmptyTable() table := schemas.NewEmptyTable()
var name, engine string var name, engine, collation string
var autoIncr, comment *string var autoIncr, comment *string
err = rows.Scan(&name, &engine, &autoIncr, &comment) err = rows.Scan(&name, &engine, &autoIncr, &comment, &collation)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -549,6 +564,7 @@ func (db *mysql) GetTables(queryer core.Queryer, ctx context.Context) ([]*schema
table.Comment = *comment table.Comment = *comment
} }
table.StoreEngine = engine table.StoreEngine = engine
table.Collation = collation
tables = append(tables, table) tables = append(tables, table)
} }
if rows.Err() != nil { if rows.Err() != nil {
@ -640,7 +656,7 @@ func (db *mysql) CreateTableSQL(ctx context.Context, queryer core.Queryer, table
for i, colName := range table.ColumnsSeq() { for i, colName := range table.ColumnsSeq() {
col := table.GetColumn(colName) col := table.GetColumn(colName)
s, _ := ColumnString(db.dialect, col, col.IsPrimaryKey && len(table.PrimaryKeys) == 1) s, _ := ColumnString(db.dialect, col, col.IsPrimaryKey && len(table.PrimaryKeys) == 1, true)
b.WriteString(s) b.WriteString(s)
if len(col.Comment) > 0 { if len(col.Comment) > 0 {

View File

@ -609,7 +609,7 @@ func (db *oracle) IsReserved(name string) bool {
} }
func (db *oracle) DropTableSQL(tableName string) (string, bool) { func (db *oracle) DropTableSQL(tableName string) (string, bool) {
return fmt.Sprintf("DROP TABLE `%s`", tableName), false return fmt.Sprintf("DROP TABLE \"%s\"", tableName), false
} }
func (db *oracle) CreateTableSQL(ctx context.Context, queryer core.Queryer, table *schemas.Table, tableName string) (string, bool, error) { func (db *oracle) CreateTableSQL(ctx context.Context, queryer core.Queryer, table *schemas.Table, tableName string) (string, bool, error) {
@ -628,7 +628,7 @@ func (db *oracle) CreateTableSQL(ctx context.Context, queryer core.Queryer, tabl
/*if col.IsPrimaryKey && len(pkList) == 1 { /*if col.IsPrimaryKey && len(pkList) == 1 {
sql += col.String(b.dialect) sql += col.String(b.dialect)
} else {*/ } else {*/
s, _ := ColumnString(db, col, false) s, _ := ColumnString(db, col, false, false)
sql += s sql += s
// } // }
sql = strings.TrimSpace(sql) sql = strings.TrimSpace(sql)
@ -645,6 +645,10 @@ func (db *oracle) CreateTableSQL(ctx context.Context, queryer core.Queryer, tabl
return sql, false, nil return sql, false, nil
} }
func (db *oracle) IsSequenceExist(ctx context.Context, queryer core.Queryer, seqName string) (bool, error) {
return db.HasRecords(queryer, ctx, `SELECT sequence_name FROM user_sequences WHERE sequence_name = :1`, seqName)
}
func (db *oracle) SetQuotePolicy(quotePolicy QuotePolicy) { func (db *oracle) SetQuotePolicy(quotePolicy QuotePolicy) {
switch quotePolicy { switch quotePolicy {
case QuotePolicyNone: case QuotePolicyNone:

View File

@ -992,7 +992,7 @@ func (db *postgres) IsTableExist(queryer core.Queryer, ctx context.Context, tabl
} }
func (db *postgres) AddColumnSQL(tableName string, col *schemas.Column) string { func (db *postgres) AddColumnSQL(tableName string, col *schemas.Column) string {
s, _ := ColumnString(db.dialect, col, true) s, _ := ColumnString(db.dialect, col, true, false)
quoter := db.dialect.Quoter() quoter := db.dialect.Quoter()
addColumnSQL := "" addColumnSQL := ""
@ -1078,7 +1078,7 @@ FROM pg_attribute f
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey) LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
LEFT JOIN pg_class AS g ON p.confrelid = g.oid LEFT JOIN pg_class AS g ON p.confrelid = g.oid
LEFT JOIN INFORMATION_SCHEMA.COLUMNS s ON s.column_name=f.attname AND c.relname=s.table_name LEFT JOIN INFORMATION_SCHEMA.COLUMNS s ON s.column_name=f.attname AND c.relname=s.table_name
WHERE n.nspname= s.table_schema AND c.relkind = 'r'::char AND c.relname = $1%s AND f.attnum > 0 ORDER BY f.attnum;` WHERE n.nspname= s.table_schema AND c.relkind = 'r' AND c.relname = $1%s AND f.attnum > 0 ORDER BY f.attnum;`
schema := db.getSchema() schema := db.getSchema()
if schema != "" { if schema != "" {

View File

@ -1120,21 +1120,6 @@ func (engine *Engine) UnMapType(t reflect.Type) {
engine.tagParser.ClearCacheTable(t) engine.tagParser.ClearCacheTable(t)
} }
// Sync the new struct changes to database, this method will automatically add
// table, column, index, unique. but will not delete or change anything.
// If you change some field, you should change the database manually.
func (engine *Engine) Sync(beans ...interface{}) error {
session := engine.NewSession()
defer session.Close()
return session.Sync(beans...)
}
// Sync2 synchronize structs to database tables
// Depricated
func (engine *Engine) Sync2(beans ...interface{}) error {
return engine.Sync(beans...)
}
// CreateTables create tabls according bean // CreateTables create tabls according bean
func (engine *Engine) CreateTables(beans ...interface{}) error { func (engine *Engine) CreateTables(beans ...interface{}) error {
session := engine.NewSession() session := engine.NewSession()

View File

@ -5,6 +5,7 @@
package integrations package integrations
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
@ -325,14 +326,14 @@ func TestIsTableEmpty(t *testing.T) {
type PictureEmpty struct { type PictureEmpty struct {
Id int64 Id int64
Url string `xorm:"unique"` //image's url Url string `xorm:"unique"` // image's url
Title string Title string
Description string Description string
Created time.Time `xorm:"created"` Created time.Time `xorm:"created"`
ILike int ILike int
PageView int PageView int
From_url string // nolint From_url string // nolint
Pre_url string `xorm:"unique"` //pre view image's url Pre_url string `xorm:"unique"` // pre view image's url
Uid int64 Uid int64
} }
@ -458,7 +459,7 @@ func TestSync2_2(t *testing.T) {
assert.NoError(t, PrepareEngine()) assert.NoError(t, PrepareEngine())
var tableNames = make(map[string]bool) tableNames := make(map[string]bool)
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
tableName := fmt.Sprintf("test_sync2_index_%d", i) tableName := fmt.Sprintf("test_sync2_index_%d", i)
tableNames[tableName] = true tableNames[tableName] = true
@ -536,3 +537,111 @@ func TestModifyColum(t *testing.T) {
_, err := testEngine.Exec(alterSQL) _, err := testEngine.Exec(alterSQL)
assert.NoError(t, err) assert.NoError(t, err)
} }
type TestCollateColumn struct {
Id int64
UserId int64 `xorm:"unique(s)"`
Name string `xorm:"varchar(20) unique(s)"`
dbtype string `xorm:"-"`
}
func (t TestCollateColumn) TableCollations() []*schemas.Collation {
if t.dbtype == string(schemas.MYSQL) {
return []*schemas.Collation{
{
Name: "utf8mb4_general_ci",
Column: "name",
},
}
} else if t.dbtype == string(schemas.MSSQL) {
return []*schemas.Collation{
{
Name: "Latin1_General_CI_AS",
Column: "name",
},
}
}
return nil
}
func TestCollate(t *testing.T) {
assert.NoError(t, PrepareEngine())
assertSync(t, &TestCollateColumn{
dbtype: string(testEngine.Dialect().URI().DBType),
})
_, err := testEngine.Insert(&TestCollateColumn{
UserId: 1,
Name: "test",
})
assert.NoError(t, err)
_, err = testEngine.Insert(&TestCollateColumn{
UserId: 1,
Name: "Test",
})
if testEngine.Dialect().URI().DBType == schemas.MYSQL {
ver, err1 := testEngine.DBVersion()
assert.NoError(t, err1)
tables, err1 := testEngine.DBMetas()
assert.NoError(t, err1)
for _, table := range tables {
if table.Name == "test_collate_column" {
col := table.GetColumn("name")
if col == nil {
assert.Error(t, errors.New("not found column"))
return
}
// tidb doesn't follow utf8mb4_general_ci
if col.Collation == "utf8mb4_general_ci" && ver.Edition != "TiDB" {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
break
}
}
} else if testEngine.Dialect().URI().DBType == schemas.MSSQL {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
// Since SQLITE don't support modify column SQL, currrently just ignore
if testEngine.Dialect().URI().DBType != schemas.MYSQL && testEngine.Dialect().URI().DBType != schemas.MSSQL {
return
}
var newCollation string
if testEngine.Dialect().URI().DBType == schemas.MYSQL {
newCollation = "utf8mb4_bin"
} else if testEngine.Dialect().URI().DBType != schemas.MSSQL {
newCollation = "Latin1_General_CS_AS"
} else {
return
}
alterSQL := testEngine.Dialect().ModifyColumnSQL("test_collate_column", &schemas.Column{
Name: "name",
SQLType: schemas.SQLType{
Name: "VARCHAR",
},
Length: 20,
Nullable: true,
DefaultIsEmpty: true,
Collation: newCollation,
})
_, err = testEngine.Exec(alterSQL)
assert.NoError(t, err)
_, err = testEngine.Insert(&TestCollateColumn{
UserId: 1,
Name: "test1",
})
assert.NoError(t, err)
_, err = testEngine.Insert(&TestCollateColumn{
UserId: 1,
Name: "Test1",
})
assert.NoError(t, err)
}

View File

@ -5,11 +5,13 @@
package integrations package integrations
import ( import (
"bytes"
"strconv" "strconv"
"testing" "testing"
"time" "time"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm/schemas" "xorm.io/xorm/schemas"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -381,3 +383,68 @@ func TestQueryStringWithLimit(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 0, len(data)) assert.EqualValues(t, 0, len(data))
} }
func TestQueryBLOBInMySQL(t *testing.T) {
assert.NoError(t, PrepareEngine())
var err error
type Avatar struct {
Id int64 `xorm:"autoincr pk"`
Avatar []byte `xorm:"BLOB"`
}
assert.NoError(t, testEngine.Sync(new(Avatar)))
testEngine.Delete(Avatar{})
repeatBytes := func(n int, b byte) []byte {
return bytes.Repeat([]byte{b}, n)
}
const N = 10
var data = []Avatar{}
for i := 0; i < N; i++ {
// allocate a []byte that is as twice big as the last one
// so that the underlying buffer will need to reallocate when querying
bs := repeatBytes(1<<(i+2), 'A'+byte(i))
data = append(data, Avatar{
Avatar: bs,
})
}
_, err = testEngine.Insert(data)
assert.NoError(t, err)
defer func() {
testEngine.Delete(Avatar{})
}()
{
records, err := testEngine.QueryInterface("select avatar from " + testEngine.Quote(testEngine.TableName("avatar", true)))
assert.NoError(t, err)
for i, record := range records {
bs := record["avatar"].([]byte)
assert.EqualValues(t, repeatBytes(1<<(i+2), 'A'+byte(i))[:3], bs[:3])
t.Logf("%d => %p => %02x %02x %02x", i, bs, bs[0], bs[1], bs[2])
}
}
{
arr := make([][]interface{}, 0)
err = testEngine.Table(testEngine.Quote(testEngine.TableName("avatar", true))).Cols("avatar").Find(&arr)
assert.NoError(t, err)
for i, record := range arr {
bs := record[0].([]byte)
assert.EqualValues(t, repeatBytes(1<<(i+2), 'A'+byte(i))[:3], bs[:3])
t.Logf("%d => %p => %02x %02x %02x", i, bs, bs[0], bs[1], bs[2])
}
}
{
arr := make([]map[string]interface{}, 0)
err = testEngine.Table(testEngine.Quote(testEngine.TableName("avatar", true))).Cols("avatar").Find(&arr)
assert.NoError(t, err)
for i, record := range arr {
bs := record["avatar"].([]byte)
assert.EqualValues(t, repeatBytes(1<<(i+2), 'A'+byte(i))[:3], bs[:3])
t.Logf("%d => %p => %02x %02x %02x", i, bs, bs[0], bs[1], bs[2])
}
}
}

View File

@ -185,3 +185,36 @@ func TestMultipleTransaction(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 0, len(ms)) assert.EqualValues(t, 0, len(ms))
} }
func TestInsertMulti2InterfaceTransaction(t *testing.T) {
type Multi2InterfaceTransaction struct {
ID uint64 `xorm:"id pk autoincr"`
Name string
Alias string
CreateTime time.Time `xorm:"created"`
UpdateTime time.Time `xorm:"updated"`
}
assert.NoError(t, PrepareEngine())
assertSync(t, new(Multi2InterfaceTransaction))
session := testEngine.NewSession()
defer session.Close()
err := session.Begin()
assert.NoError(t, err)
users := []interface{}{
&Multi2InterfaceTransaction{Name: "a", Alias: "A"},
&Multi2InterfaceTransaction{Name: "b", Alias: "B"},
&Multi2InterfaceTransaction{Name: "c", Alias: "C"},
&Multi2InterfaceTransaction{Name: "d", Alias: "D"},
}
cnt, err := session.Insert(&users)
assert.NoError(t, err)
assert.EqualValues(t, len(users), cnt)
assert.NotPanics(t, func() {
err = session.Commit()
assert.NoError(t, err)
})
}

View File

@ -6,6 +6,7 @@ package statements
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"xorm.io/xorm/internal/utils" "xorm.io/xorm/internal/utils"
@ -26,14 +27,19 @@ func (statement *Statement) ConvertIDSQL(sqlStr string) string {
return "" return ""
} }
var top string var b strings.Builder
b.WriteString("SELECT ")
pLimitN := statement.LimitN pLimitN := statement.LimitN
if pLimitN != nil && statement.dialect.URI().DBType == schemas.MSSQL { if pLimitN != nil && statement.dialect.URI().DBType == schemas.MSSQL {
top = fmt.Sprintf("TOP %d ", *pLimitN) b.WriteString("TOP ")
b.WriteString(strconv.Itoa(*pLimitN))
b.WriteString(" ")
} }
b.WriteString(colstrs)
b.WriteString(" FROM ")
b.WriteString(sqls[1])
newsql := fmt.Sprintf("SELECT %s%s FROM %v", top, colstrs, sqls[1]) return b.String()
return newsql
} }
return "" return ""
} }
@ -54,7 +60,7 @@ func (statement *Statement) ConvertUpdateSQL(sqlStr string) (string, string) {
return "", "" return "", ""
} }
var whereStr = sqls[1] whereStr := sqls[1]
// TODO: for postgres only, if any other database? // TODO: for postgres only, if any other database?
var paraStr string var paraStr string

10
schemas/collation.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2023 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package schemas
type Collation struct {
Name string
Column string // blank means it's a table collation
}

View File

@ -45,6 +45,7 @@ type Column struct {
DisableTimeZone bool DisableTimeZone bool
TimeZone *time.Location // column specified time zone TimeZone *time.Location // column specified time zone
Comment string Comment string
Collation string
} }
// NewColumn creates a new column // NewColumn creates a new column
@ -89,6 +90,8 @@ func (col *Column) ValueOfV(dataStruct *reflect.Value) (*reflect.Value, error) {
v.Set(reflect.New(v.Type().Elem())) v.Set(reflect.New(v.Type().Elem()))
} }
v = v.Elem() v = v.Elem()
} else if v.Kind() == reflect.Interface {
v = reflect.Indirect(v.Elem())
} }
v = v.FieldByIndex([]int{i}) v = v.FieldByIndex([]int{i})
} }

View File

@ -27,6 +27,7 @@ type Table struct {
StoreEngine string StoreEngine string
Charset string Charset string
Comment string Comment string
Collation string
} }
// NewEmptyTable creates an empty table // NewEmptyTable creates an empty table
@ -36,7 +37,8 @@ func NewEmptyTable() *Table {
// NewTable creates a new Table object // NewTable creates a new Table object
func NewTable(name string, t reflect.Type) *Table { func NewTable(name string, t reflect.Type) *Table {
return &Table{Name: name, Type: t, return &Table{
Name: name, Type: t,
columnsSeq: make([]string, 0), columnsSeq: make([]string, 0),
columns: make([]*Column, 0), columns: make([]*Column, 0),
columnsMap: make(map[string][]*Column), columnsMap: make(map[string][]*Column),

View File

@ -30,7 +30,7 @@ func (session *Session) cacheDelete(table *schemas.Table, tableName, sqlStr stri
} }
for _, filter := range session.engine.dialect.Filters() { for _, filter := range session.engine.dialect.Filters() {
sqlStr = filter.Do(sqlStr) sqlStr = filter.Do(session.ctx, sqlStr)
} }
newsql := session.statement.ConvertIDSQL(sqlStr) newsql := session.statement.ConvertIDSQL(sqlStr)

View File

@ -283,7 +283,7 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in
} }
for _, filter := range session.engine.dialect.Filters() { for _, filter := range session.engine.dialect.Filters() {
sqlStr = filter.Do(sqlStr) sqlStr = filter.Do(session.ctx, sqlStr)
} }
newsql := session.statement.ConvertIDSQL(sqlStr) newsql := session.statement.ConvertIDSQL(sqlStr)

View File

@ -278,7 +278,7 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf
} }
for _, filter := range session.engine.dialect.Filters() { for _, filter := range session.engine.dialect.Filters() {
sqlStr = filter.Do(sqlStr) sqlStr = filter.Do(session.ctx, sqlStr)
} }
newsql := session.statement.ConvertIDSQL(sqlStr) newsql := session.statement.ConvertIDSQL(sqlStr)
if newsql == "" { if newsql == "" {

View File

@ -353,6 +353,7 @@ func (session *Session) insertStruct(bean interface{}) (int64, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
}
if needCommit { if needCommit {
if err := session.Commit(); err != nil { if err := session.Commit(); err != nil {
return 0, err return 0, err
@ -361,7 +362,6 @@ func (session *Session) insertStruct(bean interface{}) (int64, error) {
if id == 0 { if id == 0 {
return 0, errors.New("insert successfully but not returned id") return 0, errors.New("insert successfully but not returned id")
} }
}
defer handleAfterInsertProcessorFunc(bean) defer handleAfterInsertProcessorFunc(bean)

View File

@ -13,7 +13,7 @@ import (
func (session *Session) queryPreprocess(sqlStr *string, paramStr ...interface{}) { func (session *Session) queryPreprocess(sqlStr *string, paramStr ...interface{}) {
for _, filter := range session.engine.dialect.Filters() { for _, filter := range session.engine.dialect.Filters() {
*sqlStr = filter.Do(*sqlStr) *sqlStr = filter.Do(session.ctx, *sqlStr)
} }
session.lastSQL = *sqlStr session.lastSQL = *sqlStr

View File

@ -15,7 +15,6 @@ import (
"xorm.io/xorm/dialects" "xorm.io/xorm/dialects"
"xorm.io/xorm/internal/utils" "xorm.io/xorm/internal/utils"
"xorm.io/xorm/schemas"
) )
// Ping test if database is ok // Ping test if database is ok
@ -169,7 +168,7 @@ func (session *Session) dropTable(beanOrTableName interface{}) error {
return nil return nil
} }
var seqName = utils.SeqName(tableName) seqName := utils.SeqName(tableName)
exist, err := session.engine.dialect.IsSequenceExist(session.ctx, session.getQueryer(), seqName) exist, err := session.engine.dialect.IsSequenceExist(session.ctx, session.getQueryer(), seqName)
if err != nil { if err != nil {
return err return err
@ -244,228 +243,6 @@ func (session *Session) addUnique(tableName, uqeName string) error {
return err return err
} }
// Sync2 synchronize structs to database tables
// Depricated
func (session *Session) Sync2(beans ...interface{}) error {
return session.Sync(beans...)
}
// Sync synchronize structs to database tables
func (session *Session) Sync(beans ...interface{}) error {
engine := session.engine
if session.isAutoClose {
session.isAutoClose = false
defer session.Close()
}
tables, err := engine.dialect.GetTables(session.getQueryer(), session.ctx)
if err != nil {
return err
}
session.autoResetStatement = false
defer func() {
session.autoResetStatement = true
session.resetStatement()
}()
for _, bean := range beans {
v := utils.ReflectValue(bean)
table, err := engine.tagParser.ParseWithCache(v)
if err != nil {
return err
}
var tbName string
if len(session.statement.AltTableName) > 0 {
tbName = session.statement.AltTableName
} else {
tbName = engine.TableName(bean)
}
tbNameWithSchema := engine.tbNameWithSchema(tbName)
var oriTable *schemas.Table
for _, tb := range tables {
if strings.EqualFold(engine.tbNameWithSchema(tb.Name), engine.tbNameWithSchema(tbName)) {
oriTable = tb
break
}
}
// this is a new table
if oriTable == nil {
err = session.StoreEngine(session.statement.StoreEngine).createTable(bean)
if err != nil {
return err
}
err = session.createUniques(bean)
if err != nil {
return err
}
err = session.createIndexes(bean)
if err != nil {
return err
}
continue
}
// this will modify an old table
if err = engine.loadTableInfo(oriTable); err != nil {
return err
}
// check columns
for _, col := range table.Columns() {
var oriCol *schemas.Column
for _, col2 := range oriTable.Columns() {
if strings.EqualFold(col.Name, col2.Name) {
oriCol = col2
break
}
}
// column is not exist on table
if oriCol == nil {
session.statement.RefTable = table
session.statement.SetTableName(tbNameWithSchema)
if err = session.addColumn(col.Name); err != nil {
return err
}
continue
}
err = nil
expectedType := engine.dialect.SQLType(col)
curType := engine.dialect.SQLType(oriCol)
if expectedType != curType {
if expectedType == schemas.Text &&
strings.HasPrefix(curType, schemas.Varchar) {
// currently only support mysql & postgres
if engine.dialect.URI().DBType == schemas.MYSQL ||
engine.dialect.URI().DBType == schemas.POSTGRES {
engine.logger.Infof("Table %s column %s change type from %s to %s\n",
tbNameWithSchema, col.Name, curType, expectedType)
_, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col))
} else {
engine.logger.Warnf("Table %s column %s db type is %s, struct type is %s\n",
tbNameWithSchema, col.Name, curType, expectedType)
}
} else if strings.HasPrefix(curType, schemas.Varchar) && strings.HasPrefix(expectedType, schemas.Varchar) {
if engine.dialect.URI().DBType == schemas.MYSQL {
if oriCol.Length < col.Length {
engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n",
tbNameWithSchema, col.Name, oriCol.Length, col.Length)
_, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col))
}
}
} else {
if !(strings.HasPrefix(curType, expectedType) && curType[len(expectedType)] == '(') {
if !strings.EqualFold(schemas.SQLTypeName(curType), engine.dialect.Alias(schemas.SQLTypeName(expectedType))) {
engine.logger.Warnf("Table %s column %s db type is %s, struct type is %s",
tbNameWithSchema, col.Name, curType, expectedType)
}
}
}
} else if expectedType == schemas.Varchar {
if engine.dialect.URI().DBType == schemas.MYSQL {
if oriCol.Length < col.Length {
engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n",
tbNameWithSchema, col.Name, oriCol.Length, col.Length)
_, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col))
}
}
} else if col.Comment != oriCol.Comment {
_, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col))
}
if col.Default != oriCol.Default {
switch {
case col.IsAutoIncrement: // For autoincrement column, don't check default
case (col.SQLType.Name == schemas.Bool || col.SQLType.Name == schemas.Boolean) &&
((strings.EqualFold(col.Default, "true") && oriCol.Default == "1") ||
(strings.EqualFold(col.Default, "false") && oriCol.Default == "0")):
default:
engine.logger.Warnf("Table %s Column %s db default is %s, struct default is %s",
tbName, col.Name, oriCol.Default, col.Default)
}
}
if col.Nullable != oriCol.Nullable {
engine.logger.Warnf("Table %s Column %s db nullable is %v, struct nullable is %v",
tbName, col.Name, oriCol.Nullable, col.Nullable)
}
if err != nil {
return err
}
}
var foundIndexNames = make(map[string]bool)
var addedNames = make(map[string]*schemas.Index)
for name, index := range table.Indexes {
var oriIndex *schemas.Index
for name2, index2 := range oriTable.Indexes {
if index.Equal(index2) {
oriIndex = index2
foundIndexNames[name2] = true
break
}
}
if oriIndex != nil {
if oriIndex.Type != index.Type {
sql := engine.dialect.DropIndexSQL(tbNameWithSchema, oriIndex)
_, err = session.exec(sql)
if err != nil {
return err
}
oriIndex = nil
}
}
if oriIndex == nil {
addedNames[name] = index
}
}
for name2, index2 := range oriTable.Indexes {
if _, ok := foundIndexNames[name2]; !ok {
sql := engine.dialect.DropIndexSQL(tbNameWithSchema, index2)
_, err = session.exec(sql)
if err != nil {
return err
}
}
}
for name, index := range addedNames {
if index.Type == schemas.UniqueType {
session.statement.RefTable = table
session.statement.SetTableName(tbNameWithSchema)
err = session.addUnique(tbNameWithSchema, name)
} else if index.Type == schemas.IndexType {
session.statement.RefTable = table
session.statement.SetTableName(tbNameWithSchema)
err = session.addIndex(tbNameWithSchema, name)
}
if err != nil {
return err
}
}
// check all the columns which removed from struct fields but left on database tables.
for _, colName := range oriTable.ColumnsSeq() {
if table.GetColumn(colName) == nil {
engine.logger.Warnf("Table %s has column %s but struct has not related field", engine.TableName(oriTable.Name, true), colName)
}
}
}
return nil
}
// ImportFile SQL DDL file // ImportFile SQL DDL file
func (session *Session) ImportFile(ddlPath string) ([]sql.Result, error) { func (session *Session) ImportFile(ddlPath string) ([]sql.Result, error) {
file, err := os.Open(ddlPath) file, err := os.Open(ddlPath)
@ -490,7 +267,7 @@ func (session *Session) Import(r io.Reader) ([]sql.Result, error) {
if atEOF && len(data) == 0 { if atEOF && len(data) == 0 {
return 0, nil, nil return 0, nil, nil
} }
var oriInSingleQuote = inSingleQuote oriInSingleQuote := inSingleQuote
for i, b := range data { for i, b := range data {
if startComment { if startComment {
if b == '\n' { if b == '\n' {

View File

@ -34,7 +34,7 @@ func (session *Session) cacheUpdate(table *schemas.Table, tableName, sqlStr stri
return ErrCacheFailed return ErrCacheFailed
} }
for _, filter := range session.engine.dialect.Filters() { for _, filter := range session.engine.dialect.Filters() {
newsql = filter.Do(newsql) newsql = filter.Do(session.ctx, newsql)
} }
session.engine.logger.Debugf("[cache] new sql: %v, %v", oldhead, newsql) session.engine.logger.Debugf("[cache] new sql: %v, %v", oldhead, newsql)

273
sync.go Normal file
View File

@ -0,0 +1,273 @@
// Copyright 2023 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xorm
import (
"strings"
"xorm.io/xorm/internal/utils"
"xorm.io/xorm/schemas"
)
type SyncOptions struct {
WarnIfDatabaseColumnMissed bool
}
type SyncResult struct{}
// Sync the new struct changes to database, this method will automatically add
// table, column, index, unique. but will not delete or change anything.
// If you change some field, you should change the database manually.
func (engine *Engine) Sync(beans ...interface{}) error {
session := engine.NewSession()
defer session.Close()
return session.Sync(beans...)
}
// SyncWithOptions sync the database schemas according options and table structs
func (engine *Engine) SyncWithOptions(opts SyncOptions, beans ...interface{}) (*SyncResult, error) {
session := engine.NewSession()
defer session.Close()
return session.SyncWithOptions(opts, beans...)
}
// Sync2 synchronize structs to database tables
// Depricated
func (engine *Engine) Sync2(beans ...interface{}) error {
return engine.Sync(beans...)
}
// Sync2 synchronize structs to database tables
// Depricated
func (session *Session) Sync2(beans ...interface{}) error {
return session.Sync(beans...)
}
// Sync synchronize structs to database tables
func (session *Session) Sync(beans ...interface{}) error {
_, err := session.SyncWithOptions(SyncOptions{
WarnIfDatabaseColumnMissed: false,
}, beans...)
return err
}
func (session *Session) SyncWithOptions(opts SyncOptions, beans ...interface{}) (*SyncResult, error) {
engine := session.engine
if session.isAutoClose {
session.isAutoClose = false
defer session.Close()
}
tables, err := engine.dialect.GetTables(session.getQueryer(), session.ctx)
if err != nil {
return nil, err
}
session.autoResetStatement = false
defer func() {
session.autoResetStatement = true
session.resetStatement()
}()
var syncResult SyncResult
for _, bean := range beans {
v := utils.ReflectValue(bean)
table, err := engine.tagParser.ParseWithCache(v)
if err != nil {
return nil, err
}
var tbName string
if len(session.statement.AltTableName) > 0 {
tbName = session.statement.AltTableName
} else {
tbName = engine.TableName(bean)
}
tbNameWithSchema := engine.tbNameWithSchema(tbName)
var oriTable *schemas.Table
for _, tb := range tables {
if strings.EqualFold(engine.tbNameWithSchema(tb.Name), engine.tbNameWithSchema(tbName)) {
oriTable = tb
break
}
}
// this is a new table
if oriTable == nil {
err = session.StoreEngine(session.statement.StoreEngine).createTable(bean)
if err != nil {
return nil, err
}
err = session.createUniques(bean)
if err != nil {
return nil, err
}
err = session.createIndexes(bean)
if err != nil {
return nil, err
}
continue
}
// this will modify an old table
if err = engine.loadTableInfo(oriTable); err != nil {
return nil, err
}
// check columns
for _, col := range table.Columns() {
var oriCol *schemas.Column
for _, col2 := range oriTable.Columns() {
if strings.EqualFold(col.Name, col2.Name) {
oriCol = col2
break
}
}
// column is not exist on table
if oriCol == nil {
session.statement.RefTable = table
session.statement.SetTableName(tbNameWithSchema)
if err = session.addColumn(col.Name); err != nil {
return nil, err
}
continue
}
err = nil
expectedType := engine.dialect.SQLType(col)
curType := engine.dialect.SQLType(oriCol)
if expectedType != curType {
if expectedType == schemas.Text &&
strings.HasPrefix(curType, schemas.Varchar) {
// currently only support mysql & postgres
if engine.dialect.URI().DBType == schemas.MYSQL ||
engine.dialect.URI().DBType == schemas.POSTGRES {
engine.logger.Infof("Table %s column %s change type from %s to %s\n",
tbNameWithSchema, col.Name, curType, expectedType)
_, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col))
} else {
engine.logger.Warnf("Table %s column %s db type is %s, struct type is %s\n",
tbNameWithSchema, col.Name, curType, expectedType)
}
} else if strings.HasPrefix(curType, schemas.Varchar) && strings.HasPrefix(expectedType, schemas.Varchar) {
if engine.dialect.URI().DBType == schemas.MYSQL {
if oriCol.Length < col.Length {
engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n",
tbNameWithSchema, col.Name, oriCol.Length, col.Length)
_, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col))
}
}
} else {
if !(strings.HasPrefix(curType, expectedType) && curType[len(expectedType)] == '(') {
if !strings.EqualFold(schemas.SQLTypeName(curType), engine.dialect.Alias(schemas.SQLTypeName(expectedType))) {
engine.logger.Warnf("Table %s column %s db type is %s, struct type is %s",
tbNameWithSchema, col.Name, curType, expectedType)
}
}
}
} else if expectedType == schemas.Varchar {
if engine.dialect.URI().DBType == schemas.MYSQL {
if oriCol.Length < col.Length {
engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n",
tbNameWithSchema, col.Name, oriCol.Length, col.Length)
_, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col))
}
}
} else if col.Comment != oriCol.Comment {
_, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col))
}
if col.Default != oriCol.Default {
switch {
case col.IsAutoIncrement: // For autoincrement column, don't check default
case (col.SQLType.Name == schemas.Bool || col.SQLType.Name == schemas.Boolean) &&
((strings.EqualFold(col.Default, "true") && oriCol.Default == "1") ||
(strings.EqualFold(col.Default, "false") && oriCol.Default == "0")):
default:
engine.logger.Warnf("Table %s Column %s db default is %s, struct default is %s",
tbName, col.Name, oriCol.Default, col.Default)
}
}
if col.Nullable != oriCol.Nullable {
engine.logger.Warnf("Table %s Column %s db nullable is %v, struct nullable is %v",
tbName, col.Name, oriCol.Nullable, col.Nullable)
}
if err != nil {
return nil, err
}
}
foundIndexNames := make(map[string]bool)
addedNames := make(map[string]*schemas.Index)
for name, index := range table.Indexes {
var oriIndex *schemas.Index
for name2, index2 := range oriTable.Indexes {
if index.Equal(index2) {
oriIndex = index2
foundIndexNames[name2] = true
break
}
}
if oriIndex != nil {
if oriIndex.Type != index.Type {
sql := engine.dialect.DropIndexSQL(tbNameWithSchema, oriIndex)
_, err = session.exec(sql)
if err != nil {
return nil, err
}
oriIndex = nil
}
}
if oriIndex == nil {
addedNames[name] = index
}
}
for name2, index2 := range oriTable.Indexes {
if _, ok := foundIndexNames[name2]; !ok {
sql := engine.dialect.DropIndexSQL(tbNameWithSchema, index2)
_, err = session.exec(sql)
if err != nil {
return nil, err
}
}
}
for name, index := range addedNames {
if index.Type == schemas.UniqueType {
session.statement.RefTable = table
session.statement.SetTableName(tbNameWithSchema)
err = session.addUnique(tbNameWithSchema, name)
} else if index.Type == schemas.IndexType {
session.statement.RefTable = table
session.statement.SetTableName(tbNameWithSchema)
err = session.addIndex(tbNameWithSchema, name)
}
if err != nil {
return nil, err
}
}
if opts.WarnIfDatabaseColumnMissed {
// check all the columns which removed from struct fields but left on database tables.
for _, colName := range oriTable.ColumnsSeq() {
if table.GetColumn(colName) == nil {
engine.logger.Warnf("Table %s has column %s but struct has not related field", engine.TableName(oriTable.Name, true), colName)
}
}
}
}
return &syncResult, nil
}

View File

@ -31,6 +31,12 @@ type TableIndices interface {
var tpTableIndices = reflect.TypeOf((*TableIndices)(nil)).Elem() var tpTableIndices = reflect.TypeOf((*TableIndices)(nil)).Elem()
type TableCollations interface {
TableCollations() []*schemas.Collation
}
var tpTableCollations = reflect.TypeOf((*TableCollations)(nil)).Elem()
// Parser represents a parser for xorm tag // Parser represents a parser for xorm tag
type Parser struct { type Parser struct {
identifier string identifier string
@ -356,6 +362,22 @@ func (parser *Parser) Parse(v reflect.Value) (*schemas.Table, error) {
} }
} }
collations := tableCollations(v)
for _, collation := range collations {
if collation.Name == "" {
continue
}
if collation.Column == "" {
table.Collation = collation.Name
} else {
col := table.GetColumn(collation.Column)
if col == nil {
return nil, ErrUnsupportedType
}
col.Collation = collation.Name // this may override definition in struct tag
}
}
return table, nil return table, nil
} }
@ -377,3 +399,22 @@ func tableIndices(v reflect.Value) []*schemas.Index {
} }
return nil return nil
} }
func tableCollations(v reflect.Value) []*schemas.Collation {
if v.Type().Implements(tpTableCollations) {
return v.Interface().(TableCollations).TableCollations()
}
if v.Kind() == reflect.Ptr {
v = v.Elem()
if v.Type().Implements(tpTableCollations) {
return v.Interface().(TableCollations).TableCollations()
}
} else if v.CanAddr() {
v1 := v.Addr()
if v1.Type().Implements(tpTableCollations) {
return v1.Interface().(TableCollations).TableCollations()
}
}
return nil
}

View File

@ -123,6 +123,7 @@ var defaultTagHandlers = map[string]Handler{
"COMMENT": CommentTagHandler, "COMMENT": CommentTagHandler,
"EXTENDS": ExtendsTagHandler, "EXTENDS": ExtendsTagHandler,
"UNSIGNED": UnsignedTagHandler, "UNSIGNED": UnsignedTagHandler,
"COLLATE": CollateTagHandler,
} }
func init() { func init() {
@ -282,6 +283,16 @@ func CommentTagHandler(ctx *Context) error {
return nil return nil
} }
func CollateTagHandler(ctx *Context) error {
if len(ctx.params) > 0 {
ctx.col.Collation = ctx.params[0]
} else {
ctx.col.Collation = ctx.nextTag
ctx.ignoreNext = true
}
return nil
}
// SQLTypeTagHandler describes SQL Type tag handler // SQLTypeTagHandler describes SQL Type tag handler
func SQLTypeTagHandler(ctx *Context) error { func SQLTypeTagHandler(ctx *Context) error {
ctx.col.SQLType = schemas.SQLType{Name: ctx.tagUname} ctx.col.SQLType = schemas.SQLType{Name: ctx.tagUname}

View File

@ -11,11 +11,12 @@ import (
) )
func TestSplitTag(t *testing.T) { func TestSplitTag(t *testing.T) {
var cases = []struct { cases := []struct {
tag string tag string
tags []tag tags []tag
}{ }{
{"not null default '2000-01-01 00:00:00' TIMESTAMP", []tag{ {
"not null default '2000-01-01 00:00:00' TIMESTAMP", []tag{
{ {
name: "not", name: "not",
}, },
@ -33,13 +34,15 @@ func TestSplitTag(t *testing.T) {
}, },
}, },
}, },
{"TEXT", []tag{ {
"TEXT", []tag{
{ {
name: "TEXT", name: "TEXT",
}, },
}, },
}, },
{"default('2000-01-01 00:00:00')", []tag{ {
"default('2000-01-01 00:00:00')", []tag{
{ {
name: "default", name: "default",
params: []string{ params: []string{
@ -48,7 +51,8 @@ func TestSplitTag(t *testing.T) {
}, },
}, },
}, },
{"json binary", []tag{ {
"json binary", []tag{
{ {
name: "json", name: "json",
}, },
@ -57,14 +61,16 @@ func TestSplitTag(t *testing.T) {
}, },
}, },
}, },
{"numeric(10, 2)", []tag{ {
"numeric(10, 2)", []tag{
{ {
name: "numeric", name: "numeric",
params: []string{"10", "2"}, params: []string{"10", "2"},
}, },
}, },
}, },
{"numeric(10, 2) notnull", []tag{ {
"numeric(10, 2) notnull", []tag{
{ {
name: "numeric", name: "numeric",
params: []string{"10", "2"}, params: []string{"10", "2"},
@ -74,6 +80,16 @@ func TestSplitTag(t *testing.T) {
}, },
}, },
}, },
{
"collate utf8mb4_bin", []tag{
{
name: "collate",
},
{
name: "utf8mb4_bin",
},
},
},
} }
for _, kase := range cases { for _, kase := range cases {
@ -82,7 +98,7 @@ func TestSplitTag(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, len(tags), len(kase.tags)) assert.EqualValues(t, len(tags), len(kase.tags))
for i := 0; i < len(tags); i++ { for i := 0; i < len(tags); i++ {
assert.Equal(t, tags[i], kase.tags[i]) assert.Equal(t, kase.tags[i], tags[i])
} }
}) })
} }