feat: Production readiness improvements for WHOOSH council formation
Major security, observability, and configuration improvements:
## Security Hardening
- Implemented configurable CORS (no more wildcards)
- Added comprehensive auth middleware for admin endpoints
- Enhanced webhook HMAC validation
- Added input validation and rate limiting
- Security headers and CSP policies
## Configuration Management
- Made N8N webhook URL configurable (WHOOSH_N8N_BASE_URL)
- Replaced all hardcoded endpoints with environment variables
- Added feature flags for LLM vs heuristic composition
- Gitea fetch hardening with EAGER_FILTER and FULL_RESCAN options
## API Completeness
- Implemented GetCouncilComposition function
- Added GET /api/v1/councils/{id} endpoint
- Council artifacts API (POST/GET /api/v1/councils/{id}/artifacts)
- /admin/health/details endpoint with component status
- Database lookup for repository URLs (no hardcoded fallbacks)
## Observability & Performance
- Added OpenTelemetry distributed tracing with goal/pulse correlation
- Performance optimization database indexes
- Comprehensive health monitoring
- Enhanced logging and error handling
## Infrastructure
- Production-ready P2P discovery (replaces mock implementation)
- Removed unused Redis configuration
- Enhanced Docker Swarm integration
- Added migration files for performance indexes
## Code Quality
- Comprehensive input validation
- Graceful error handling and failsafe fallbacks
- Backwards compatibility maintained
- Following security best practices
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
12
vendor/github.com/golang-migrate/migrate/v4/.dockerignore
generated
vendored
Normal file
12
vendor/github.com/golang-migrate/migrate/v4/.dockerignore
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Project
|
||||
FAQ.md
|
||||
README.md
|
||||
LICENSE
|
||||
.gitignore
|
||||
.travis.yml
|
||||
CONTRIBUTING.md
|
||||
MIGRATIONS.md
|
||||
docker-deploy.sh
|
||||
|
||||
# Golang
|
||||
testing
|
||||
10
vendor/github.com/golang-migrate/migrate/v4/.gitignore
generated
vendored
Normal file
10
vendor/github.com/golang-migrate/migrate/v4/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
cli/build
|
||||
cli/cli
|
||||
cli/migrate
|
||||
.coverage
|
||||
.godoc.pid
|
||||
vendor/
|
||||
.vscode/
|
||||
.idea
|
||||
dist/
|
||||
26
vendor/github.com/golang-migrate/migrate/v4/.golangci.yml
generated
vendored
Normal file
26
vendor/github.com/golang-migrate/migrate/v4/.golangci.yml
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
run:
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
timeout: 5m
|
||||
linters:
|
||||
enable:
|
||||
#- golint
|
||||
- interfacer
|
||||
- unconvert
|
||||
#- dupl
|
||||
- goconst
|
||||
- gofmt
|
||||
- misspell
|
||||
- unparam
|
||||
- nakedret
|
||||
- prealloc
|
||||
#- gosec
|
||||
linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
issues:
|
||||
max-same-issues: 0
|
||||
max-issues-per-linter: 0
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
# gosec: Duplicated errcheck checks
|
||||
- G104
|
||||
103
vendor/github.com/golang-migrate/migrate/v4/.goreleaser.yml
generated
vendored
Normal file
103
vendor/github.com/golang-migrate/migrate/v4/.goreleaser.yml
generated
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
project_name: migrate
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- 386
|
||||
goarm:
|
||||
- 7
|
||||
main: ./cmd/migrate
|
||||
ldflags:
|
||||
- '-w -s -X main.Version={{ .Version }} -extldflags "static"'
|
||||
flags:
|
||||
- "-tags={{ .Env.DATABASE }} {{ .Env.SOURCE }}"
|
||||
- "-trimpath"
|
||||
nfpms:
|
||||
- homepage: "https://github.com/golang-migrate/migrate"
|
||||
maintainer: "dhui@users.noreply.github.com"
|
||||
license: MIT
|
||||
description: "Database migrations"
|
||||
formats:
|
||||
- deb
|
||||
file_name_template: "{{ .ProjectName }}.{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
dockers:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.github-actions
|
||||
use: buildx
|
||||
ids:
|
||||
- migrate
|
||||
image_templates:
|
||||
- 'migrate/migrate:{{ .Tag }}-amd64'
|
||||
build_flag_templates:
|
||||
- '--label=org.opencontainers.image.created={{ .Date }}'
|
||||
- '--label=org.opencontainers.image.title={{ .ProjectName }}'
|
||||
- '--label=org.opencontainers.image.revision={{ .FullCommit }}'
|
||||
- '--label=org.opencontainers.image.version={{ .Version }}'
|
||||
- "--label=org.opencontainers.image.source={{ .GitURL }}"
|
||||
- "--platform=linux/amd64"
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile.github-actions
|
||||
use: buildx
|
||||
ids:
|
||||
- migrate
|
||||
image_templates:
|
||||
- 'migrate/migrate:{{ .Tag }}-arm64'
|
||||
build_flag_templates:
|
||||
- '--label=org.opencontainers.image.created={{ .Date }}'
|
||||
- '--label=org.opencontainers.image.title={{ .ProjectName }}'
|
||||
- '--label=org.opencontainers.image.revision={{ .FullCommit }}'
|
||||
- '--label=org.opencontainers.image.version={{ .Version }}'
|
||||
- "--label=org.opencontainers.image.source={{ .GitURL }}"
|
||||
- "--platform=linux/arm64"
|
||||
|
||||
docker_manifests:
|
||||
- name_template: 'migrate/migrate:{{ .Tag }}'
|
||||
image_templates:
|
||||
- 'migrate/migrate:{{ .Tag }}-amd64'
|
||||
- 'migrate/migrate:{{ .Tag }}-arm64'
|
||||
- name_template: 'migrate/migrate:{{ .Major }}'
|
||||
image_templates:
|
||||
- 'migrate/migrate:{{ .Tag }}-amd64'
|
||||
- 'migrate/migrate:{{ .Tag }}-arm64'
|
||||
- name_template: 'migrate/migrate:latest'
|
||||
image_templates:
|
||||
- 'migrate/migrate:{{ .Tag }}-amd64'
|
||||
- 'migrate/migrate:{{ .Tag }}-arm64'
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}.{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
checksum:
|
||||
name_template: 'sha256sum.txt'
|
||||
release:
|
||||
draft: true
|
||||
prerelease: auto
|
||||
source:
|
||||
enabled: true
|
||||
rlcp: true
|
||||
format: zip
|
||||
changelog:
|
||||
skip: false
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- Merge pull request
|
||||
- Merge branch
|
||||
- go mod tidy
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
138
vendor/github.com/golang-migrate/migrate/v4/.travis.yml
generated
vendored
Normal file
138
vendor/github.com/golang-migrate/migrate/v4/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
language: go
|
||||
sudo: required
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
include:
|
||||
# Supported versions of Go: https://golang.org/dl/
|
||||
- go: "1.14.x"
|
||||
- go: "1.15.x"
|
||||
- go: master
|
||||
|
||||
go_import_path: github.com/golang-migrate/migrate
|
||||
|
||||
env:
|
||||
global:
|
||||
- GO111MODULE=on
|
||||
- MIGRATE_TEST_CONTAINER_BOOT_TIMEOUT=60
|
||||
- DOCKER_USERNAME=golangmigrate
|
||||
- secure: "oSOznzUrgr5h45qW4PONkREpisPAt40tnM+KFWtS/Ggu5UI2Ie0CmyYXWuBjbt7B97a4yN9Qzmn8FxJHJ7kk+ABOi3muhkxeIhr6esXbzHhX/Jhv0mj1xkzX7KoVN9oHBz3cOI/QeRyEAO68xjDHNE2kby4RTT9VBt6TQUakKVkqI5qkqLBTADepCjVC+9XhxVxUNyeWKU8ormaUfJBjoNVoDlwXekUPnJenfmfZqXxUInvBCfUyp7Pq+kurBORmg4yc6qOlRYuK67Xw+i5xpjbZouNlXPk0rq7pPy5zjhmZQ3kImoFPvNMeKViDcI6kSIJKtjdhms9/g/6MgXS9HlL5kFy8tYKbsyiHnHB1BsvaLAKXctbUZFDPstgMPADfnad2kZXPrNqIhfWKZrGRWidawCYJ1sKKwYxLMKrtA0umqgMoL90MmBOELhuGmvMV0cFJB+zo+K2YWjEiMGd8xRb5mC5aAy0ZcCehO46jGtpr217EJmMF8Ywr7cFqM2Shg5U2jev9qUpYiXwmPnJKDuoT2ZHuHmPgFIkYiWC5yeJnnmG5bed1sKBp93AFrJX+1Rx5oC4BpNegewmBZKpOSwls/D1uMAeQK3dPmQHLsT6o2VBLfeDGr+zY0R85ywwPZCv00vGol02zYoTqN7eFqr6Qhjr/qx5K1nnxJdFK3Ts="
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $GOPATH/pkg
|
||||
|
||||
|
||||
before_install:
|
||||
# Update docker to latest version: https://docs.travis-ci.com/user/docker/#installing-a-newer-docker-version
|
||||
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
- sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
- sudo apt-get update
|
||||
- sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
|
||||
# Install golangci-lint
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0
|
||||
- echo "TRAVIS_GO_VERSION=${TRAVIS_GO_VERSION}"
|
||||
|
||||
install:
|
||||
- go get github.com/mattn/goveralls
|
||||
|
||||
script:
|
||||
- golangci-lint run
|
||||
- make test COVERAGE_DIR=/tmp/coverage
|
||||
|
||||
after_success:
|
||||
- goveralls -service=travis-ci -coverprofile /tmp/coverage/combined.txt
|
||||
- make list-external-deps > dependency_tree.txt && cat dependency_tree.txt
|
||||
- make build-cli
|
||||
- gem install --no-document fpm
|
||||
- fpm -s dir -t deb -n migrate -v "$(git describe --tags 2>/dev/null | cut -c 2-)" --license MIT -m dhui@users.noreply.github.com --url https://github.com/golang-migrate/migrate --description='Database migrations' -a amd64 -p migrate.$(git describe --tags 2>/dev/null | cut -c 2-).deb --deb-no-default-config-files -f -C cli/build migrate.linux-amd64=/usr/local/bin/migrate
|
||||
|
||||
deploy:
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: hWH1HLPpzpfA8pXQ93T1qKQVFSpQp0as/JLQ7D91jHuJ8p+RxVeqblDrR6HQY/95R/nyiE9GJmvUolSuw5h449LSrGxPtVWhdh6EnkxlQHlen5XeMhVjRjFV0sE9qGe8v7uAkiTfRO61ktTWHrEAvw5qpyqnNISodmZS78XIasPODQbNlzwINhWhDTHIjXGb4FpizYaL3OGCanrxfR9fQyCaqKGGBjRq3Mfq8U6Yd4mApmsE+uJxgaZV8K5zBqpkSzQRWhcVGNL5DuLsU3gfSJOo7kZeA2G71SHffH577dBoqtCZ4VFv169CoUZehLWCb+7XKJZmHXVujCURATSySLGUOPc6EoLFAn3YtsCA04mS4bZVo5FZPWVwfhjmkhtDR4f6wscKp7r1HsFHSOgm59QfETQdrn4MnZ44H2Jd39axqndn5DvK9EcZVjPHynOPnueXP2u6mTuUgh2VyyWBCDO3CNo0fGlo7VJI69IkIWNSD87K9cHZWYMClyKZkUzS+PmRAhHRYbVd+9ZjKOmnU36kUHNDG/ft1D4ogsY+rhVtXB4lgWDM5adri+EIScYdYnB1/pQexLBigcJY9uE7nQTR0U6QgVNYvun7uRNs40E0c4voSfmPdFO0FlOD2y1oQhnaXfWLbu9nMcTcs4RFGrcC7NzkUN4/WjG8s285V6w=
|
||||
skip_cleanup: true
|
||||
on:
|
||||
go: "1.15.x"
|
||||
repo: golang-migrate/migrate
|
||||
tags: true
|
||||
file:
|
||||
- cli/build/migrate.linux-amd64.tar.gz
|
||||
- cli/build/migrate.linux-armv7.tar.gz
|
||||
- cli/build/migrate.linux-arm64.tar.gz
|
||||
- cli/build/migrate.darwin-amd64.tar.gz
|
||||
- cli/build/migrate.windows-amd64.exe.tar.gz
|
||||
- cli/build/migrate.windows-386.exe.tar.gz
|
||||
- cli/build/sha256sum.txt
|
||||
- dependency_tree.txt
|
||||
- provider: packagecloud
|
||||
repository: migrate
|
||||
username: golang-migrate
|
||||
token:
|
||||
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
|
||||
dist: ubuntu/xenial
|
||||
package_glob: '*.deb'
|
||||
skip_cleanup: true
|
||||
on:
|
||||
go: "1.15.x"
|
||||
repo: golang-migrate/migrate
|
||||
tags: true
|
||||
- provider: packagecloud
|
||||
repository: migrate
|
||||
username: golang-migrate
|
||||
token:
|
||||
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
|
||||
dist: ubuntu/bionic
|
||||
package_glob: '*.deb'
|
||||
skip_cleanup: true
|
||||
on:
|
||||
go: "1.15.x"
|
||||
repo: golang-migrate/migrate
|
||||
tags: true
|
||||
- provider: packagecloud
|
||||
repository: migrate
|
||||
username: golang-migrate
|
||||
token:
|
||||
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
|
||||
dist: ubuntu/focal
|
||||
package_glob: '*.deb'
|
||||
skip_cleanup: true
|
||||
on:
|
||||
go: "1.15.x"
|
||||
repo: golang-migrate/migrate
|
||||
tags: true
|
||||
- provider: packagecloud
|
||||
repository: migrate
|
||||
username: golang-migrate
|
||||
token:
|
||||
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
|
||||
dist: debian/stretch
|
||||
package_glob: '*.deb'
|
||||
skip_cleanup: true
|
||||
on:
|
||||
go: "1.15.x"
|
||||
repo: golang-migrate/migrate
|
||||
tags: true
|
||||
- provider: packagecloud
|
||||
repository: migrate
|
||||
username: golang-migrate
|
||||
token:
|
||||
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
|
||||
dist: debian/buster
|
||||
package_glob: '*.deb'
|
||||
skip_cleanup: true
|
||||
on:
|
||||
go: "1.15.x"
|
||||
repo: golang-migrate/migrate
|
||||
tags: true
|
||||
- provider: script
|
||||
script: ./docker-deploy.sh
|
||||
skip_cleanup: true
|
||||
on:
|
||||
go: "1.15.x"
|
||||
repo: golang-migrate/migrate
|
||||
tags: true
|
||||
24
vendor/github.com/golang-migrate/migrate/v4/CONTRIBUTING.md
generated
vendored
Normal file
24
vendor/github.com/golang-migrate/migrate/v4/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Development, Testing and Contributing
|
||||
|
||||
1. Make sure you have a running Docker daemon
|
||||
(Install for [MacOS](https://docs.docker.com/docker-for-mac/))
|
||||
1. Use a version of Go that supports [modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) (e.g. Go 1.11+)
|
||||
1. Fork this repo and `git clone` somewhere to `$GOPATH/src/github.com/golang-migrate/migrate`
|
||||
* Ensure that [Go modules are enabled](https://golang.org/cmd/go/#hdr-Preliminary_module_support) (e.g. your repo path or the `GO111MODULE` environment variable are set correctly)
|
||||
1. Install [golangci-lint](https://github.com/golangci/golangci-lint#install)
|
||||
1. Run the linter: `golangci-lint run`
|
||||
1. Confirm tests are working: `make test-short`
|
||||
1. Write awesome code ...
|
||||
1. `make test` to run all tests against all database versions
|
||||
1. Push code and open Pull Request
|
||||
|
||||
Some more helpful commands:
|
||||
|
||||
* You can specify which database/ source tests to run:
|
||||
`make test-short SOURCE='file go_bindata' DATABASE='postgres cassandra'`
|
||||
* After `make test`, run `make html-coverage` which opens a shiny test coverage overview.
|
||||
* `make build-cli` builds the CLI in directory `cli/build/`.
|
||||
* `make list-external-deps` lists all external dependencies for each package
|
||||
* `make docs && make open-docs` opens godoc in your browser, `make kill-docs` kills the godoc server.
|
||||
Repeatedly call `make docs` to refresh the server.
|
||||
* Set the `DOCKER_API_VERSION` environment variable to the latest supported version if you get errors regarding the docker client API version being too new.
|
||||
26
vendor/github.com/golang-migrate/migrate/v4/Dockerfile
generated
vendored
Normal file
26
vendor/github.com/golang-migrate/migrate/v4/Dockerfile
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM golang:1.21-alpine3.19 AS builder
|
||||
ARG VERSION
|
||||
|
||||
RUN apk add --no-cache git gcc musl-dev make
|
||||
|
||||
WORKDIR /go/src/github.com/golang-migrate/migrate
|
||||
|
||||
ENV GO111MODULE=on
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN make build-docker
|
||||
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
COPY --from=builder /go/src/github.com/golang-migrate/migrate/build/migrate.linux-386 /usr/local/bin/migrate
|
||||
RUN ln -s /usr/local/bin/migrate /migrate
|
||||
|
||||
ENTRYPOINT ["migrate"]
|
||||
CMD ["--help"]
|
||||
17
vendor/github.com/golang-migrate/migrate/v4/Dockerfile.circleci
generated
vendored
Normal file
17
vendor/github.com/golang-migrate/migrate/v4/Dockerfile.circleci
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
ARG DOCKER_IMAGE
|
||||
FROM $DOCKER_IMAGE
|
||||
|
||||
RUN apk add --no-cache git gcc musl-dev make
|
||||
|
||||
WORKDIR /go/src/github.com/golang-migrate/migrate
|
||||
|
||||
ENV GO111MODULE=on
|
||||
ENV COVERAGE_DIR=/tmp/coverage
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . ./
|
||||
|
||||
CMD ["make", "test"]
|
||||
11
vendor/github.com/golang-migrate/migrate/v4/Dockerfile.github-actions
generated
vendored
Normal file
11
vendor/github.com/golang-migrate/migrate/v4/Dockerfile.github-actions
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
COPY migrate /usr/local/bin/migrate
|
||||
|
||||
RUN ln -s /usr/local/bin/migrate /usr/bin/migrate
|
||||
RUN ln -s /usr/local/bin/migrate /migrate
|
||||
|
||||
ENTRYPOINT ["migrate"]
|
||||
CMD ["--help"]
|
||||
79
vendor/github.com/golang-migrate/migrate/v4/FAQ.md
generated
vendored
Normal file
79
vendor/github.com/golang-migrate/migrate/v4/FAQ.md
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
# FAQ
|
||||
|
||||
#### How is the code base structured?
|
||||
```
|
||||
/ package migrate (the heart of everything)
|
||||
/cli the CLI wrapper
|
||||
/database database driver and sub directories have the actual driver implementations
|
||||
/source source driver and sub directories have the actual driver implementations
|
||||
```
|
||||
|
||||
#### Why is there no `source/driver.go:Last()`?
|
||||
It's not needed. And unless the source has a "native" way to read a directory in reversed order,
|
||||
it might be expensive to do a full directory scan in order to get the last element.
|
||||
|
||||
#### What is a NilMigration? NilVersion?
|
||||
NilMigration defines a migration without a body. NilVersion is defined as const -1.
|
||||
|
||||
#### What is the difference between uint(version) and int(targetVersion)?
|
||||
version refers to an existing migration version coming from a source and therefore can never be negative.
|
||||
targetVersion can either be a version OR represent a NilVersion, which equals -1.
|
||||
|
||||
#### What's the difference between Next/Previous and Up/Down?
|
||||
```
|
||||
1_first_migration.up.extension next -> 2_second_migration.up.extension ...
|
||||
1_first_migration.down.extension <- previous 2_second_migration.down.extension ...
|
||||
```
|
||||
|
||||
#### Why two separate files (up and down) for a migration?
|
||||
It makes all of our lives easier. No new markup/syntax to learn for users
|
||||
and existing database utility tools continue to work as expected.
|
||||
|
||||
#### How many migrations can migrate handle?
|
||||
Whatever the maximum positive signed integer value is for your platform.
|
||||
For 32bit it would be 2,147,483,647 migrations. Migrate only keeps references to
|
||||
the currently run and pre-fetched migrations in memory. Please note that some
|
||||
source drivers need to do build a full "directory" tree first, which puts some
|
||||
heat on the memory consumption.
|
||||
|
||||
#### Are the table tests in migrate_test.go bloated?
|
||||
Yes and no. There are duplicate test cases for sure but they don't hurt here. In fact
|
||||
the tests are very visual now and might help new users understand expected behaviors quickly.
|
||||
Migrate from version x to y and y is the last migration? Just check out the test for
|
||||
that particular case and know what's going on instantly.
|
||||
|
||||
#### What is Docker being used for?
|
||||
Only for testing. See [testing/docker.go](testing/docker.go)
|
||||
|
||||
#### Why not just use docker-compose?
|
||||
It doesn't give us enough runtime control for testing. We want to be able to bring up containers fast
|
||||
and whenever we want, not just once at the beginning of all tests.
|
||||
|
||||
#### Can I maintain my driver in my own repository?
|
||||
Yes, technically thats possible. We want to encourage you to contribute your driver to this repository though.
|
||||
The driver's functionality is dictated by migrate's interfaces. That means there should really
|
||||
just be one driver for a database/ source. We want to prevent a future where several drivers doing the exact same thing,
|
||||
just implemented a bit differently, co-exist somewhere on GitHub. If users have to do research first to find the
|
||||
"best" available driver for a database in order to get started, we would have failed as an open source community.
|
||||
|
||||
#### Can I mix multiple sources during a batch of migrations?
|
||||
No.
|
||||
|
||||
#### What does "dirty" database mean?
|
||||
Before a migration runs, each database sets a dirty flag. Execution stops if a migration fails and the dirty state persists,
|
||||
which prevents attempts to run more migrations on top of a failed migration. You need to manually fix the error
|
||||
and then "force" the expected version.
|
||||
|
||||
#### What happens if two programs try and update the database at the same time?
|
||||
Database-specific locking features are used by *some* database drivers to prevent multiple instances of migrate from running migrations at the same time
|
||||
the same database at the same time. For example, the MySQL driver uses the `GET_LOCK` function, while the Postgres driver uses
|
||||
the `pg_advisory_lock` function.
|
||||
|
||||
#### Do I need to create a table for tracking migration version used?
|
||||
No, it is done automatically.
|
||||
|
||||
#### Can I use migrate with a non-Go project?
|
||||
Yes, you can use the migrate CLI in a non-Go project, but there are probably other libraries/frameworks available that offer better test and deploy integrations in that language/framework.
|
||||
|
||||
#### I have got an error `Dirty database version 1. Fix and force version`. What should I do?
|
||||
Keep calm and refer to [the getting started docs](GETTING_STARTED.md#forcing-your-database-version).
|
||||
53
vendor/github.com/golang-migrate/migrate/v4/GETTING_STARTED.md
generated
vendored
Normal file
53
vendor/github.com/golang-migrate/migrate/v4/GETTING_STARTED.md
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# Getting started
|
||||
Before you start, you should understand the concept of forward/up and reverse/down database migrations.
|
||||
|
||||
Configure a database for your application. Make sure that your database driver is supported [here](README.md#databases).
|
||||
|
||||
## Create migrations
|
||||
Create some migrations using migrate CLI. Here is an example:
|
||||
```
|
||||
migrate create -ext sql -dir db/migrations -seq create_users_table
|
||||
```
|
||||
Once you create your files, you should fill them.
|
||||
|
||||
**IMPORTANT:** In a project developed by more than one person there is a chance of migrations inconsistency - e.g. two developers can create conflicting migrations, and the developer that created their migration later gets it merged to the repository first.
|
||||
Developers and Teams should keep an eye on such cases (especially during code review).
|
||||
[Here](https://github.com/golang-migrate/migrate/issues/179#issuecomment-475821264) is the issue summary if you would like to read more.
|
||||
|
||||
Consider making your migrations idempotent - we can run the same sql code twice in a row with the same result. This makes our migrations more robust. On the other hand, it causes slightly less control over database schema - e.g. let's say you forgot to drop the table in down migration. You run down migration - the table is still there. When you run up migration again - `CREATE TABLE` would return an error, helping you find an issue in down migration, while `CREATE TABLE IF NOT EXISTS` would not. Use those conditions wisely.
|
||||
|
||||
In case you would like to run several commands/queries in one migration, you should wrap them in a transaction (if your database supports it).
|
||||
This way if one of commands fails, our database will remain unchanged.
|
||||
|
||||
## Run migrations
|
||||
Run your migrations through the CLI or your app and check if they applied expected changes.
|
||||
Just to give you an idea:
|
||||
```
|
||||
migrate -database YOUR_DATABASE_URL -path PATH_TO_YOUR_MIGRATIONS up
|
||||
```
|
||||
|
||||
Just add the code to your app and you're ready to go!
|
||||
|
||||
Before committing your migrations you should run your migrations up, down, and then up again to see if migrations are working properly both ways.
|
||||
(e.g. if you created a table in a migration but reverse migration did not delete it, you will encounter an error when running the forward migration again)
|
||||
It's also worth checking your migrations in a separate, containerized environment. You can find some tools at the [end of this document](#further-reading).
|
||||
|
||||
**IMPORTANT:** If you would like to run multiple instances of your app on different machines be sure to use a database that supports locking when running migrations. Otherwise you may encounter issues.
|
||||
|
||||
## Forcing your database version
|
||||
In case you run a migration that contained an error, migrate will not let you run other migrations on the same database. You will see an error like `Dirty database version 1. Fix and force version`, even when you fix the erred migration. This means your database was marked as 'dirty'.
|
||||
You need to investigate the migration error - was your migration applied partially, or was it not applied at all? Once you know, you should force your database to a version reflecting it's real state. You can do so with `force` command:
|
||||
```
|
||||
migrate -path PATH_TO_YOUR_MIGRATIONS -database YOUR_DATABASE_URL force VERSION
|
||||
```
|
||||
Once you force the version and your migration was fixed, your database is 'clean' again and you can proceed with your migrations.
|
||||
|
||||
For details and example of usage see [this comment](https://github.com/golang-migrate/migrate/issues/282#issuecomment-530743258).
|
||||
|
||||
## Further reading:
|
||||
- [PostgreSQL tutorial](database/postgres/TUTORIAL.md)
|
||||
- [Best practices](MIGRATIONS.md)
|
||||
- [FAQ](FAQ.md)
|
||||
- Tools for testing your migrations in a container:
|
||||
- https://github.com/dhui/dktest
|
||||
- https://github.com/ory/dockertest
|
||||
28
vendor/github.com/golang-migrate/migrate/v4/LICENSE
generated
vendored
Normal file
28
vendor/github.com/golang-migrate/migrate/v4/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Original Work
|
||||
Copyright (c) 2016 Matthias Kadenbach
|
||||
https://github.com/mattes/migrate
|
||||
|
||||
Modified Work
|
||||
Copyright (c) 2018 Dale Hui
|
||||
https://github.com/golang-migrate/migrate
|
||||
|
||||
|
||||
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.
|
||||
86
vendor/github.com/golang-migrate/migrate/v4/MIGRATIONS.md
generated
vendored
Normal file
86
vendor/github.com/golang-migrate/migrate/v4/MIGRATIONS.md
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
# Migrations
|
||||
|
||||
## Migration Filename Format
|
||||
|
||||
A single logical migration is represented as two separate migration files, one
|
||||
to migrate "up" to the specified version from the previous version, and a second
|
||||
to migrate back "down" to the previous version. These migrations can be provided
|
||||
by any one of the supported [migration sources](./README.md#migration-sources).
|
||||
|
||||
The ordering and direction of the migration files is determined by the filenames
|
||||
used for them. `migrate` expects the filenames of migrations to have the format:
|
||||
|
||||
{version}_{title}.up.{extension}
|
||||
{version}_{title}.down.{extension}
|
||||
|
||||
The `title` of each migration is unused, and is only for readability. Similarly,
|
||||
the `extension` of the migration files is not checked by the library, and should
|
||||
be an appropriate format for the database in use (`.sql` for SQL variants, for
|
||||
instance).
|
||||
|
||||
Versions of migrations may be represented as any 64 bit unsigned integer.
|
||||
All migrations are applied upward in order of increasing version number, and
|
||||
downward by decreasing version number.
|
||||
|
||||
Common versioning schemes include incrementing integers:
|
||||
|
||||
1_initialize_schema.down.sql
|
||||
1_initialize_schema.up.sql
|
||||
2_add_table.down.sql
|
||||
2_add_table.up.sql
|
||||
...
|
||||
|
||||
Or timestamps at an appropriate resolution:
|
||||
|
||||
1500360784_initialize_schema.down.sql
|
||||
1500360784_initialize_schema.up.sql
|
||||
1500445949_add_table.down.sql
|
||||
1500445949_add_table.up.sql
|
||||
...
|
||||
|
||||
But any scheme resulting in distinct, incrementing integers as versions is valid.
|
||||
|
||||
It is suggested that the version number of corresponding `up` and `down` migration
|
||||
files be equivalent for clarity, but they are allowed to differ so long as the
|
||||
relative ordering of the migrations is preserved.
|
||||
|
||||
The migration files are permitted to be "empty", in the event that a migration
|
||||
is a no-op or is irreversible. It is recommended to still include both migration
|
||||
files by making the whole migration file consist of a comment.
|
||||
If your database does not support comments, then deleting the migration file will also work.
|
||||
Note, an actual empty file (e.g. a 0 byte file) may cause issues with your database since migrate
|
||||
will attempt to run an empty query. In this case, deleting the migration file will also work.
|
||||
For the rational of this behavior see:
|
||||
[#244 (comment)](https://github.com/golang-migrate/migrate/issues/244#issuecomment-510758270)
|
||||
|
||||
## Migration Content Format
|
||||
|
||||
The format of the migration files themselves varies between database systems.
|
||||
Different databases have different semantics around schema changes and when and
|
||||
how they are allowed to occur
|
||||
(for instance, [if schema changes can occur within a transaction](https://wiki.postgresql.org/wiki/Transactional_DDL_in_PostgreSQL:_A_Competitive_Analysis)).
|
||||
|
||||
As such, the `migrate` library has little to no checking around the format of
|
||||
migration sources. The migration files are generally processed directly by the
|
||||
drivers as raw operations.
|
||||
|
||||
## Reversibility of Migrations
|
||||
|
||||
Best practice for writing schema migration is that all migrations should be
|
||||
reversible. It should in theory be possible for run migrations down and back up
|
||||
through any and all versions with the state being fully cleaned and recreated
|
||||
by doing so.
|
||||
|
||||
By adhering to this recommended practice, development and deployment of new code
|
||||
is cleaner and easier (cleaning database state for a new feature should be as
|
||||
easy as migrating down to a prior version, and back up to the latest).
|
||||
|
||||
As opposed to some other migration libraries, `migrate` represents up and down
|
||||
migrations as separate files. This prevents any non-standard file syntax from
|
||||
being introduced which may result in unintended behavior or errors, depending
|
||||
on what database is processing the file.
|
||||
|
||||
While it is technically possible for an up or down migration to exist on its own
|
||||
without an equivalently versioned counterpart, it is strongly recommended to
|
||||
always include a down migration which cleans up the state of the corresponding
|
||||
up migration.
|
||||
120
vendor/github.com/golang-migrate/migrate/v4/Makefile
generated
vendored
Normal file
120
vendor/github.com/golang-migrate/migrate/v4/Makefile
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
SOURCE ?= file go_bindata github github_ee bitbucket aws_s3 google_cloud_storage godoc_vfs gitlab
|
||||
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb yugabytedb clickhouse mongodb sqlserver firebird neo4j pgx pgx5 rqlite
|
||||
DATABASE_TEST ?= $(DATABASE) sqlite sqlite3 sqlcipher
|
||||
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
|
||||
TEST_FLAGS ?=
|
||||
REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)")
|
||||
COVERAGE_DIR ?= .coverage
|
||||
|
||||
build:
|
||||
CGO_ENABLED=0 go build -ldflags='-X main.Version=$(VERSION)' -tags '$(DATABASE) $(SOURCE)' ./cmd/migrate
|
||||
|
||||
build-docker:
|
||||
CGO_ENABLED=0 go build -a -o build/migrate.linux-386 -ldflags="-s -w -X main.Version=${VERSION}" -tags "$(DATABASE) $(SOURCE)" ./cmd/migrate
|
||||
|
||||
build-cli: clean
|
||||
-mkdir ./cli/build
|
||||
cd ./cmd/migrate && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o ../../cli/build/migrate.linux-amd64 -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cmd/migrate && CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -a -o ../../cli/build/migrate.linux-armv7 -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cmd/migrate && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -o ../../cli/build/migrate.linux-arm64 -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cmd/migrate && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -o ../../cli/build/migrate.darwin-amd64 -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cmd/migrate && CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -a -o ../../cli/build/migrate.windows-386.exe -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cmd/migrate && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -a -o ../../cli/build/migrate.windows-amd64.exe -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cli/build && find . -name 'migrate*' | xargs -I{} tar czf {}.tar.gz {}
|
||||
cd ./cli/build && shasum -a 256 * > sha256sum.txt
|
||||
cat ./cli/build/sha256sum.txt
|
||||
|
||||
|
||||
clean:
|
||||
-rm -r ./cli/build
|
||||
|
||||
|
||||
test-short:
|
||||
make test-with-flags --ignore-errors TEST_FLAGS='-short'
|
||||
|
||||
|
||||
test:
|
||||
@-rm -r $(COVERAGE_DIR)
|
||||
@mkdir $(COVERAGE_DIR)
|
||||
make test-with-flags TEST_FLAGS='-v -race -covermode atomic -coverprofile $$(COVERAGE_DIR)/combined.txt -bench=. -benchmem -timeout 20m'
|
||||
|
||||
|
||||
test-with-flags:
|
||||
@echo SOURCE: $(SOURCE)
|
||||
@echo DATABASE_TEST: $(DATABASE_TEST)
|
||||
|
||||
@go test $(TEST_FLAGS) ./...
|
||||
|
||||
|
||||
kill-orphaned-docker-containers:
|
||||
docker rm -f $(shell docker ps -aq --filter label=migrate_test)
|
||||
|
||||
|
||||
html-coverage:
|
||||
go tool cover -html=$(COVERAGE_DIR)/combined.txt
|
||||
|
||||
|
||||
list-external-deps:
|
||||
$(call external_deps,'.')
|
||||
$(call external_deps,'./cli/...')
|
||||
$(call external_deps,'./testing/...')
|
||||
|
||||
$(foreach v, $(SOURCE), $(call external_deps,'./source/$(v)/...'))
|
||||
$(call external_deps,'./source/testing/...')
|
||||
$(call external_deps,'./source/stub/...')
|
||||
|
||||
$(foreach v, $(DATABASE), $(call external_deps,'./database/$(v)/...'))
|
||||
$(call external_deps,'./database/testing/...')
|
||||
$(call external_deps,'./database/stub/...')
|
||||
|
||||
|
||||
restore-import-paths:
|
||||
find . -name '*.go' -type f -execdir sed -i '' s%\"github.com/$(REPO_OWNER)/migrate%\"github.com/mattes/migrate%g '{}' \;
|
||||
|
||||
|
||||
rewrite-import-paths:
|
||||
find . -name '*.go' -type f -execdir sed -i '' s%\"github.com/mattes/migrate%\"github.com/$(REPO_OWNER)/migrate%g '{}' \;
|
||||
|
||||
|
||||
# example: fswatch -0 --exclude .godoc.pid --event Updated . | xargs -0 -n1 -I{} make docs
|
||||
docs:
|
||||
-make kill-docs
|
||||
nohup godoc -play -http=127.0.0.1:6064 </dev/null >/dev/null 2>&1 & echo $$! > .godoc.pid
|
||||
cat .godoc.pid
|
||||
|
||||
|
||||
kill-docs:
|
||||
@cat .godoc.pid
|
||||
kill -9 $$(cat .godoc.pid)
|
||||
rm .godoc.pid
|
||||
|
||||
|
||||
open-docs:
|
||||
open http://localhost:6064/pkg/github.com/$(REPO_OWNER)/migrate
|
||||
|
||||
|
||||
# example: make release V=0.0.0
|
||||
release:
|
||||
git tag v$(V)
|
||||
@read -p "Press enter to confirm and push to origin ..." && git push origin v$(V)
|
||||
|
||||
echo-source:
|
||||
@echo "$(SOURCE)"
|
||||
|
||||
echo-database:
|
||||
@echo "$(DATABASE)"
|
||||
|
||||
|
||||
define external_deps
|
||||
@echo '-- $(1)'; go list -f '{{join .Deps "\n"}}' $(1) | grep -v github.com/$(REPO_OWNER)/migrate | xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}'
|
||||
|
||||
endef
|
||||
|
||||
|
||||
.PHONY: build build-docker build-cli clean test-short test test-with-flags html-coverage \
|
||||
restore-import-paths rewrite-import-paths list-external-deps release \
|
||||
docs kill-docs open-docs kill-orphaned-docker-containers echo-source echo-database
|
||||
|
||||
SHELL = /bin/sh
|
||||
RAND = $(shell echo $$RANDOM)
|
||||
|
||||
196
vendor/github.com/golang-migrate/migrate/v4/README.md
generated
vendored
Normal file
196
vendor/github.com/golang-migrate/migrate/v4/README.md
generated
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
[](https://github.com/golang-migrate/migrate/actions/workflows/ci.yaml?query=branch%3Amaster)
|
||||
[](https://pkg.go.dev/github.com/golang-migrate/migrate/v4)
|
||||
[](https://coveralls.io/github/golang-migrate/migrate?branch=master)
|
||||
[](https://packagecloud.io/golang-migrate/migrate?filter=debs)
|
||||
[](https://hub.docker.com/r/migrate/migrate/)
|
||||

|
||||
[](https://github.com/golang-migrate/migrate/releases)
|
||||
[](https://goreportcard.com/report/github.com/golang-migrate/migrate/v4)
|
||||
|
||||
# migrate
|
||||
|
||||
__Database migrations written in Go. Use as [CLI](#cli-usage) or import as [library](#use-in-your-go-project).__
|
||||
|
||||
* Migrate reads migrations from [sources](#migration-sources)
|
||||
and applies them in correct order to a [database](#databases).
|
||||
* Drivers are "dumb", migrate glues everything together and makes sure the logic is bulletproof.
|
||||
(Keeps the drivers lightweight, too.)
|
||||
* Database drivers don't assume things or try to correct user input. When in doubt, fail.
|
||||
|
||||
Forked from [mattes/migrate](https://github.com/mattes/migrate)
|
||||
|
||||
## Databases
|
||||
|
||||
Database drivers run migrations. [Add a new database?](database/driver.go)
|
||||
|
||||
* [PostgreSQL](database/postgres)
|
||||
* [PGX v4](database/pgx)
|
||||
* [PGX v5](database/pgx/v5)
|
||||
* [Redshift](database/redshift)
|
||||
* [Ql](database/ql)
|
||||
* [Cassandra / ScyllaDB](database/cassandra)
|
||||
* [SQLite](database/sqlite)
|
||||
* [SQLite3](database/sqlite3) ([todo #165](https://github.com/mattes/migrate/issues/165))
|
||||
* [SQLCipher](database/sqlcipher)
|
||||
* [MySQL / MariaDB](database/mysql)
|
||||
* [Neo4j](database/neo4j)
|
||||
* [MongoDB](database/mongodb)
|
||||
* [CrateDB](database/crate) ([todo #170](https://github.com/mattes/migrate/issues/170))
|
||||
* [Shell](database/shell) ([todo #171](https://github.com/mattes/migrate/issues/171))
|
||||
* [Google Cloud Spanner](database/spanner)
|
||||
* [CockroachDB](database/cockroachdb)
|
||||
* [YugabyteDB](database/yugabytedb)
|
||||
* [ClickHouse](database/clickhouse)
|
||||
* [Firebird](database/firebird)
|
||||
* [MS SQL Server](database/sqlserver)
|
||||
* [RQLite](database/rqlite)
|
||||
|
||||
### Database URLs
|
||||
|
||||
Database connection strings are specified via URLs. The URL format is driver dependent but generally has the form: `dbdriver://username:password@host:port/dbname?param1=true¶m2=false`
|
||||
|
||||
Any [reserved URL characters](https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters) need to be escaped. Note, the `%` character also [needs to be escaped](https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_the_percent_character)
|
||||
|
||||
Explicitly, the following characters need to be escaped:
|
||||
`!`, `#`, `$`, `%`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `/`, `:`, `;`, `=`, `?`, `@`, `[`, `]`
|
||||
|
||||
It's easiest to always run the URL parts of your DB connection URL (e.g. username, password, etc) through an URL encoder. See the example Python snippets below:
|
||||
|
||||
```bash
|
||||
$ python3 -c 'import urllib.parse; print(urllib.parse.quote(input("String to encode: "), ""))'
|
||||
String to encode: FAKEpassword!#$%&'()*+,/:;=?@[]
|
||||
FAKEpassword%21%23%24%25%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D
|
||||
$ python2 -c 'import urllib; print urllib.quote(raw_input("String to encode: "), "")'
|
||||
String to encode: FAKEpassword!#$%&'()*+,/:;=?@[]
|
||||
FAKEpassword%21%23%24%25%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D
|
||||
$
|
||||
```
|
||||
|
||||
## Migration Sources
|
||||
|
||||
Source drivers read migrations from local or remote sources. [Add a new source?](source/driver.go)
|
||||
|
||||
* [Filesystem](source/file) - read from filesystem
|
||||
* [io/fs](source/iofs) - read from a Go [io/fs](https://pkg.go.dev/io/fs#FS)
|
||||
* [Go-Bindata](source/go_bindata) - read from embedded binary data ([jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata))
|
||||
* [pkger](source/pkger) - read from embedded binary data ([markbates/pkger](https://github.com/markbates/pkger))
|
||||
* [GitHub](source/github) - read from remote GitHub repositories
|
||||
* [GitHub Enterprise](source/github_ee) - read from remote GitHub Enterprise repositories
|
||||
* [Bitbucket](source/bitbucket) - read from remote Bitbucket repositories
|
||||
* [Gitlab](source/gitlab) - read from remote Gitlab repositories
|
||||
* [AWS S3](source/aws_s3) - read from Amazon Web Services S3
|
||||
* [Google Cloud Storage](source/google_cloud_storage) - read from Google Cloud Platform Storage
|
||||
|
||||
## CLI usage
|
||||
|
||||
* Simple wrapper around this library.
|
||||
* Handles ctrl+c (SIGINT) gracefully.
|
||||
* No config search paths, no config files, no magic ENV var injections.
|
||||
|
||||
__[CLI Documentation](cmd/migrate)__
|
||||
|
||||
### Basic usage
|
||||
|
||||
```bash
|
||||
$ migrate -source file://path/to/migrations -database postgres://localhost:5432/database up 2
|
||||
```
|
||||
|
||||
### Docker usage
|
||||
|
||||
```bash
|
||||
$ docker run -v {{ migration dir }}:/migrations --network host migrate/migrate
|
||||
-path=/migrations/ -database postgres://localhost:5432/database up 2
|
||||
```
|
||||
|
||||
## Use in your Go project
|
||||
|
||||
* API is stable and frozen for this release (v3 & v4).
|
||||
* Uses [Go modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) to manage dependencies.
|
||||
* To help prevent database corruptions, it supports graceful stops via `GracefulStop chan bool`.
|
||||
* Bring your own logger.
|
||||
* Uses `io.Reader` streams internally for low memory overhead.
|
||||
* Thread-safe and no goroutine leaks.
|
||||
|
||||
__[Go Documentation](https://pkg.go.dev/github.com/golang-migrate/migrate/v4)__
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/github"
|
||||
)
|
||||
|
||||
func main() {
|
||||
m, err := migrate.New(
|
||||
"github://mattes:personal-access-token@mattes/migrate_test",
|
||||
"postgres://localhost:5432/database?sslmode=enable")
|
||||
m.Steps(2)
|
||||
}
|
||||
```
|
||||
|
||||
Want to use an existing database client?
|
||||
|
||||
```go
|
||||
import (
|
||||
"database/sql"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("postgres", "postgres://localhost:5432/database?sslmode=enable")
|
||||
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file:///migrations",
|
||||
"postgres", driver)
|
||||
m.Up() // or m.Step(2) if you want to explicitly set the number of migrations to run
|
||||
}
|
||||
```
|
||||
|
||||
## Getting started
|
||||
|
||||
Go to [getting started](GETTING_STARTED.md)
|
||||
|
||||
## Tutorials
|
||||
|
||||
* [CockroachDB](database/cockroachdb/TUTORIAL.md)
|
||||
* [PostgreSQL](database/postgres/TUTORIAL.md)
|
||||
|
||||
(more tutorials to come)
|
||||
|
||||
## Migration files
|
||||
|
||||
Each migration has an up and down migration. [Why?](FAQ.md#why-two-separate-files-up-and-down-for-a-migration)
|
||||
|
||||
```bash
|
||||
1481574547_create_users_table.up.sql
|
||||
1481574547_create_users_table.down.sql
|
||||
```
|
||||
|
||||
[Best practices: How to write migrations.](MIGRATIONS.md)
|
||||
|
||||
## Coming from another db migration tool?
|
||||
|
||||
Check out [migradaptor](https://github.com/musinit/migradaptor/).
|
||||
*Note: migradaptor is not affliated or supported by this project*
|
||||
|
||||
## Versions
|
||||
|
||||
Version | Supported? | Import | Notes
|
||||
--------|------------|--------|------
|
||||
**master** | :white_check_mark: | `import "github.com/golang-migrate/migrate/v4"` | New features and bug fixes arrive here first |
|
||||
**v4** | :white_check_mark: | `import "github.com/golang-migrate/migrate/v4"` | Used for stable releases |
|
||||
**v3** | :x: | `import "github.com/golang-migrate/migrate"` (with package manager) or `import "gopkg.in/golang-migrate/migrate.v3"` (not recommended) | **DO NOT USE** - No longer supported |
|
||||
|
||||
## Development and Contributing
|
||||
|
||||
Yes, please! [`Makefile`](Makefile) is your friend,
|
||||
read the [development guide](CONTRIBUTING.md).
|
||||
|
||||
Also have a look at the [FAQ](FAQ.md).
|
||||
|
||||
---
|
||||
|
||||
Looking for alternatives? [https://awesome-go.com/#database](https://awesome-go.com/#database).
|
||||
16
vendor/github.com/golang-migrate/migrate/v4/SECURITY.md
generated
vendored
Normal file
16
vendor/github.com/golang-migrate/migrate/v4/SECURITY.md
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| master | :white_check_mark: |
|
||||
| 4.x | :white_check_mark: |
|
||||
| 3.x | :x: |
|
||||
| < 3.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We prefer [coordinated disclosures](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure). To start one, create a GitHub security advisory following [these instructions](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)
|
||||
|
||||
Please suggest potential impact and urgency in your reports.
|
||||
123
vendor/github.com/golang-migrate/migrate/v4/database/driver.go
generated
vendored
Normal file
123
vendor/github.com/golang-migrate/migrate/v4/database/driver.go
generated
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
// Package database provides the Driver interface.
|
||||
// All database drivers must implement this interface, register themselves,
|
||||
// optionally provide a `WithInstance` function and pass the tests
|
||||
// in package database/testing.
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
iurl "github.com/golang-migrate/migrate/v4/internal/url"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLocked = fmt.Errorf("can't acquire lock")
|
||||
ErrNotLocked = fmt.Errorf("can't unlock, as not currently locked")
|
||||
)
|
||||
|
||||
const NilVersion int = -1
|
||||
|
||||
var driversMu sync.RWMutex
|
||||
var drivers = make(map[string]Driver)
|
||||
|
||||
// Driver is the interface every database driver must implement.
|
||||
//
|
||||
// How to implement a database driver?
|
||||
// 1. Implement this interface.
|
||||
// 2. Optionally, add a function named `WithInstance`.
|
||||
// This function should accept an existing DB instance and a Config{} struct
|
||||
// and return a driver instance.
|
||||
// 3. Add a test that calls database/testing.go:Test()
|
||||
// 4. Add own tests for Open(), WithInstance() (when provided) and Close().
|
||||
// All other functions are tested by tests in database/testing.
|
||||
// Saves you some time and makes sure all database drivers behave the same way.
|
||||
// 5. Call Register in init().
|
||||
// 6. Create a internal/cli/build_<driver-name>.go file
|
||||
// 7. Add driver name in 'DATABASE' variable in Makefile
|
||||
//
|
||||
// Guidelines:
|
||||
// - Don't try to correct user input. Don't assume things.
|
||||
// When in doubt, return an error and explain the situation to the user.
|
||||
// - All configuration input must come from the URL string in func Open()
|
||||
// or the Config{} struct in WithInstance. Don't os.Getenv().
|
||||
type Driver interface {
|
||||
// Open returns a new driver instance configured with parameters
|
||||
// coming from the URL string. Migrate will call this function
|
||||
// only once per instance.
|
||||
Open(url string) (Driver, error)
|
||||
|
||||
// Close closes the underlying database instance managed by the driver.
|
||||
// Migrate will call this function only once per instance.
|
||||
Close() error
|
||||
|
||||
// Lock should acquire a database lock so that only one migration process
|
||||
// can run at a time. Migrate will call this function before Run is called.
|
||||
// If the implementation can't provide this functionality, return nil.
|
||||
// Return database.ErrLocked if database is already locked.
|
||||
Lock() error
|
||||
|
||||
// Unlock should release the lock. Migrate will call this function after
|
||||
// all migrations have been run.
|
||||
Unlock() error
|
||||
|
||||
// Run applies a migration to the database. migration is guaranteed to be not nil.
|
||||
Run(migration io.Reader) error
|
||||
|
||||
// SetVersion saves version and dirty state.
|
||||
// Migrate will call this function before and after each call to Run.
|
||||
// version must be >= -1. -1 means NilVersion.
|
||||
SetVersion(version int, dirty bool) error
|
||||
|
||||
// Version returns the currently active version and if the database is dirty.
|
||||
// When no migration has been applied, it must return version -1.
|
||||
// Dirty means, a previous migration failed and user interaction is required.
|
||||
Version() (version int, dirty bool, err error)
|
||||
|
||||
// Drop deletes everything in the database.
|
||||
// Note that this is a breaking action, a new call to Open() is necessary to
|
||||
// ensure subsequent calls work as expected.
|
||||
Drop() error
|
||||
}
|
||||
|
||||
// Open returns a new driver instance.
|
||||
func Open(url string) (Driver, error) {
|
||||
scheme, err := iurl.SchemeFromURL(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driversMu.RLock()
|
||||
d, ok := drivers[scheme]
|
||||
driversMu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("database driver: unknown driver %v (forgotten import?)", scheme)
|
||||
}
|
||||
|
||||
return d.Open(url)
|
||||
}
|
||||
|
||||
// Register globally registers a driver.
|
||||
func Register(name string, driver Driver) {
|
||||
driversMu.Lock()
|
||||
defer driversMu.Unlock()
|
||||
if driver == nil {
|
||||
panic("Register driver is nil")
|
||||
}
|
||||
if _, dup := drivers[name]; dup {
|
||||
panic("Register called twice for driver " + name)
|
||||
}
|
||||
drivers[name] = driver
|
||||
}
|
||||
|
||||
// List lists the registered drivers
|
||||
func List() []string {
|
||||
driversMu.RLock()
|
||||
defer driversMu.RUnlock()
|
||||
names := make([]string, 0, len(drivers))
|
||||
for n := range drivers {
|
||||
names = append(names, n)
|
||||
}
|
||||
return names
|
||||
}
|
||||
27
vendor/github.com/golang-migrate/migrate/v4/database/error.go
generated
vendored
Normal file
27
vendor/github.com/golang-migrate/migrate/v4/database/error.go
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Error should be used for errors involving queries ran against the database
|
||||
type Error struct {
|
||||
// Optional: the line number
|
||||
Line uint
|
||||
|
||||
// Query is a query excerpt
|
||||
Query []byte
|
||||
|
||||
// Err is a useful/helping error message for humans
|
||||
Err string
|
||||
|
||||
// OrigErr is the underlying error
|
||||
OrigErr error
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
if len(e.Err) == 0 {
|
||||
return fmt.Sprintf("%v in line %v: %s", e.OrigErr, e.Line, e.Query)
|
||||
}
|
||||
return fmt.Sprintf("%v in line %v: %s (details: %v)", e.Err, e.Line, e.Query, e.OrigErr)
|
||||
}
|
||||
46
vendor/github.com/golang-migrate/migrate/v4/database/multistmt/parse.go
generated
vendored
Normal file
46
vendor/github.com/golang-migrate/migrate/v4/database/multistmt/parse.go
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
// Package multistmt provides methods for parsing multi-statement database migrations
|
||||
package multistmt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// StartBufSize is the default starting size of the buffer used to scan and parse multi-statement migrations
|
||||
var StartBufSize = 4096
|
||||
|
||||
// Handler handles a single migration parsed from a multi-statement migration.
|
||||
// It's given the single migration to handle and returns whether or not further statements
|
||||
// from the multi-statement migration should be parsed and handled.
|
||||
type Handler func(migration []byte) bool
|
||||
|
||||
func splitWithDelimiter(delimiter []byte) func(d []byte, atEOF bool) (int, []byte, error) {
|
||||
return func(d []byte, atEOF bool) (int, []byte, error) {
|
||||
// SplitFunc inspired by bufio.ScanLines() implementation
|
||||
if atEOF {
|
||||
if len(d) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
return len(d), d, nil
|
||||
}
|
||||
if i := bytes.Index(d, delimiter); i >= 0 {
|
||||
return i + len(delimiter), d[:i+len(delimiter)], nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the given multi-statement migration
|
||||
func Parse(reader io.Reader, delimiter []byte, maxMigrationSize int, h Handler) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(make([]byte, 0, StartBufSize), maxMigrationSize)
|
||||
scanner.Split(splitWithDelimiter(delimiter))
|
||||
for scanner.Scan() {
|
||||
cont := h(scanner.Bytes())
|
||||
if !cont {
|
||||
break
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
39
vendor/github.com/golang-migrate/migrate/v4/database/postgres/README.md
generated
vendored
Normal file
39
vendor/github.com/golang-migrate/migrate/v4/database/postgres/README.md
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# postgres
|
||||
|
||||
`postgres://user:password@host:port/dbname?query` (`postgresql://` works, too)
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
|
||||
| `x-migrations-table-quoted` | `MigrationsTableQuoted` | By default, migrate quotes the migration table for SQL injection safety reasons. This option disable quoting and naively checks that you have quoted the migration table name. e.g. `"my_schema"."schema_migrations"` |
|
||||
| `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds |
|
||||
| `x-multi-statement` | `MultiStatementEnabled` | Enable multi-statement execution (default: false) |
|
||||
| `x-multi-statement-max-size` | `MultiStatementMaxSize` | Maximum size of single statement in bytes (default: 10MB) |
|
||||
| `dbname` | `DatabaseName` | The name of the database to connect to |
|
||||
| `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. |
|
||||
| `user` | | The user to sign in as |
|
||||
| `password` | | The user's password |
|
||||
| `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) |
|
||||
| `port` | | The port to bind to. (default is 5432) |
|
||||
| `fallback_application_name` | | An application_name to fall back to if one isn't provided. |
|
||||
| `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. |
|
||||
| `sslcert` | | Cert file location. The file must contain PEM encoded data. |
|
||||
| `sslkey` | | Key file location. The file must contain PEM encoded data. |
|
||||
| `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. |
|
||||
| `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) |
|
||||
|
||||
|
||||
## Upgrading from v1
|
||||
|
||||
1. Write down the current migration version from schema_migrations
|
||||
1. `DROP TABLE schema_migrations`
|
||||
2. Wrap your existing migrations in transactions ([BEGIN/COMMIT](https://www.postgresql.org/docs/current/static/transaction-iso.html)) if you use multiple statements within one migration.
|
||||
3. Download and install the latest migrate version.
|
||||
4. Force the current migration version with `migrate force <current_version>`.
|
||||
|
||||
## Multi-statement mode
|
||||
|
||||
In PostgreSQL running multiple SQL statements in one `Exec` executes them inside a transaction. Sometimes this
|
||||
behavior is not desirable because some statements can be only run outside of transaction (e.g.
|
||||
`CREATE INDEX CONCURRENTLY`). If you want to use `CREATE INDEX CONCURRENTLY` without activating multi-statement mode
|
||||
you have to put such statements in a separate migration files.
|
||||
167
vendor/github.com/golang-migrate/migrate/v4/database/postgres/TUTORIAL.md
generated
vendored
Normal file
167
vendor/github.com/golang-migrate/migrate/v4/database/postgres/TUTORIAL.md
generated
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
# PostgreSQL tutorial for beginners
|
||||
|
||||
## Create/configure database
|
||||
|
||||
For the purpose of this tutorial let's create PostgreSQL database called `example`.
|
||||
Our user here is `postgres`, password `password`, and host is `localhost`.
|
||||
```
|
||||
psql -h localhost -U postgres -w -c "create database example;"
|
||||
```
|
||||
When using Migrate CLI we need to pass to database URL. Let's export it to a variable for convenience:
|
||||
```
|
||||
export POSTGRESQL_URL='postgres://postgres:password@localhost:5432/example?sslmode=disable'
|
||||
```
|
||||
`sslmode=disable` means that the connection with our database will not be encrypted. Enabling it is left as an exercise.
|
||||
|
||||
You can find further description of database URLs [here](README.md#database-urls).
|
||||
|
||||
## Create migrations
|
||||
Let's create table called `users`:
|
||||
```
|
||||
migrate create -ext sql -dir db/migrations -seq create_users_table
|
||||
```
|
||||
If there were no errors, we should have two files available under `db/migrations` folder:
|
||||
- 000001_create_users_table.down.sql
|
||||
- 000001_create_users_table.up.sql
|
||||
|
||||
Note the `sql` extension that we provided.
|
||||
|
||||
In the `.up.sql` file let's create the table:
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS users(
|
||||
user_id serial PRIMARY KEY,
|
||||
username VARCHAR (50) UNIQUE NOT NULL,
|
||||
password VARCHAR (50) NOT NULL,
|
||||
email VARCHAR (300) UNIQUE NOT NULL
|
||||
);
|
||||
```
|
||||
And in the `.down.sql` let's delete it:
|
||||
```sql
|
||||
DROP TABLE IF EXISTS users;
|
||||
```
|
||||
By adding `IF EXISTS/IF NOT EXISTS` we are making migrations idempotent - you can read more about idempotency in [getting started](../../GETTING_STARTED.md#create-migrations)
|
||||
|
||||
## Run migrations
|
||||
```
|
||||
migrate -database ${POSTGRESQL_URL} -path db/migrations up
|
||||
```
|
||||
Let's check if the table was created properly by running `psql example -c "\d users"`.
|
||||
The output you are supposed to see:
|
||||
```
|
||||
Table "public.users"
|
||||
Column | Type | Modifiers
|
||||
----------+------------------------+---------------------------------------------------------
|
||||
user_id | integer | not null default nextval('users_user_id_seq'::regclass)
|
||||
username | character varying(50) | not null
|
||||
password | character varying(50) | not null
|
||||
email | character varying(300) | not null
|
||||
Indexes:
|
||||
"users_pkey" PRIMARY KEY, btree (user_id)
|
||||
"users_email_key" UNIQUE CONSTRAINT, btree (email)
|
||||
"users_username_key" UNIQUE CONSTRAINT, btree (username)
|
||||
```
|
||||
Great! Now let's check if running reverse migration also works:
|
||||
```
|
||||
migrate -database ${POSTGRESQL_URL} -path db/migrations down
|
||||
```
|
||||
Make sure to check if your database changed as expected in this case as well.
|
||||
|
||||
## Database transactions
|
||||
|
||||
To show database transactions usage, let's create another set of migrations by running:
|
||||
```
|
||||
migrate create -ext sql -dir db/migrations -seq add_mood_to_users
|
||||
```
|
||||
Again, it should create for us two migrations files:
|
||||
- 000002_add_mood_to_users.down.sql
|
||||
- 000002_add_mood_to_users.up.sql
|
||||
|
||||
In Postgres, when we want our queries to be done in a transaction, we need to wrap it with `BEGIN` and `COMMIT` commands.
|
||||
In our example, we are going to add a column to our database that can only accept enumerable values or NULL.
|
||||
Migration up:
|
||||
```sql
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE enum_mood AS ENUM (
|
||||
'happy',
|
||||
'sad',
|
||||
'neutral'
|
||||
);
|
||||
ALTER TABLE users ADD COLUMN mood enum_mood;
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
Migration down:
|
||||
```sql
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE users DROP COLUMN mood;
|
||||
DROP TYPE enum_mood;
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
Now we can run our new migration and check the database:
|
||||
```
|
||||
migrate -database ${POSTGRESQL_URL} -path db/migrations up
|
||||
psql example -c "\d users"
|
||||
```
|
||||
Expected output:
|
||||
```
|
||||
Table "public.users"
|
||||
Column | Type | Modifiers
|
||||
----------+------------------------+---------------------------------------------------------
|
||||
user_id | integer | not null default nextval('users_user_id_seq'::regclass)
|
||||
username | character varying(50) | not null
|
||||
password | character varying(50) | not null
|
||||
email | character varying(300) | not null
|
||||
mood | enum_mood |
|
||||
Indexes:
|
||||
"users_pkey" PRIMARY KEY, btree (user_id)
|
||||
"users_email_key" UNIQUE CONSTRAINT, btree (email)
|
||||
"users_username_key" UNIQUE CONSTRAINT, btree (username)
|
||||
```
|
||||
|
||||
## Optional: Run migrations within your Go app
|
||||
Here is a very simple app running migrations for the above configuration:
|
||||
```go
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
func main() {
|
||||
m, err := migrate.New(
|
||||
"file://db/migrations",
|
||||
"postgres://postgres:postgres@localhost:5432/example?sslmode=disable")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := m.Up(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
You can find details [here](README.md#use-in-your-go-project)
|
||||
|
||||
## Fix issue where migrations run twice
|
||||
|
||||
When the schema and role names are the same, you might run into issues if you create this schema using migrations.
|
||||
This is caused by the fact that the [default `search_path`](https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH) is `"$user", public`.
|
||||
In the first run (with an empty database) the migrate table is created in `public`.
|
||||
When the migrations create the `$user` schema, the next run will store (a new) migrate table in this schema (due to order of schemas in `search_path`) and tries to apply all migrations again (most likely failing).
|
||||
|
||||
To solve this you need to change the default `search_path` by removing the `$user` component, so the migrate table is always stored in the (available) `public` schema.
|
||||
This can be done using the [`search_path` query parameter in the URL](https://github.com/jexia/migrate/blob/fix-postgres-version-table/database/postgres/README.md#postgres).
|
||||
|
||||
For example to force the migrations table in the public schema you can use:
|
||||
```
|
||||
export POSTGRESQL_URL='postgres://postgres:password@localhost:5432/example?sslmode=disable&search_path=public'
|
||||
```
|
||||
|
||||
Note that you need to explicitly add the schema names to the table names in your migrations when you to modify the tables of the non-public schema.
|
||||
|
||||
Alternatively you can add the non-public schema manually (before applying the migrations) if that is possible in your case and let the tool store the migrations table in this schema as well.
|
||||
493
vendor/github.com/golang-migrate/migrate/v4/database/postgres/postgres.go
generated
vendored
Normal file
493
vendor/github.com/golang-migrate/migrate/v4/database/postgres/postgres.go
generated
vendored
Normal file
@@ -0,0 +1,493 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/golang-migrate/migrate/v4/database/multistmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db := Postgres{}
|
||||
database.Register("postgres", &db)
|
||||
database.Register("postgresql", &db)
|
||||
}
|
||||
|
||||
var (
|
||||
multiStmtDelimiter = []byte(";")
|
||||
|
||||
DefaultMigrationsTable = "schema_migrations"
|
||||
DefaultMultiStatementMaxSize = 10 * 1 << 20 // 10 MB
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNilConfig = fmt.Errorf("no config")
|
||||
ErrNoDatabaseName = fmt.Errorf("no database name")
|
||||
ErrNoSchema = fmt.Errorf("no schema")
|
||||
ErrDatabaseDirty = fmt.Errorf("database is dirty")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MigrationsTable string
|
||||
MigrationsTableQuoted bool
|
||||
MultiStatementEnabled bool
|
||||
DatabaseName string
|
||||
SchemaName string
|
||||
migrationsSchemaName string
|
||||
migrationsTableName string
|
||||
StatementTimeout time.Duration
|
||||
MultiStatementMaxSize int
|
||||
}
|
||||
|
||||
type Postgres struct {
|
||||
// Locking and unlocking need to use the same connection
|
||||
conn *sql.Conn
|
||||
db *sql.DB
|
||||
isLocked atomic.Bool
|
||||
|
||||
// Open and WithInstance need to guarantee that config is never nil
|
||||
config *Config
|
||||
}
|
||||
|
||||
func WithConnection(ctx context.Context, conn *sql.Conn, config *Config) (*Postgres, error) {
|
||||
if config == nil {
|
||||
return nil, ErrNilConfig
|
||||
}
|
||||
|
||||
if err := conn.PingContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.DatabaseName == "" {
|
||||
query := `SELECT CURRENT_DATABASE()`
|
||||
var databaseName string
|
||||
if err := conn.QueryRowContext(ctx, query).Scan(&databaseName); err != nil {
|
||||
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if len(databaseName) == 0 {
|
||||
return nil, ErrNoDatabaseName
|
||||
}
|
||||
|
||||
config.DatabaseName = databaseName
|
||||
}
|
||||
|
||||
if config.SchemaName == "" {
|
||||
query := `SELECT CURRENT_SCHEMA()`
|
||||
var schemaName sql.NullString
|
||||
if err := conn.QueryRowContext(ctx, query).Scan(&schemaName); err != nil {
|
||||
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if !schemaName.Valid {
|
||||
return nil, ErrNoSchema
|
||||
}
|
||||
|
||||
config.SchemaName = schemaName.String
|
||||
}
|
||||
|
||||
if len(config.MigrationsTable) == 0 {
|
||||
config.MigrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
config.migrationsSchemaName = config.SchemaName
|
||||
config.migrationsTableName = config.MigrationsTable
|
||||
if config.MigrationsTableQuoted {
|
||||
re := regexp.MustCompile(`"(.*?)"`)
|
||||
result := re.FindAllStringSubmatch(config.MigrationsTable, -1)
|
||||
config.migrationsTableName = result[len(result)-1][1]
|
||||
if len(result) == 2 {
|
||||
config.migrationsSchemaName = result[0][1]
|
||||
} else if len(result) > 2 {
|
||||
return nil, fmt.Errorf("\"%s\" MigrationsTable contains too many dot characters", config.MigrationsTable)
|
||||
}
|
||||
}
|
||||
|
||||
px := &Postgres{
|
||||
conn: conn,
|
||||
config: config,
|
||||
}
|
||||
|
||||
if err := px.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := instance.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := instance.Conn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
px, err := WithConnection(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
px.db = instance
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
purl, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", migrate.FilterCustomQuery(purl).String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
migrationsTable := purl.Query().Get("x-migrations-table")
|
||||
migrationsTableQuoted := false
|
||||
if s := purl.Query().Get("x-migrations-table-quoted"); len(s) > 0 {
|
||||
migrationsTableQuoted, err = strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse option x-migrations-table-quoted: %w", err)
|
||||
}
|
||||
}
|
||||
if (len(migrationsTable) > 0) && (migrationsTableQuoted) && ((migrationsTable[0] != '"') || (migrationsTable[len(migrationsTable)-1] != '"')) {
|
||||
return nil, fmt.Errorf("x-migrations-table must be quoted (for instance '\"migrate\".\"schema_migrations\"') when x-migrations-table-quoted is enabled, current value is: %s", migrationsTable)
|
||||
}
|
||||
|
||||
statementTimeoutString := purl.Query().Get("x-statement-timeout")
|
||||
statementTimeout := 0
|
||||
if statementTimeoutString != "" {
|
||||
statementTimeout, err = strconv.Atoi(statementTimeoutString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
multiStatementMaxSize := DefaultMultiStatementMaxSize
|
||||
if s := purl.Query().Get("x-multi-statement-max-size"); len(s) > 0 {
|
||||
multiStatementMaxSize, err = strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if multiStatementMaxSize <= 0 {
|
||||
multiStatementMaxSize = DefaultMultiStatementMaxSize
|
||||
}
|
||||
}
|
||||
|
||||
multiStatementEnabled := false
|
||||
if s := purl.Query().Get("x-multi-statement"); len(s) > 0 {
|
||||
multiStatementEnabled, err = strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse option x-multi-statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
px, err := WithInstance(db, &Config{
|
||||
DatabaseName: purl.Path,
|
||||
MigrationsTable: migrationsTable,
|
||||
MigrationsTableQuoted: migrationsTableQuoted,
|
||||
StatementTimeout: time.Duration(statementTimeout) * time.Millisecond,
|
||||
MultiStatementEnabled: multiStatementEnabled,
|
||||
MultiStatementMaxSize: multiStatementMaxSize,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Close() error {
|
||||
connErr := p.conn.Close()
|
||||
var dbErr error
|
||||
if p.db != nil {
|
||||
dbErr = p.db.Close()
|
||||
}
|
||||
|
||||
if connErr != nil || dbErr != nil {
|
||||
return fmt.Errorf("conn: %v, db: %v", connErr, dbErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS
|
||||
func (p *Postgres) Lock() error {
|
||||
return database.CasRestoreOnErr(&p.isLocked, false, true, database.ErrLocked, func() error {
|
||||
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.migrationsSchemaName, p.config.migrationsTableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This will wait indefinitely until the lock can be acquired.
|
||||
query := `SELECT pg_advisory_lock($1)`
|
||||
if _, err := p.conn.ExecContext(context.Background(), query, aid); err != nil {
|
||||
return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Postgres) Unlock() error {
|
||||
return database.CasRestoreOnErr(&p.isLocked, true, false, database.ErrNotLocked, func() error {
|
||||
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.migrationsSchemaName, p.config.migrationsTableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := `SELECT pg_advisory_unlock($1)`
|
||||
if _, err := p.conn.ExecContext(context.Background(), query, aid); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Postgres) Run(migration io.Reader) error {
|
||||
if p.config.MultiStatementEnabled {
|
||||
var err error
|
||||
if e := multistmt.Parse(migration, multiStmtDelimiter, p.config.MultiStatementMaxSize, func(m []byte) bool {
|
||||
if err = p.runStatement(m); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}); e != nil {
|
||||
return e
|
||||
}
|
||||
return err
|
||||
}
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.runStatement(migr)
|
||||
}
|
||||
|
||||
func (p *Postgres) runStatement(statement []byte) error {
|
||||
ctx := context.Background()
|
||||
if p.config.StatementTimeout != 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, p.config.StatementTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
query := string(statement)
|
||||
if strings.TrimSpace(query) == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := p.conn.ExecContext(ctx, query); err != nil {
|
||||
if pgErr, ok := err.(*pq.Error); ok {
|
||||
var line uint
|
||||
var col uint
|
||||
var lineColOK bool
|
||||
if pgErr.Position != "" {
|
||||
if pos, err := strconv.ParseUint(pgErr.Position, 10, 64); err == nil {
|
||||
line, col, lineColOK = computeLineFromPos(query, int(pos))
|
||||
}
|
||||
}
|
||||
message := fmt.Sprintf("migration failed: %s", pgErr.Message)
|
||||
if lineColOK {
|
||||
message = fmt.Sprintf("%s (column %d)", message, col)
|
||||
}
|
||||
if pgErr.Detail != "" {
|
||||
message = fmt.Sprintf("%s, %s", message, pgErr.Detail)
|
||||
}
|
||||
return database.Error{OrigErr: err, Err: message, Query: statement, Line: line}
|
||||
}
|
||||
return database.Error{OrigErr: err, Err: "migration failed", Query: statement}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
|
||||
// replace crlf with lf
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
// pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes
|
||||
runes := []rune(s)
|
||||
if pos > len(runes) {
|
||||
return 0, 0, false
|
||||
}
|
||||
sel := runes[:pos]
|
||||
line = uint(runesCount(sel, newLine) + 1)
|
||||
col = uint(pos - 1 - runesLastIndex(sel, newLine))
|
||||
return line, col, true
|
||||
}
|
||||
|
||||
const newLine = '\n'
|
||||
|
||||
func runesCount(input []rune, target rune) int {
|
||||
var count int
|
||||
for _, r := range input {
|
||||
if r == target {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func runesLastIndex(input []rune, target rune) int {
|
||||
for i := len(input) - 1; i >= 0; i-- {
|
||||
if input[i] == target {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (p *Postgres) SetVersion(version int, dirty bool) error {
|
||||
tx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{})
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Err: "transaction start failed"}
|
||||
}
|
||||
|
||||
query := `TRUNCATE ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + pq.QuoteIdentifier(p.config.migrationsTableName)
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
// Also re-write the schema version for nil dirty versions to prevent
|
||||
// empty schema version for failed down migration on the first migration
|
||||
// See: https://github.com/golang-migrate/migrate/issues/330
|
||||
if version >= 0 || (version == database.NilVersion && dirty) {
|
||||
query = `INSERT INTO ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + pq.QuoteIdentifier(p.config.migrationsTableName) + ` (version, dirty) VALUES ($1, $2)`
|
||||
if _, err := tx.Exec(query, version, dirty); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return &database.Error{OrigErr: err, Err: "transaction commit failed"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Version() (version int, dirty bool, err error) {
|
||||
query := `SELECT version, dirty FROM ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + pq.QuoteIdentifier(p.config.migrationsTableName) + ` LIMIT 1`
|
||||
err = p.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return database.NilVersion, false, nil
|
||||
|
||||
case err != nil:
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
if e.Code.Name() == "undefined_table" {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
}
|
||||
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
|
||||
default:
|
||||
return version, dirty, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Postgres) Drop() (err error) {
|
||||
// select all tables in current schema
|
||||
query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_type='BASE TABLE'`
|
||||
tables, err := p.conn.QueryContext(context.Background(), query)
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
// delete one table after another
|
||||
tableNames := make([]string, 0)
|
||||
for tables.Next() {
|
||||
var tableName string
|
||||
if err := tables.Scan(&tableName); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tableName) > 0 {
|
||||
tableNames = append(tableNames, tableName)
|
||||
}
|
||||
}
|
||||
if err := tables.Err(); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if len(tableNames) > 0 {
|
||||
// delete one by one ...
|
||||
for _, t := range tableNames {
|
||||
query = `DROP TABLE IF EXISTS ` + pq.QuoteIdentifier(t) + ` CASCADE`
|
||||
if _, err := p.conn.ExecContext(context.Background(), query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureVersionTable checks if versions table exists and, if not, creates it.
|
||||
// Note that this function locks the database, which deviates from the usual
|
||||
// convention of "caller locks" in the Postgres type.
|
||||
func (p *Postgres) ensureVersionTable() (err error) {
|
||||
if err = p.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// This block checks whether the `MigrationsTable` already exists. This is useful because it allows read only postgres
|
||||
// users to also check the current version of the schema. Previously, even if `MigrationsTable` existed, the
|
||||
// `CREATE TABLE IF NOT EXISTS...` query would fail because the user does not have the CREATE permission.
|
||||
// Taken from https://github.com/mattes/migrate/blob/master/database/postgres/postgres.go#L258
|
||||
query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 LIMIT 1`
|
||||
row := p.conn.QueryRowContext(context.Background(), query, p.config.migrationsSchemaName, p.config.migrationsTableName)
|
||||
|
||||
var count int
|
||||
err = row.Scan(&count)
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
query = `CREATE TABLE IF NOT EXISTS ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + pq.QuoteIdentifier(p.config.migrationsTableName) + ` (version bigint not null primary key, dirty boolean not null)`
|
||||
if _, err = p.conn.ExecContext(context.Background(), query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
33
vendor/github.com/golang-migrate/migrate/v4/database/util.go
generated
vendored
Normal file
33
vendor/github.com/golang-migrate/migrate/v4/database/util.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"hash/crc32"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const advisoryLockIDSalt uint = 1486364155
|
||||
|
||||
// GenerateAdvisoryLockId inspired by rails migrations, see https://goo.gl/8o9bCT
|
||||
func GenerateAdvisoryLockId(databaseName string, additionalNames ...string) (string, error) { // nolint: golint
|
||||
if len(additionalNames) > 0 {
|
||||
databaseName = strings.Join(append(additionalNames, databaseName), "\x00")
|
||||
}
|
||||
sum := crc32.ChecksumIEEE([]byte(databaseName))
|
||||
sum = sum * uint32(advisoryLockIDSalt)
|
||||
return fmt.Sprint(sum), nil
|
||||
}
|
||||
|
||||
// CasRestoreOnErr CAS wrapper to automatically restore the lock state on error
|
||||
func CasRestoreOnErr(lock *atomic.Bool, o, n bool, casErr error, f func() error) error {
|
||||
if !lock.CAS(o, n) {
|
||||
return casErr
|
||||
}
|
||||
if err := f(); err != nil {
|
||||
// Automatically unlock/lock on error
|
||||
lock.Store(o)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
5
vendor/github.com/golang-migrate/migrate/v4/docker-deploy.sh
generated
vendored
Normal file
5
vendor/github.com/golang-migrate/migrate/v4/docker-deploy.sh
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin && \
|
||||
docker build --build-arg VERSION="$TRAVIS_TAG" . -t migrate/migrate -t migrate/migrate:"$TRAVIS_TAG" && \
|
||||
docker push migrate/migrate:"$TRAVIS_TAG" && docker push migrate/migrate
|
||||
25
vendor/github.com/golang-migrate/migrate/v4/internal/url/url.go
generated
vendored
Normal file
25
vendor/github.com/golang-migrate/migrate/v4/internal/url/url.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
package url
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var errNoScheme = errors.New("no scheme")
|
||||
var errEmptyURL = errors.New("URL cannot be empty")
|
||||
|
||||
// schemeFromURL returns the scheme from a URL string
|
||||
func SchemeFromURL(url string) (string, error) {
|
||||
if url == "" {
|
||||
return "", errEmptyURL
|
||||
}
|
||||
|
||||
i := strings.Index(url, ":")
|
||||
|
||||
// No : or : is the first character.
|
||||
if i < 1 {
|
||||
return "", errNoScheme
|
||||
}
|
||||
|
||||
return url[0:i], nil
|
||||
}
|
||||
12
vendor/github.com/golang-migrate/migrate/v4/log.go
generated
vendored
Normal file
12
vendor/github.com/golang-migrate/migrate/v4/log.go
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
package migrate
|
||||
|
||||
// Logger is an interface so you can pass in your own
|
||||
// logging implementation.
|
||||
type Logger interface {
|
||||
|
||||
// Printf is like fmt.Printf
|
||||
Printf(format string, v ...interface{})
|
||||
|
||||
// Verbose should return true when verbose logging output is wanted
|
||||
Verbose() bool
|
||||
}
|
||||
981
vendor/github.com/golang-migrate/migrate/v4/migrate.go
generated
vendored
Normal file
981
vendor/github.com/golang-migrate/migrate/v4/migrate.go
generated
vendored
Normal file
@@ -0,0 +1,981 @@
|
||||
// Package migrate reads migrations from sources and runs them against databases.
|
||||
// Sources are defined by the `source.Driver` and databases by the `database.Driver`
|
||||
// interface. The driver interfaces are kept "dumb", all migration logic is kept
|
||||
// in this package.
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
iurl "github.com/golang-migrate/migrate/v4/internal/url"
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
)
|
||||
|
||||
// DefaultPrefetchMigrations sets the number of migrations to pre-read
|
||||
// from the source. This is helpful if the source is remote, but has little
|
||||
// effect for a local source (i.e. file system).
|
||||
// Please note that this setting has a major impact on the memory usage,
|
||||
// since each pre-read migration is buffered in memory. See DefaultBufferSize.
|
||||
var DefaultPrefetchMigrations = uint(10)
|
||||
|
||||
// DefaultLockTimeout sets the max time a database driver has to acquire a lock.
|
||||
var DefaultLockTimeout = 15 * time.Second
|
||||
|
||||
var (
|
||||
ErrNoChange = errors.New("no change")
|
||||
ErrNilVersion = errors.New("no migration")
|
||||
ErrInvalidVersion = errors.New("version must be >= -1")
|
||||
ErrLocked = errors.New("database locked")
|
||||
ErrLockTimeout = errors.New("timeout: can't acquire database lock")
|
||||
)
|
||||
|
||||
// ErrShortLimit is an error returned when not enough migrations
|
||||
// can be returned by a source for a given limit.
|
||||
type ErrShortLimit struct {
|
||||
Short uint
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e ErrShortLimit) Error() string {
|
||||
return fmt.Sprintf("limit %v short", e.Short)
|
||||
}
|
||||
|
||||
type ErrDirty struct {
|
||||
Version int
|
||||
}
|
||||
|
||||
func (e ErrDirty) Error() string {
|
||||
return fmt.Sprintf("Dirty database version %v. Fix and force version.", e.Version)
|
||||
}
|
||||
|
||||
type Migrate struct {
|
||||
sourceName string
|
||||
sourceDrv source.Driver
|
||||
databaseName string
|
||||
databaseDrv database.Driver
|
||||
|
||||
// Log accepts a Logger interface
|
||||
Log Logger
|
||||
|
||||
// GracefulStop accepts `true` and will stop executing migrations
|
||||
// as soon as possible at a safe break point, so that the database
|
||||
// is not corrupted.
|
||||
GracefulStop chan bool
|
||||
isLockedMu *sync.Mutex
|
||||
|
||||
isGracefulStop bool
|
||||
isLocked bool
|
||||
|
||||
// PrefetchMigrations defaults to DefaultPrefetchMigrations,
|
||||
// but can be set per Migrate instance.
|
||||
PrefetchMigrations uint
|
||||
|
||||
// LockTimeout defaults to DefaultLockTimeout,
|
||||
// but can be set per Migrate instance.
|
||||
LockTimeout time.Duration
|
||||
}
|
||||
|
||||
// New returns a new Migrate instance from a source URL and a database URL.
|
||||
// The URL scheme is defined by each driver.
|
||||
func New(sourceURL, databaseURL string) (*Migrate, error) {
|
||||
m := newCommon()
|
||||
|
||||
sourceName, err := iurl.SchemeFromURL(sourceURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sourceName = sourceName
|
||||
|
||||
databaseName, err := iurl.SchemeFromURL(databaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.databaseName = databaseName
|
||||
|
||||
sourceDrv, err := source.Open(sourceURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sourceDrv = sourceDrv
|
||||
|
||||
databaseDrv, err := database.Open(databaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.databaseDrv = databaseDrv
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// NewWithDatabaseInstance returns a new Migrate instance from a source URL
|
||||
// and an existing database instance. The source URL scheme is defined by each driver.
|
||||
// Use any string that can serve as an identifier during logging as databaseName.
|
||||
// You are responsible for closing the underlying database client if necessary.
|
||||
func NewWithDatabaseInstance(sourceURL string, databaseName string, databaseInstance database.Driver) (*Migrate, error) {
|
||||
m := newCommon()
|
||||
|
||||
sourceName, err := iurl.SchemeFromURL(sourceURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sourceName = sourceName
|
||||
|
||||
m.databaseName = databaseName
|
||||
|
||||
sourceDrv, err := source.Open(sourceURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sourceDrv = sourceDrv
|
||||
|
||||
m.databaseDrv = databaseInstance
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// NewWithSourceInstance returns a new Migrate instance from an existing source instance
|
||||
// and a database URL. The database URL scheme is defined by each driver.
|
||||
// Use any string that can serve as an identifier during logging as sourceName.
|
||||
// You are responsible for closing the underlying source client if necessary.
|
||||
func NewWithSourceInstance(sourceName string, sourceInstance source.Driver, databaseURL string) (*Migrate, error) {
|
||||
m := newCommon()
|
||||
|
||||
databaseName, err := iurl.SchemeFromURL(databaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.databaseName = databaseName
|
||||
|
||||
m.sourceName = sourceName
|
||||
|
||||
databaseDrv, err := database.Open(databaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.databaseDrv = databaseDrv
|
||||
|
||||
m.sourceDrv = sourceInstance
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// NewWithInstance returns a new Migrate instance from an existing source and
|
||||
// database instance. Use any string that can serve as an identifier during logging
|
||||
// as sourceName and databaseName. You are responsible for closing down
|
||||
// the underlying source and database client if necessary.
|
||||
func NewWithInstance(sourceName string, sourceInstance source.Driver, databaseName string, databaseInstance database.Driver) (*Migrate, error) {
|
||||
m := newCommon()
|
||||
|
||||
m.sourceName = sourceName
|
||||
m.databaseName = databaseName
|
||||
|
||||
m.sourceDrv = sourceInstance
|
||||
m.databaseDrv = databaseInstance
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func newCommon() *Migrate {
|
||||
return &Migrate{
|
||||
GracefulStop: make(chan bool, 1),
|
||||
PrefetchMigrations: DefaultPrefetchMigrations,
|
||||
LockTimeout: DefaultLockTimeout,
|
||||
isLockedMu: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the source and the database.
|
||||
func (m *Migrate) Close() (source error, database error) {
|
||||
databaseSrvClose := make(chan error)
|
||||
sourceSrvClose := make(chan error)
|
||||
|
||||
m.logVerbosePrintf("Closing source and database\n")
|
||||
|
||||
go func() {
|
||||
databaseSrvClose <- m.databaseDrv.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
sourceSrvClose <- m.sourceDrv.Close()
|
||||
}()
|
||||
|
||||
return <-sourceSrvClose, <-databaseSrvClose
|
||||
}
|
||||
|
||||
// Migrate looks at the currently active migration version,
|
||||
// then migrates either up or down to the specified version.
|
||||
func (m *Migrate) Migrate(version uint) error {
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, dirty, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
if dirty {
|
||||
return m.unlockErr(ErrDirty{curVersion})
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
go m.read(curVersion, int(version), ret)
|
||||
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
// Steps looks at the currently active migration version.
|
||||
// It will migrate up if n > 0, and down if n < 0.
|
||||
func (m *Migrate) Steps(n int) error {
|
||||
if n == 0 {
|
||||
return ErrNoChange
|
||||
}
|
||||
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, dirty, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
if dirty {
|
||||
return m.unlockErr(ErrDirty{curVersion})
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
|
||||
if n > 0 {
|
||||
go m.readUp(curVersion, n, ret)
|
||||
} else {
|
||||
go m.readDown(curVersion, -n, ret)
|
||||
}
|
||||
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
// Up looks at the currently active migration version
|
||||
// and will migrate all the way up (applying all up migrations).
|
||||
func (m *Migrate) Up() error {
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, dirty, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
if dirty {
|
||||
return m.unlockErr(ErrDirty{curVersion})
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
|
||||
go m.readUp(curVersion, -1, ret)
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
// Down looks at the currently active migration version
|
||||
// and will migrate all the way down (applying all down migrations).
|
||||
func (m *Migrate) Down() error {
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, dirty, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
if dirty {
|
||||
return m.unlockErr(ErrDirty{curVersion})
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
go m.readDown(curVersion, -1, ret)
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
// Drop deletes everything in the database.
|
||||
func (m *Migrate) Drop() error {
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.databaseDrv.Drop(); err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
return m.unlock()
|
||||
}
|
||||
|
||||
// Run runs any migration provided by you against the database.
|
||||
// It does not check any currently active version in database.
|
||||
// Usually you don't need this function at all. Use Migrate,
|
||||
// Steps, Up or Down instead.
|
||||
func (m *Migrate) Run(migration ...*Migration) error {
|
||||
if len(migration) == 0 {
|
||||
return ErrNoChange
|
||||
}
|
||||
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, dirty, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
if dirty {
|
||||
return m.unlockErr(ErrDirty{curVersion})
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
|
||||
go func() {
|
||||
defer close(ret)
|
||||
for _, migr := range migration {
|
||||
if m.PrefetchMigrations > 0 && migr.Body != nil {
|
||||
m.logVerbosePrintf("Start buffering %v\n", migr.LogString())
|
||||
} else {
|
||||
m.logVerbosePrintf("Scheduled %v\n", migr.LogString())
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go func(migr *Migration) {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}(migr)
|
||||
}
|
||||
}()
|
||||
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
// Force sets a migration version.
|
||||
// It does not check any currently active version in database.
|
||||
// It resets the dirty state to false.
|
||||
func (m *Migrate) Force(version int) error {
|
||||
if version < -1 {
|
||||
return ErrInvalidVersion
|
||||
}
|
||||
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.databaseDrv.SetVersion(version, false); err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
return m.unlock()
|
||||
}
|
||||
|
||||
// Version returns the currently active migration version.
|
||||
// If no migration has been applied, yet, it will return ErrNilVersion.
|
||||
func (m *Migrate) Version() (version uint, dirty bool, err error) {
|
||||
v, d, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
if v == database.NilVersion {
|
||||
return 0, false, ErrNilVersion
|
||||
}
|
||||
|
||||
return suint(v), d, nil
|
||||
}
|
||||
|
||||
// read reads either up or down migrations from source `from` to `to`.
|
||||
// Each migration is then written to the ret channel.
|
||||
// If an error occurs during reading, that error is written to the ret channel, too.
|
||||
// Once read is done reading it will close the ret channel.
|
||||
func (m *Migrate) read(from int, to int, ret chan<- interface{}) {
|
||||
defer close(ret)
|
||||
|
||||
// check if from version exists
|
||||
if from >= 0 {
|
||||
if err := m.versionExists(suint(from)); err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check if to version exists
|
||||
if to >= 0 {
|
||||
if err := m.versionExists(suint(to)); err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// no change?
|
||||
if from == to {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
if from < to {
|
||||
// it's going up
|
||||
// apply first migration if from is nil version
|
||||
if from == -1 {
|
||||
firstVersion, err := m.sourceDrv.First()
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(firstVersion, int(firstVersion))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go func() {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}()
|
||||
|
||||
from = int(firstVersion)
|
||||
}
|
||||
|
||||
// run until we reach target ...
|
||||
for from < to {
|
||||
if m.stop() {
|
||||
return
|
||||
}
|
||||
|
||||
next, err := m.sourceDrv.Next(suint(from))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(next, int(next))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go func() {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}()
|
||||
|
||||
from = int(next)
|
||||
}
|
||||
|
||||
} else {
|
||||
// it's going down
|
||||
// run until we reach target ...
|
||||
for from > to && from >= 0 {
|
||||
if m.stop() {
|
||||
return
|
||||
}
|
||||
|
||||
prev, err := m.sourceDrv.Prev(suint(from))
|
||||
if errors.Is(err, os.ErrNotExist) && to == -1 {
|
||||
// apply nil migration
|
||||
migr, err := m.newMigration(suint(from), -1)
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
ret <- migr
|
||||
go func() {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
|
||||
} else if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(suint(from), int(prev))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go func() {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}()
|
||||
|
||||
from = int(prev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readUp reads up migrations from `from` limitted by `limit`.
|
||||
// limit can be -1, implying no limit and reading until there are no more migrations.
|
||||
// Each migration is then written to the ret channel.
|
||||
// If an error occurs during reading, that error is written to the ret channel, too.
|
||||
// Once readUp is done reading it will close the ret channel.
|
||||
func (m *Migrate) readUp(from int, limit int, ret chan<- interface{}) {
|
||||
defer close(ret)
|
||||
|
||||
// check if from version exists
|
||||
if from >= 0 {
|
||||
if err := m.versionExists(suint(from)); err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
count := 0
|
||||
for count < limit || limit == -1 {
|
||||
if m.stop() {
|
||||
return
|
||||
}
|
||||
|
||||
// apply first migration if from is nil version
|
||||
if from == -1 {
|
||||
firstVersion, err := m.sourceDrv.First()
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(firstVersion, int(firstVersion))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go func() {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}()
|
||||
from = int(firstVersion)
|
||||
count++
|
||||
continue
|
||||
}
|
||||
|
||||
// apply next migration
|
||||
next, err := m.sourceDrv.Next(suint(from))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// no limit, but no migrations applied?
|
||||
if limit == -1 && count == 0 {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
// no limit, reached end
|
||||
if limit == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
// reached end, and didn't apply any migrations
|
||||
if limit > 0 && count == 0 {
|
||||
ret <- os.ErrNotExist
|
||||
return
|
||||
}
|
||||
|
||||
// applied less migrations than limit?
|
||||
if count < limit {
|
||||
ret <- ErrShortLimit{suint(limit - count)}
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(next, int(next))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go func() {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}()
|
||||
from = int(next)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
// readDown reads down migrations from `from` limitted by `limit`.
|
||||
// limit can be -1, implying no limit and reading until there are no more migrations.
|
||||
// Each migration is then written to the ret channel.
|
||||
// If an error occurs during reading, that error is written to the ret channel, too.
|
||||
// Once readDown is done reading it will close the ret channel.
|
||||
func (m *Migrate) readDown(from int, limit int, ret chan<- interface{}) {
|
||||
defer close(ret)
|
||||
|
||||
// check if from version exists
|
||||
if from >= 0 {
|
||||
if err := m.versionExists(suint(from)); err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
// no change if already at nil version
|
||||
if from == -1 && limit == -1 {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
// can't go over limit if already at nil version
|
||||
if from == -1 && limit > 0 {
|
||||
ret <- os.ErrNotExist
|
||||
return
|
||||
}
|
||||
|
||||
count := 0
|
||||
for count < limit || limit == -1 {
|
||||
if m.stop() {
|
||||
return
|
||||
}
|
||||
|
||||
prev, err := m.sourceDrv.Prev(suint(from))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// no limit or haven't reached limit, apply "first" migration
|
||||
if limit == -1 || limit-count > 0 {
|
||||
firstVersion, err := m.sourceDrv.First()
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(firstVersion, -1)
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
ret <- migr
|
||||
go func() {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}()
|
||||
count++
|
||||
}
|
||||
|
||||
if count < limit {
|
||||
ret <- ErrShortLimit{suint(limit - count)}
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(suint(from), int(prev))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go func() {
|
||||
if err := migr.Buffer(); err != nil {
|
||||
m.logErr(err)
|
||||
}
|
||||
}()
|
||||
from = int(prev)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
// runMigrations reads *Migration and error from a channel. Any other type
|
||||
// sent on this channel will result in a panic. Each migration is then
|
||||
// proxied to the database driver and run against the database.
|
||||
// Before running a newly received migration it will check if it's supposed
|
||||
// to stop execution because it might have received a stop signal on the
|
||||
// GracefulStop channel.
|
||||
func (m *Migrate) runMigrations(ret <-chan interface{}) error {
|
||||
for r := range ret {
|
||||
|
||||
if m.stop() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch r := r.(type) {
|
||||
case error:
|
||||
return r
|
||||
|
||||
case *Migration:
|
||||
migr := r
|
||||
|
||||
// set version with dirty state
|
||||
if err := m.databaseDrv.SetVersion(migr.TargetVersion, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if migr.Body != nil {
|
||||
m.logVerbosePrintf("Read and execute %v\n", migr.LogString())
|
||||
if err := m.databaseDrv.Run(migr.BufferedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set clean state
|
||||
if err := m.databaseDrv.SetVersion(migr.TargetVersion, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endTime := time.Now()
|
||||
readTime := migr.FinishedReading.Sub(migr.StartedBuffering)
|
||||
runTime := endTime.Sub(migr.FinishedReading)
|
||||
|
||||
// log either verbose or normal
|
||||
if m.Log != nil {
|
||||
if m.Log.Verbose() {
|
||||
m.logPrintf("Finished %v (read %v, ran %v)\n", migr.LogString(), readTime, runTime)
|
||||
} else {
|
||||
m.logPrintf("%v (%v)\n", migr.LogString(), readTime+runTime)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown type: %T with value: %+v", r, r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// versionExists checks the source if either the up or down migration for
|
||||
// the specified migration version exists.
|
||||
func (m *Migrate) versionExists(version uint) (result error) {
|
||||
// try up migration first
|
||||
up, _, err := m.sourceDrv.ReadUp(version)
|
||||
if err == nil {
|
||||
defer func() {
|
||||
if errClose := up.Close(); errClose != nil {
|
||||
result = multierror.Append(result, errClose)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
// then try down migration
|
||||
down, _, err := m.sourceDrv.ReadDown(version)
|
||||
if err == nil {
|
||||
defer func() {
|
||||
if errClose := down.Close(); errClose != nil {
|
||||
result = multierror.Append(result, errClose)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = fmt.Errorf("no migration found for version %d: %w", version, err)
|
||||
m.logErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// stop returns true if no more migrations should be run against the database
|
||||
// because a stop signal was received on the GracefulStop channel.
|
||||
// Calls are cheap and this function is not blocking.
|
||||
func (m *Migrate) stop() bool {
|
||||
if m.isGracefulStop {
|
||||
return true
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.GracefulStop:
|
||||
m.isGracefulStop = true
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// newMigration is a helper func that returns a *Migration for the
|
||||
// specified version and targetVersion.
|
||||
func (m *Migrate) newMigration(version uint, targetVersion int) (*Migration, error) {
|
||||
var migr *Migration
|
||||
|
||||
if targetVersion >= int(version) {
|
||||
r, identifier, err := m.sourceDrv.ReadUp(version)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// create "empty" migration
|
||||
migr, err = NewMigration(nil, "", version, targetVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
|
||||
} else {
|
||||
// create migration from up source
|
||||
migr, err = NewMigration(r, identifier, version, targetVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
r, identifier, err := m.sourceDrv.ReadDown(version)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// create "empty" migration
|
||||
migr, err = NewMigration(nil, "", version, targetVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
|
||||
} else {
|
||||
// create migration from down source
|
||||
migr, err = NewMigration(r, identifier, version, targetVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.PrefetchMigrations > 0 && migr.Body != nil {
|
||||
m.logVerbosePrintf("Start buffering %v\n", migr.LogString())
|
||||
} else {
|
||||
m.logVerbosePrintf("Scheduled %v\n", migr.LogString())
|
||||
}
|
||||
|
||||
return migr, nil
|
||||
}
|
||||
|
||||
// lock is a thread safe helper function to lock the database.
|
||||
// It should be called as late as possible when running migrations.
|
||||
func (m *Migrate) lock() error {
|
||||
m.isLockedMu.Lock()
|
||||
defer m.isLockedMu.Unlock()
|
||||
|
||||
if m.isLocked {
|
||||
return ErrLocked
|
||||
}
|
||||
|
||||
// create done channel, used in the timeout goroutine
|
||||
done := make(chan bool, 1)
|
||||
defer func() {
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// use errchan to signal error back to this context
|
||||
errchan := make(chan error, 2)
|
||||
|
||||
// start timeout goroutine
|
||||
timeout := time.After(m.LockTimeout)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-timeout:
|
||||
errchan <- ErrLockTimeout
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// now try to acquire the lock
|
||||
go func() {
|
||||
if err := m.databaseDrv.Lock(); err != nil {
|
||||
errchan <- err
|
||||
} else {
|
||||
errchan <- nil
|
||||
}
|
||||
}()
|
||||
|
||||
// wait until we either receive ErrLockTimeout or error from Lock operation
|
||||
err := <-errchan
|
||||
if err == nil {
|
||||
m.isLocked = true
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// unlock is a thread safe helper function to unlock the database.
|
||||
// It should be called as early as possible when no more migrations are
|
||||
// expected to be executed.
|
||||
func (m *Migrate) unlock() error {
|
||||
m.isLockedMu.Lock()
|
||||
defer m.isLockedMu.Unlock()
|
||||
|
||||
if err := m.databaseDrv.Unlock(); err != nil {
|
||||
// BUG: Can potentially create a deadlock. Add a timeout.
|
||||
return err
|
||||
}
|
||||
|
||||
m.isLocked = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// unlockErr calls unlock and returns a combined error
|
||||
// if a prevErr is not nil.
|
||||
func (m *Migrate) unlockErr(prevErr error) error {
|
||||
if err := m.unlock(); err != nil {
|
||||
return multierror.Append(prevErr, err)
|
||||
}
|
||||
return prevErr
|
||||
}
|
||||
|
||||
// logPrintf writes to m.Log if not nil
|
||||
func (m *Migrate) logPrintf(format string, v ...interface{}) {
|
||||
if m.Log != nil {
|
||||
m.Log.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// logVerbosePrintf writes to m.Log if not nil. Use for verbose logging output.
|
||||
func (m *Migrate) logVerbosePrintf(format string, v ...interface{}) {
|
||||
if m.Log != nil && m.Log.Verbose() {
|
||||
m.Log.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// logErr writes error to m.Log if not nil
|
||||
func (m *Migrate) logErr(err error) {
|
||||
if m.Log != nil {
|
||||
m.Log.Printf("error: %v", err)
|
||||
}
|
||||
}
|
||||
160
vendor/github.com/golang-migrate/migrate/v4/migration.go
generated
vendored
Normal file
160
vendor/github.com/golang-migrate/migrate/v4/migration.go
generated
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultBufferSize sets the in memory buffer size (in Bytes) for every
|
||||
// pre-read migration (see DefaultPrefetchMigrations).
|
||||
var DefaultBufferSize = uint(100000)
|
||||
|
||||
// Migration holds information about a migration.
|
||||
// It is initially created from data coming from the source and then
|
||||
// used when run against the database.
|
||||
type Migration struct {
|
||||
// Identifier can be any string to help identifying
|
||||
// the migration in the source.
|
||||
Identifier string
|
||||
|
||||
// Version is the version of this migration.
|
||||
Version uint
|
||||
|
||||
// TargetVersion is the migration version after this migration
|
||||
// has been applied to the database.
|
||||
// Can be -1, implying that this is a NilVersion.
|
||||
TargetVersion int
|
||||
|
||||
// Body holds an io.ReadCloser to the source.
|
||||
Body io.ReadCloser
|
||||
|
||||
// BufferedBody holds an buffered io.Reader to the underlying Body.
|
||||
BufferedBody io.Reader
|
||||
|
||||
// BufferSize defaults to DefaultBufferSize
|
||||
BufferSize uint
|
||||
|
||||
// bufferWriter holds an io.WriteCloser and pipes to BufferBody.
|
||||
// It's an *Closer for flow control.
|
||||
bufferWriter io.WriteCloser
|
||||
|
||||
// Scheduled is the time when the migration was scheduled/ queued.
|
||||
Scheduled time.Time
|
||||
|
||||
// StartedBuffering is the time when buffering of the migration source started.
|
||||
StartedBuffering time.Time
|
||||
|
||||
// FinishedBuffering is the time when buffering of the migration source finished.
|
||||
FinishedBuffering time.Time
|
||||
|
||||
// FinishedReading is the time when the migration source is fully read.
|
||||
FinishedReading time.Time
|
||||
|
||||
// BytesRead holds the number of Bytes read from the migration source.
|
||||
BytesRead int64
|
||||
}
|
||||
|
||||
// NewMigration returns a new Migration and sets the body, identifier,
|
||||
// version and targetVersion. Body can be nil, which turns this migration
|
||||
// into a "NilMigration". If no identifier is provided, it will default to "<empty>".
|
||||
// targetVersion can be -1, implying it is a NilVersion.
|
||||
//
|
||||
// What is a NilMigration?
|
||||
// Usually each migration version coming from source is expected to have an
|
||||
// Up and Down migration. This is not a hard requirement though, leading to
|
||||
// a situation where only the Up or Down migration is present. So let's say
|
||||
// the user wants to migrate up to a version that doesn't have the actual Up
|
||||
// migration, in that case we still want to apply the version, but with an empty
|
||||
// body. We are calling that a NilMigration, a migration with an empty body.
|
||||
//
|
||||
// What is a NilVersion?
|
||||
// NilVersion is a const(-1). When running down migrations and we are at the
|
||||
// last down migration, there is no next down migration, the targetVersion should
|
||||
// be nil. Nil in this case is represented by -1 (because type int).
|
||||
func NewMigration(body io.ReadCloser, identifier string,
|
||||
version uint, targetVersion int) (*Migration, error) {
|
||||
tnow := time.Now()
|
||||
m := &Migration{
|
||||
Identifier: identifier,
|
||||
Version: version,
|
||||
TargetVersion: targetVersion,
|
||||
Scheduled: tnow,
|
||||
}
|
||||
|
||||
if body == nil {
|
||||
if len(identifier) == 0 {
|
||||
m.Identifier = "<empty>"
|
||||
}
|
||||
|
||||
m.StartedBuffering = tnow
|
||||
m.FinishedBuffering = tnow
|
||||
m.FinishedReading = tnow
|
||||
return m, nil
|
||||
}
|
||||
|
||||
br, bw := io.Pipe()
|
||||
m.Body = body // want to simulate low latency? newSlowReader(body)
|
||||
m.BufferSize = DefaultBufferSize
|
||||
m.BufferedBody = br
|
||||
m.bufferWriter = bw
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// String implements string.Stringer and is used in tests.
|
||||
func (m *Migration) String() string {
|
||||
return fmt.Sprintf("%v [%v=>%v]", m.Identifier, m.Version, m.TargetVersion)
|
||||
}
|
||||
|
||||
// LogString returns a string describing this migration to humans.
|
||||
func (m *Migration) LogString() string {
|
||||
directionStr := "u"
|
||||
if m.TargetVersion < int(m.Version) {
|
||||
directionStr = "d"
|
||||
}
|
||||
return fmt.Sprintf("%v/%v %v", m.Version, directionStr, m.Identifier)
|
||||
}
|
||||
|
||||
// Buffer buffers Body up to BufferSize.
|
||||
// Calling this function blocks. Call with goroutine.
|
||||
func (m *Migration) Buffer() error {
|
||||
if m.Body == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.StartedBuffering = time.Now()
|
||||
|
||||
b := bufio.NewReaderSize(m.Body, int(m.BufferSize))
|
||||
|
||||
// start reading from body, peek won't move the read pointer though
|
||||
// poor man's solution?
|
||||
if _, err := b.Peek(int(m.BufferSize)); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
m.FinishedBuffering = time.Now()
|
||||
|
||||
// write to bufferWriter, this will block until
|
||||
// something starts reading from m.Buffer
|
||||
n, err := b.WriteTo(m.bufferWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.FinishedReading = time.Now()
|
||||
m.BytesRead = n
|
||||
|
||||
// close bufferWriter so Buffer knows that there is no
|
||||
// more data coming
|
||||
if err := m.bufferWriter.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// it's safe to close the Body too
|
||||
if err := m.Body.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
118
vendor/github.com/golang-migrate/migrate/v4/source/driver.go
generated
vendored
Normal file
118
vendor/github.com/golang-migrate/migrate/v4/source/driver.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
// Package source provides the Source interface.
|
||||
// All source drivers must implement this interface, register themselves,
|
||||
// optionally provide a `WithInstance` function and pass the tests
|
||||
// in package source/testing.
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var driversMu sync.RWMutex
|
||||
var drivers = make(map[string]Driver)
|
||||
|
||||
// Driver is the interface every source driver must implement.
|
||||
//
|
||||
// How to implement a source driver?
|
||||
// 1. Implement this interface.
|
||||
// 2. Optionally, add a function named `WithInstance`.
|
||||
// This function should accept an existing source instance and a Config{} struct
|
||||
// and return a driver instance.
|
||||
// 3. Add a test that calls source/testing.go:Test()
|
||||
// 4. Add own tests for Open(), WithInstance() (when provided) and Close().
|
||||
// All other functions are tested by tests in source/testing.
|
||||
// Saves you some time and makes sure all source drivers behave the same way.
|
||||
// 5. Call Register in init().
|
||||
//
|
||||
// Guidelines:
|
||||
// - All configuration input must come from the URL string in func Open()
|
||||
// or the Config{} struct in WithInstance. Don't os.Getenv().
|
||||
// - Drivers are supposed to be read only.
|
||||
// - Ideally don't load any contents (into memory) in Open or WithInstance.
|
||||
type Driver interface {
|
||||
// Open returns a new driver instance configured with parameters
|
||||
// coming from the URL string. Migrate will call this function
|
||||
// only once per instance.
|
||||
Open(url string) (Driver, error)
|
||||
|
||||
// Close closes the underlying source instance managed by the driver.
|
||||
// Migrate will call this function only once per instance.
|
||||
Close() error
|
||||
|
||||
// First returns the very first migration version available to the driver.
|
||||
// Migrate will call this function multiple times.
|
||||
// If there is no version available, it must return os.ErrNotExist.
|
||||
First() (version uint, err error)
|
||||
|
||||
// Prev returns the previous version for a given version available to the driver.
|
||||
// Migrate will call this function multiple times.
|
||||
// If there is no previous version available, it must return os.ErrNotExist.
|
||||
Prev(version uint) (prevVersion uint, err error)
|
||||
|
||||
// Next returns the next version for a given version available to the driver.
|
||||
// Migrate will call this function multiple times.
|
||||
// If there is no next version available, it must return os.ErrNotExist.
|
||||
Next(version uint) (nextVersion uint, err error)
|
||||
|
||||
// ReadUp returns the UP migration body and an identifier that helps
|
||||
// finding this migration in the source for a given version.
|
||||
// If there is no up migration available for this version,
|
||||
// it must return os.ErrNotExist.
|
||||
// Do not start reading, just return the ReadCloser!
|
||||
ReadUp(version uint) (r io.ReadCloser, identifier string, err error)
|
||||
|
||||
// ReadDown returns the DOWN migration body and an identifier that helps
|
||||
// finding this migration in the source for a given version.
|
||||
// If there is no down migration available for this version,
|
||||
// it must return os.ErrNotExist.
|
||||
// Do not start reading, just return the ReadCloser!
|
||||
ReadDown(version uint) (r io.ReadCloser, identifier string, err error)
|
||||
}
|
||||
|
||||
// Open returns a new driver instance.
|
||||
func Open(url string) (Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme == "" {
|
||||
return nil, fmt.Errorf("source driver: invalid URL scheme")
|
||||
}
|
||||
|
||||
driversMu.RLock()
|
||||
d, ok := drivers[u.Scheme]
|
||||
driversMu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("source driver: unknown driver '%s' (forgotten import?)", u.Scheme)
|
||||
}
|
||||
|
||||
return d.Open(url)
|
||||
}
|
||||
|
||||
// Register globally registers a driver.
|
||||
func Register(name string, driver Driver) {
|
||||
driversMu.Lock()
|
||||
defer driversMu.Unlock()
|
||||
if driver == nil {
|
||||
panic("Register driver is nil")
|
||||
}
|
||||
if _, dup := drivers[name]; dup {
|
||||
panic("Register called twice for driver " + name)
|
||||
}
|
||||
drivers[name] = driver
|
||||
}
|
||||
|
||||
// List lists the registered drivers
|
||||
func List() []string {
|
||||
driversMu.RLock()
|
||||
defer driversMu.RUnlock()
|
||||
names := make([]string, 0, len(drivers))
|
||||
for n := range drivers {
|
||||
names = append(names, n)
|
||||
}
|
||||
return names
|
||||
}
|
||||
15
vendor/github.com/golang-migrate/migrate/v4/source/errors.go
generated
vendored
Normal file
15
vendor/github.com/golang-migrate/migrate/v4/source/errors.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
package source
|
||||
|
||||
import "os"
|
||||
|
||||
// ErrDuplicateMigration is an error type for reporting duplicate migration
|
||||
// files.
|
||||
type ErrDuplicateMigration struct {
|
||||
Migration
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
// Error implements error interface.
|
||||
func (e ErrDuplicateMigration) Error() string {
|
||||
return "duplicate migration file: " + e.Name()
|
||||
}
|
||||
4
vendor/github.com/golang-migrate/migrate/v4/source/file/README.md
generated
vendored
Normal file
4
vendor/github.com/golang-migrate/migrate/v4/source/file/README.md
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# file
|
||||
|
||||
`file:///absolute/path`
|
||||
`file://relative/path`
|
||||
66
vendor/github.com/golang-migrate/migrate/v4/source/file/file.go
generated
vendored
Normal file
66
vendor/github.com/golang-migrate/migrate/v4/source/file/file.go
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
nurl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("file", &File{})
|
||||
}
|
||||
|
||||
type File struct {
|
||||
iofs.PartialDriver
|
||||
url string
|
||||
path string
|
||||
}
|
||||
|
||||
func (f *File) Open(url string) (source.Driver, error) {
|
||||
p, err := parseURL(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nf := &File{
|
||||
url: url,
|
||||
path: p,
|
||||
}
|
||||
if err := nf.Init(os.DirFS(p), "."); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nf, nil
|
||||
}
|
||||
|
||||
func parseURL(url string) (string, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// concat host and path to restore full path
|
||||
// host might be `.`
|
||||
p := u.Opaque
|
||||
if len(p) == 0 {
|
||||
p = u.Host + u.Path
|
||||
}
|
||||
|
||||
if len(p) == 0 {
|
||||
// default to current directory if no path
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p = wd
|
||||
|
||||
} else if p[0:1] == "." || p[0:1] != "/" {
|
||||
// make path absolute if relative
|
||||
abs, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p = abs
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
3
vendor/github.com/golang-migrate/migrate/v4/source/iofs/README.md
generated
vendored
Normal file
3
vendor/github.com/golang-migrate/migrate/v4/source/iofs/README.md
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# iofs
|
||||
|
||||
https://pkg.go.dev/github.com/golang-migrate/migrate/v4/source/iofs
|
||||
10
vendor/github.com/golang-migrate/migrate/v4/source/iofs/doc.go
generated
vendored
Normal file
10
vendor/github.com/golang-migrate/migrate/v4/source/iofs/doc.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Package iofs provides the Go 1.16+ io/fs#FS driver.
|
||||
|
||||
It can accept various file systems (like embed.FS, archive/zip#Reader) implementing io/fs#FS.
|
||||
|
||||
This driver cannot be used with Go versions 1.15 and below.
|
||||
|
||||
Also, Opening with a URL scheme is not supported.
|
||||
*/
|
||||
package iofs
|
||||
176
vendor/github.com/golang-migrate/migrate/v4/source/iofs/iofs.go
generated
vendored
Normal file
176
vendor/github.com/golang-migrate/migrate/v4/source/iofs/iofs.go
generated
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package iofs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
)
|
||||
|
||||
type driver struct {
|
||||
PartialDriver
|
||||
}
|
||||
|
||||
// New returns a new Driver from io/fs#FS and a relative path.
|
||||
func New(fsys fs.FS, path string) (source.Driver, error) {
|
||||
var i driver
|
||||
if err := i.Init(fsys, path); err != nil {
|
||||
return nil, fmt.Errorf("failed to init driver with path %s: %w", path, err)
|
||||
}
|
||||
return &i, nil
|
||||
}
|
||||
|
||||
// Open is part of source.Driver interface implementation.
|
||||
// Open cannot be called on the iofs passthrough driver.
|
||||
func (d *driver) Open(url string) (source.Driver, error) {
|
||||
return nil, errors.New("Open() cannot be called on the iofs passthrough driver")
|
||||
}
|
||||
|
||||
// PartialDriver is a helper service for creating new source drivers working with
|
||||
// io/fs.FS instances. It implements all source.Driver interface methods
|
||||
// except for Open(). New driver could embed this struct and add missing Open()
|
||||
// method.
|
||||
//
|
||||
// To prepare PartialDriver for use Init() function.
|
||||
type PartialDriver struct {
|
||||
migrations *source.Migrations
|
||||
fsys fs.FS
|
||||
path string
|
||||
}
|
||||
|
||||
// Init prepares not initialized IoFS instance to read migrations from a
|
||||
// io/fs#FS instance and a relative path.
|
||||
func (d *PartialDriver) Init(fsys fs.FS, path string) error {
|
||||
entries, err := fs.ReadDir(fsys, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ms := source.NewMigrations()
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
m, err := source.DefaultParse(e.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
file, err := e.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ms.Append(m) {
|
||||
return source.ErrDuplicateMigration{
|
||||
Migration: *m,
|
||||
FileInfo: file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.fsys = fsys
|
||||
d.path = path
|
||||
d.migrations = ms
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is part of source.Driver interface implementation.
|
||||
// Closes the file system if possible.
|
||||
func (d *PartialDriver) Close() error {
|
||||
c, ok := d.fsys.(io.Closer)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
// First is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) First() (version uint, err error) {
|
||||
if version, ok := d.migrations.First(); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &fs.PathError{
|
||||
Op: "first",
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// Prev is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) Prev(version uint) (prevVersion uint, err error) {
|
||||
if version, ok := d.migrations.Prev(version); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &fs.PathError{
|
||||
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// Next is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) Next(version uint) (nextVersion uint, err error) {
|
||||
if version, ok := d.migrations.Next(version); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &fs.PathError{
|
||||
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadUp is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := d.migrations.Up(version); ok {
|
||||
body, err := d.open(path.Join(d.path, m.Raw))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return body, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &fs.PathError{
|
||||
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadDown is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := d.migrations.Down(version); ok {
|
||||
body, err := d.open(path.Join(d.path, m.Raw))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return body, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &fs.PathError{
|
||||
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *PartialDriver) open(path string) (fs.File, error) {
|
||||
f, err := d.fsys.Open(path)
|
||||
if err == nil {
|
||||
return f, nil
|
||||
}
|
||||
// Some non-standard file systems may return errors that don't include the path, that
|
||||
// makes debugging harder.
|
||||
if !errors.As(err, new(*fs.PathError)) {
|
||||
err = &fs.PathError{
|
||||
Op: "open",
|
||||
Path: path,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
133
vendor/github.com/golang-migrate/migrate/v4/source/migration.go
generated
vendored
Normal file
133
vendor/github.com/golang-migrate/migrate/v4/source/migration.go
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Direction is either up or down.
|
||||
type Direction string
|
||||
|
||||
const (
|
||||
Down Direction = "down"
|
||||
Up Direction = "up"
|
||||
)
|
||||
|
||||
// Migration is a helper struct for source drivers that need to
|
||||
// build the full directory tree in memory.
|
||||
// Migration is fully independent from migrate.Migration.
|
||||
type Migration struct {
|
||||
// Version is the version of this migration.
|
||||
Version uint
|
||||
|
||||
// Identifier can be any string that helps identifying
|
||||
// this migration in the source.
|
||||
Identifier string
|
||||
|
||||
// Direction is either Up or Down.
|
||||
Direction Direction
|
||||
|
||||
// Raw holds the raw location path to this migration in source.
|
||||
// ReadUp and ReadDown will use this.
|
||||
Raw string
|
||||
}
|
||||
|
||||
// Migrations wraps Migration and has an internal index
|
||||
// to keep track of Migration order.
|
||||
type Migrations struct {
|
||||
index uintSlice
|
||||
migrations map[uint]map[Direction]*Migration
|
||||
}
|
||||
|
||||
func NewMigrations() *Migrations {
|
||||
return &Migrations{
|
||||
index: make(uintSlice, 0),
|
||||
migrations: make(map[uint]map[Direction]*Migration),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Migrations) Append(m *Migration) (ok bool) {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if i.migrations[m.Version] == nil {
|
||||
i.migrations[m.Version] = make(map[Direction]*Migration)
|
||||
}
|
||||
|
||||
// reject duplicate versions
|
||||
if _, dup := i.migrations[m.Version][m.Direction]; dup {
|
||||
return false
|
||||
}
|
||||
|
||||
i.migrations[m.Version][m.Direction] = m
|
||||
i.buildIndex()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (i *Migrations) buildIndex() {
|
||||
i.index = make(uintSlice, 0, len(i.migrations))
|
||||
for version := range i.migrations {
|
||||
i.index = append(i.index, version)
|
||||
}
|
||||
sort.Slice(i.index, func(x, y int) bool {
|
||||
return i.index[x] < i.index[y]
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Migrations) First() (version uint, ok bool) {
|
||||
if len(i.index) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return i.index[0], true
|
||||
}
|
||||
|
||||
func (i *Migrations) Prev(version uint) (prevVersion uint, ok bool) {
|
||||
pos := i.findPos(version)
|
||||
if pos >= 1 && len(i.index) > pos-1 {
|
||||
return i.index[pos-1], true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (i *Migrations) Next(version uint) (nextVersion uint, ok bool) {
|
||||
pos := i.findPos(version)
|
||||
if pos >= 0 && len(i.index) > pos+1 {
|
||||
return i.index[pos+1], true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (i *Migrations) Up(version uint) (m *Migration, ok bool) {
|
||||
if _, ok := i.migrations[version]; ok {
|
||||
if mx, ok := i.migrations[version][Up]; ok {
|
||||
return mx, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (i *Migrations) Down(version uint) (m *Migration, ok bool) {
|
||||
if _, ok := i.migrations[version]; ok {
|
||||
if mx, ok := i.migrations[version][Down]; ok {
|
||||
return mx, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (i *Migrations) findPos(version uint) int {
|
||||
if len(i.index) > 0 {
|
||||
ix := i.index.Search(version)
|
||||
if ix < len(i.index) && i.index[ix] == version {
|
||||
return ix
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type uintSlice []uint
|
||||
|
||||
func (s uintSlice) Search(x uint) int {
|
||||
return sort.Search(len(s), func(i int) bool { return s[i] >= x })
|
||||
}
|
||||
40
vendor/github.com/golang-migrate/migrate/v4/source/parse.go
generated
vendored
Normal file
40
vendor/github.com/golang-migrate/migrate/v4/source/parse.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrParse = fmt.Errorf("no match")
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultParse = Parse
|
||||
DefaultRegex = Regex
|
||||
)
|
||||
|
||||
// Regex matches the following pattern:
|
||||
//
|
||||
// 123_name.up.ext
|
||||
// 123_name.down.ext
|
||||
var Regex = regexp.MustCompile(`^([0-9]+)_(.*)\.(` + string(Down) + `|` + string(Up) + `)\.(.*)$`)
|
||||
|
||||
// Parse returns Migration for matching Regex pattern.
|
||||
func Parse(raw string) (*Migration, error) {
|
||||
m := Regex.FindStringSubmatch(raw)
|
||||
if len(m) == 5 {
|
||||
versionUint64, err := strconv.ParseUint(m[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Migration{
|
||||
Version: uint(versionUint64),
|
||||
Identifier: m[2],
|
||||
Direction: Direction(m[3]),
|
||||
Raw: raw,
|
||||
}, nil
|
||||
}
|
||||
return nil, ErrParse
|
||||
}
|
||||
61
vendor/github.com/golang-migrate/migrate/v4/util.go
generated
vendored
Normal file
61
vendor/github.com/golang-migrate/migrate/v4/util.go
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
nurl "net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MultiError holds multiple errors.
|
||||
//
|
||||
// Deprecated: Use github.com/hashicorp/go-multierror instead
|
||||
type MultiError struct {
|
||||
Errs []error
|
||||
}
|
||||
|
||||
// NewMultiError returns an error type holding multiple errors.
|
||||
//
|
||||
// Deprecated: Use github.com/hashicorp/go-multierror instead
|
||||
func NewMultiError(errs ...error) MultiError {
|
||||
compactErrs := make([]error, 0)
|
||||
for _, e := range errs {
|
||||
if e != nil {
|
||||
compactErrs = append(compactErrs, e)
|
||||
}
|
||||
}
|
||||
return MultiError{compactErrs}
|
||||
}
|
||||
|
||||
// Error implements error. Multiple errors are concatenated with 'and's.
|
||||
func (m MultiError) Error() string {
|
||||
var strs = make([]string, 0)
|
||||
for _, e := range m.Errs {
|
||||
if len(e.Error()) > 0 {
|
||||
strs = append(strs, e.Error())
|
||||
}
|
||||
}
|
||||
return strings.Join(strs, " and ")
|
||||
}
|
||||
|
||||
// suint safely converts int to uint
|
||||
// see https://goo.gl/wEcqof
|
||||
// see https://goo.gl/pai7Dr
|
||||
func suint(n int) uint {
|
||||
if n < 0 {
|
||||
panic(fmt.Sprintf("suint(%v) expects input >= 0", n))
|
||||
}
|
||||
return uint(n)
|
||||
}
|
||||
|
||||
// FilterCustomQuery filters all query values starting with `x-`
|
||||
func FilterCustomQuery(u *nurl.URL) *nurl.URL {
|
||||
ux := *u
|
||||
vx := make(nurl.Values)
|
||||
for k, v := range ux.Query() {
|
||||
if len(k) <= 1 || k[0:2] != "x-" {
|
||||
vx[k] = v
|
||||
}
|
||||
}
|
||||
ux.RawQuery = vx.Encode()
|
||||
return &ux
|
||||
}
|
||||
Reference in New Issue
Block a user