Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f90c751b10 | ||
|
|
c7a0a74aef | ||
|
|
50117bac35 | ||
|
|
d9c67dc417 | ||
|
|
a5075e2de1 | ||
|
|
9477979a55 | ||
|
|
f4abb4bba7 | ||
|
|
3461b6bfd7 | ||
|
|
602c5edb72 | ||
|
|
a7da767fa3 | ||
|
|
b55e3ea06d | ||
|
|
2bd822b3aa | ||
|
|
460d57267a | ||
|
|
1e4b96537d | ||
|
|
837dda3cc9 | ||
|
|
1620c56290 | ||
|
|
257fa847d6 | ||
|
|
001b70ac7d | ||
|
|
89e308c340 | ||
|
|
fed8fe9f0a | ||
|
|
472ef2e75c | ||
|
|
8d7625987a | ||
|
|
9f9df7c897 | ||
|
|
a371c8ea2b | ||
|
|
43cc3b3ea5 | ||
|
|
f939a89925 | ||
|
|
6dd86e0ded | ||
|
|
70e6d6d574 | ||
|
|
a51d0da644 | ||
|
|
3f583f0b61 | ||
|
|
ce3cb727b6 | ||
|
|
1ee8337800 | ||
|
|
952d08e30b | ||
|
|
39ab04d659 | ||
|
|
8b9c5f7712 | ||
|
|
b4ec9bccb1 | ||
|
|
ed4bdd4614 | ||
|
|
8945e853c4 | ||
|
|
7108d806dd | ||
|
|
682016f04c | ||
|
|
f3e6b5a737 | ||
|
|
0a17402aa2 | ||
|
|
5eee0c8030 | ||
|
|
30690d7df3 | ||
|
|
642a24d61b | ||
|
|
5300864bba | ||
|
|
278833935c | ||
|
|
6b9f24bf7f | ||
|
|
9023d66a0b | ||
|
|
10494908af | ||
|
|
033835ac1b | ||
|
|
329152ed62 | ||
|
|
34c2b4ad9c | ||
|
|
a3b7633246 | ||
|
|
5b97c921ed | ||
|
|
3c3ce91f29 | ||
|
|
f37ef795db | ||
|
|
1af841d787 | ||
|
|
7269490309 | ||
|
|
fccd197e40 | ||
|
|
e6d84f6f80 | ||
|
|
146bfa05b3 | ||
|
|
604248c97e | ||
|
|
a4d0a1b8d9 | ||
|
|
60d73be14e | ||
|
|
36d17ba2ec | ||
|
|
24a6476044 | ||
|
|
a8680333c3 | ||
|
|
d477553dcd | ||
|
|
e145cded4a | ||
|
|
7651c8a37e | ||
|
|
e5a152b9f6 | ||
|
|
12c619ee47 | ||
|
|
bc06922b4d | ||
|
|
8b091919d4 | ||
|
|
c5137c417c | ||
|
|
c378583d78 | ||
|
|
e22d0128cc | ||
|
|
555501f7cd | ||
|
|
d5eb594426 | ||
|
|
67c71f9a9d | ||
|
|
1e02b42a7a | ||
|
|
5b6f623dec | ||
|
|
ddd7fa61da | ||
|
|
7e8f6be051 | ||
|
|
65a3bd56cc | ||
|
|
e40e64c4ac | ||
|
|
6ceb5a9467 | ||
|
|
0444ec8531 | ||
|
|
f4d986b9d9 | ||
|
|
ee93fd4aff | ||
|
|
d28e549a68 | ||
|
|
897be38496 | ||
|
|
1b1c6f6d14 | ||
|
|
f44379d6f1 | ||
|
|
e1a7e31b1d | ||
|
|
899a244399 | ||
|
|
41e389a078 | ||
|
|
2477f639fd | ||
|
|
1585b870f8 | ||
|
|
b16220c938 | ||
|
|
34594af0ab | ||
|
|
1735cb3920 | ||
|
|
cabb2db836 | ||
|
|
da72428fa0 | ||
|
|
c81eaf7918 | ||
|
|
04b36ebaac | ||
|
|
45e4f1ef2f | ||
|
|
859d92bd77 | ||
|
|
e428ecebea | ||
|
|
0c456c49a6 | ||
|
|
f100226758 | ||
|
|
c52377528f | ||
|
|
a78d1abfa8 | ||
|
|
837776f0c7 | ||
|
|
128b6509cc | ||
|
|
2e0872f3e8 | ||
|
|
d1df97b2d1 | ||
|
|
1a002d0e9e | ||
|
|
2c5df8742b | ||
|
|
f4950c144e | ||
|
|
b1d02e222f | ||
|
|
49cac86ab2 | ||
|
|
06614d9cf5 | ||
|
|
1b707a76f4 | ||
|
|
2884a8e851 | ||
|
|
4bc6777f19 | ||
|
|
9d70a397e4 | ||
|
|
e9133367d7 | ||
|
|
87ba13c398 | ||
|
|
ff8a961068 | ||
|
|
7f85f9cf64 | ||
|
|
a5dc5d1efc | ||
|
|
94b8fa5946 | ||
|
|
0d41589774 | ||
|
|
a860f0cbd4 | ||
|
|
516f038f30 | ||
|
|
0350a00606 | ||
|
|
814769373f | ||
|
|
0815e2d770 | ||
|
|
5aa4670cbb | ||
|
|
d63a5c2c28 | ||
|
|
cd17c5a808 | ||
|
|
f2c4b52679 | ||
|
|
c7c50118a0 | ||
|
|
9c551d4398 | ||
|
|
091ad5db99 | ||
|
|
ab24e7674d | ||
|
|
e8edcdce66 | ||
|
|
691f687eb8 | ||
|
|
eb64ffa1ad | ||
|
|
a94396c3a8 | ||
|
|
0ba6fc3166 | ||
|
|
20636841a9 | ||
|
|
0695426fbf | ||
|
|
1d1cd48953 | ||
|
|
76efa726ed | ||
|
|
90c5015954 | ||
|
|
5026488530 | ||
|
|
fb224367d7 | ||
|
|
47a2661dd4 | ||
|
|
b39ee92ed9 | ||
|
|
c3ebd527d8 | ||
|
|
f375aebc5c | ||
|
|
834fa39423 | ||
|
|
bfedabbc35 | ||
|
|
7d03609443 | ||
|
|
5ded96d97d | ||
|
|
b567287637 | ||
|
|
4078ef8941 | ||
|
|
7eac9190d4 | ||
|
|
669437c3b8 | ||
|
|
cf03803fe0 | ||
|
|
5163ac7824 | ||
|
|
ee8a8e50a7 | ||
|
|
72957b6966 | ||
|
|
12968a75af | ||
|
|
bead4a952b | ||
|
|
129922aae3 | ||
|
|
f2e0b335d7 | ||
|
|
876a13d423 | ||
|
|
f8afa5a6f6 | ||
|
|
45d23ed92a | ||
|
|
856ea12df9 | ||
|
|
208ac532e8 | ||
|
|
bad30b549d | ||
|
|
7a72550614 | ||
|
|
691bc773cc | ||
|
|
383386b35f | ||
|
|
315a205b59 | ||
|
|
23d8d33f57 | ||
|
|
92dec355ef | ||
|
|
f9c067b3e0 | ||
|
|
0b3b628703 | ||
|
|
c62dbbacc2 | ||
|
|
8d96f81de6 | ||
|
|
ea8bebc7ef | ||
|
|
1020682de5 | ||
|
|
d72e5c6096 | ||
|
|
84009cf2ab | ||
|
|
ddaf669dc4 | ||
|
|
e968557bc6 | ||
|
|
5be1166fa3 | ||
|
|
b94ed1a710 | ||
|
|
c34675fa54 | ||
|
|
2c61c69199 | ||
|
|
586c129bae | ||
|
|
2111d2f93e | ||
|
|
b63a0d4880 | ||
|
|
3b02b182ee | ||
|
|
8ec04221b4 | ||
|
|
8d1322e09a | ||
|
|
1d693e8836 | ||
|
|
4831e0ed50 | ||
|
|
30b6b9aeac | ||
|
|
952f3f046e | ||
|
|
e497646d9c | ||
|
|
179dba434c | ||
|
|
f87b567859 | ||
|
|
7cacfc2080 | ||
|
|
448a9b1819 | ||
|
|
e7af02e753 | ||
|
|
e5d8970cd0 | ||
|
|
fd42ff1729 | ||
|
|
88d30a4217 | ||
|
|
64755d0b20 | ||
|
|
90a3ac4f87 | ||
|
|
50112e7650 | ||
|
|
dc26c41ac9 | ||
|
|
145baa187d | ||
|
|
eb59b750e8 | ||
|
|
c9ff76f294 | ||
|
|
b63177a7da | ||
|
|
839421e733 | ||
|
|
9fe7383564 | ||
|
|
90273fe0aa | ||
|
|
a7f9122726 | ||
|
|
e7e707727d | ||
|
|
31791adbc0 | ||
|
|
41f3495935 | ||
|
|
8324b455c0 | ||
|
|
c367ed2f12 | ||
|
|
5ff4dc804a | ||
|
|
4ca6c06627 | ||
|
|
865751e1b3 | ||
|
|
03e74b9289 | ||
|
|
a2cd4036be | ||
|
|
58eb755472 | ||
|
|
30411f49a1 | ||
|
|
723c60fe0d | ||
|
|
1d0b934641 | ||
|
|
0d539f4cae | ||
|
|
37a9174b9d | ||
|
|
30deade574 | ||
|
|
2d1de76527 | ||
|
|
d5be909ccd | ||
|
|
2fdf921b1e | ||
|
|
405ef9df07 | ||
|
|
856f9c4192 | ||
|
|
79c741fa73 | ||
|
|
ca03021154 | ||
|
|
5eb1604704 | ||
|
|
b74509abe2 | ||
|
|
9a407b6083 | ||
|
|
5582a2083b | ||
|
|
bf926fa253 | ||
|
|
9f21d9ca68 | ||
|
|
76d03050d2 | ||
|
|
4bd83668eb | ||
|
|
8c64ac7ccd | ||
|
|
03613f14ac | ||
|
|
162fc2bb3d | ||
|
|
c47c8cc9d9 | ||
|
|
3bba76f90a | ||
|
|
0d585e2a56 | ||
|
|
eebc4c437a | ||
|
|
fca5869487 | ||
|
|
af0c073f52 | ||
|
|
edff41a91c | ||
|
|
5dc2e47f4e | ||
|
|
89836f4f11 | ||
|
|
3843f9233f | ||
|
|
185a4ded0e | ||
|
|
b2be9e7b62 | ||
|
|
885d03c4af | ||
|
|
acc11eda6d | ||
|
|
b5e3e92f00 | ||
|
|
075d6e8130 | ||
|
|
bcd0d884bf |
@@ -25,7 +25,7 @@ e.g. s3, github, go-bindata, gcs, file
|
||||
Obtained by running: `migrate -help`
|
||||
|
||||
**Loaded Database Drivers**
|
||||
e.g. spanner, stub, clickhouse, cockroachdb, crdb-postgres, postgres, postgresql, pgx, redshift, cassandra, cockroach, mysql
|
||||
e.g. spanner, stub, datastore, cockroachdb, crdb-postgres, postgres, postgresql, pgx, redshift, cassandra, cockroach, mysql
|
||||
Obtained by running: `migrate -help`
|
||||
|
||||
**Go Version**
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="migrate">
|
||||
<rect width="1280" height="640" fill="#0A0A0A"/>
|
||||
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
|
||||
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">migrate</text>
|
||||
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">Database migrations. CLI and Golang library.</text>
|
||||
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
|
||||
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
|
||||
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
+32
-18
@@ -6,22 +6,27 @@ on:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: lint
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.26.4.x"
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.6.2
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.16.x", "1.17.x"]
|
||||
go: ["1.24.x", "1.25.x"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
@@ -38,7 +43,7 @@ jobs:
|
||||
check-coverage:
|
||||
name: Check coverage
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
@@ -47,7 +52,7 @@ jobs:
|
||||
goreleaser:
|
||||
name: Release a new version
|
||||
needs: [lint, test]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
environment: GoReleaser
|
||||
# This job only runs when
|
||||
# 1. When the previous `lint` and `test` jobs has completed successfully
|
||||
@@ -55,19 +60,19 @@ jobs:
|
||||
# 3. When the workflow is triggered by a tag with `v` prefix
|
||||
if: ${{ success() && github.repository == 'golang-migrate/migrate' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.17.x"
|
||||
go-version: "1.26.4.x"
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/login-action@v1
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: golangmigrate
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -75,10 +80,10 @@ jobs:
|
||||
- run: echo "SOURCE=$(make echo-source)" >> $GITHUB_ENV
|
||||
- run: echo "DATABASE=$(make echo-database)" >> $GITHUB_ENV
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v2
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -89,9 +94,18 @@ jobs:
|
||||
- run: package_cloud push golang-migrate/migrate/ubuntu/focal dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/ubuntu/jammy dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/ubuntu/noble dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/debian/buster dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/debian/bullseye dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/debian/bookworm dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26.4'
|
||||
|
||||
- name: Build CLI binaries
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
mkdir -p cli/build
|
||||
|
||||
for pair in "linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64"; do
|
||||
OS=${pair%/*}
|
||||
ARCH=${pair#*/}
|
||||
OUTPUT="cli/build/migrate.${OS}-${ARCH}"
|
||||
echo "Building $OUTPUT..."
|
||||
CGO_ENABLED=0 GOOS=$OS GOARCH=$ARCH \
|
||||
go build -a -o "$OUTPUT" \
|
||||
-ldflags="-s -w -X main.Version=$VERSION" \
|
||||
-tags 'datastore file' \
|
||||
./cmd/migrate
|
||||
ln -f "$OUTPUT" cli/build/migrate
|
||||
tar czf "${OUTPUT}.tar.gz" -C cli/build migrate
|
||||
done
|
||||
|
||||
cd cli/build && sha256sum *.tar.gz > sha256sum.txt
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} \
|
||||
cli/build/*.tar.gz \
|
||||
cli/build/sha256sum.txt \
|
||||
--title "${{ github.ref_name }}" \
|
||||
--generate-notes \
|
||||
--latest
|
||||
@@ -8,3 +8,4 @@ vendor/
|
||||
.vscode/
|
||||
.idea
|
||||
dist/
|
||||
migrate
|
||||
|
||||
+29
-18
@@ -1,26 +1,37 @@
|
||||
run:
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
timeout: 5m
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
#- golint
|
||||
- interfacer
|
||||
- unconvert
|
||||
#- dupl
|
||||
- goconst
|
||||
- gofmt
|
||||
- misspell
|
||||
- unparam
|
||||
- nakedret
|
||||
- prealloc
|
||||
#- gosec
|
||||
linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
- revive
|
||||
- unconvert
|
||||
- unparam
|
||||
settings:
|
||||
misspell:
|
||||
locale: US
|
||||
revive:
|
||||
rules:
|
||||
- name: redundant-build-tag
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- path: (.+)\.go$
|
||||
text: G104
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-same-issues: 0
|
||||
max-issues-per-linter: 0
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
# gosec: Duplicated errcheck checks
|
||||
- G104
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.16-alpine3.13 AS builder
|
||||
FROM golang:1.26.4-alpine3.21 AS builder
|
||||
ARG VERSION
|
||||
|
||||
RUN apk add --no-cache git gcc musl-dev make
|
||||
@@ -15,7 +15,7 @@ COPY . ./
|
||||
|
||||
RUN make build-docker
|
||||
|
||||
FROM alpine:3.13
|
||||
FROM alpine:3.21
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.13
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
@@ -8,4 +8,4 @@ RUN ln -s /usr/local/bin/migrate /usr/bin/migrate
|
||||
RUN ln -s /usr/local/bin/migrate /migrate
|
||||
|
||||
ENTRYPOINT ["migrate"]
|
||||
CMD ["--help"]
|
||||
CMD ["--help"]
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
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 respository though.
|
||||
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
|
||||
@@ -65,7 +65,7 @@
|
||||
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
|
||||
Database-specific locking features are used by *some* database drivers to prevent multiple instances of migrate from running migrations on
|
||||
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.
|
||||
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
# 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)
|
||||
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:
|
||||
@@ -10,7 +10,7 @@ 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 his migration later gets it merged to the repository first.
|
||||
**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.
|
||||
|
||||
@@ -28,9 +28,9 @@ 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 commiting your migrations you should run your migrations up, down, and then up again to see if migrations are working properly both ways.
|
||||
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 in the end of this document.
|
||||
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.
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Hanzo Golang Migrate
|
||||
|
||||
## Overview
|
||||
Go module: github.com/golang-migrate/migrate/v4
|
||||
|
||||
## Tech Stack
|
||||
- **Language**: Go
|
||||
|
||||
## Build & Run
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Structure
|
||||
```
|
||||
golang-migrate/
|
||||
CONTRIBUTING.md
|
||||
Dockerfile
|
||||
Dockerfile.circleci
|
||||
Dockerfile.github-actions
|
||||
FAQ.md
|
||||
GETTING_STARTED.md
|
||||
LICENSE
|
||||
MIGRATIONS.md
|
||||
Makefile
|
||||
README.md
|
||||
SECURITY.md
|
||||
cli/
|
||||
cmd/
|
||||
database/
|
||||
dktesting/
|
||||
```
|
||||
|
||||
## Key Files
|
||||
- `README.md` -- Project documentation
|
||||
- `go.mod` -- Go module definition
|
||||
- `Makefile` -- Build automation
|
||||
- `Dockerfile` -- Container build
|
||||
@@ -1,5 +1,5 @@
|
||||
SOURCE ?= file go_bindata github github_ee bitbucket aws_s3 google_cloud_storage godoc_vfs gitlab
|
||||
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb sqlserver firebird neo4j pgx
|
||||
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb yugabytedb datastore 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 ?=
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
[](https://github.com/golang-migrate/migrate/actions/workflows/ci.yaml?query=branch%3Amaster)
|
||||
<p align="center"><img src=".github/hero.svg" alt="migrate" width="880"></p>
|
||||
|
||||
[](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)
|
||||
[](https://goreportcard.com/report/github.com/golang-migrate/migrate/v4)
|
||||
|
||||
# migrate
|
||||
|
||||
@@ -24,23 +26,26 @@ Forked from [mattes/migrate](https://github.com/mattes/migrate)
|
||||
Database drivers run migrations. [Add a new database?](database/driver.go)
|
||||
|
||||
* [PostgreSQL](database/postgres)
|
||||
* [PGX](database/pgx)
|
||||
* [PGX v4](database/pgx)
|
||||
* [PGX v5](database/pgx/v5)
|
||||
* [Redshift](database/redshift)
|
||||
* [Ql](database/ql)
|
||||
* [Cassandra](database/cassandra)
|
||||
* [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)
|
||||
* [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)
|
||||
* [ClickHouse](database/clickhouse)
|
||||
* [YugabyteDB](database/yugabytedb)
|
||||
* [Datastore](database/datastore)
|
||||
* [Firebird](database/firebird)
|
||||
* [MS SQL Server](database/sqlserver)
|
||||
* [rqlite](database/rqlite)
|
||||
|
||||
### Database URLs
|
||||
|
||||
@@ -84,7 +89,7 @@ Source drivers read migrations from local or remote sources. [Add a new source?]
|
||||
* Handles ctrl+c (SIGINT) gracefully.
|
||||
* No config search paths, no config files, no magic ENV var injections.
|
||||
|
||||
__[CLI Documentation](cmd/migrate)__
|
||||
[CLI Documentation](cmd/migrate) (includes CLI install instructions)
|
||||
|
||||
### Basic usage
|
||||
|
||||
@@ -108,7 +113,7 @@ $ docker run -v {{ migration dir }}:/migrations --network host migrate/migrate
|
||||
* Uses `io.Reader` streams internally for low memory overhead.
|
||||
* Thread-safe and no goroutine leaks.
|
||||
|
||||
__[Go Documentation](https://godoc.org/github.com/golang-migrate/migrate)__
|
||||
__[Go Documentation](https://pkg.go.dev/github.com/golang-migrate/migrate/v4)__
|
||||
|
||||
```go
|
||||
import (
|
||||
@@ -142,7 +147,7 @@ func main() {
|
||||
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
|
||||
m.Up() // or m.Steps(2) if you want to explicitly set the number of migrations to run
|
||||
}
|
||||
```
|
||||
|
||||
@@ -168,6 +173,11 @@ Each migration has an up and down migration. [Why?](FAQ.md#why-two-separate-file
|
||||
|
||||
[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 affiliated or supported by this project*
|
||||
|
||||
## Versions
|
||||
|
||||
Version | Supported? | Import | Notes
|
||||
|
||||
+16
@@ -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.
|
||||
+15
-11
@@ -7,7 +7,7 @@
|
||||
[Release Downloads](https://github.com/golang-migrate/migrate/releases)
|
||||
|
||||
```bash
|
||||
$ curl -L https://github.com/golang-migrate/migrate/releases/download/$version/migrate.$platform-amd64.tar.gz | tar xvz
|
||||
$ curl -L https://github.com/golang-migrate/migrate/releases/download/$version/migrate.$os-$arch.tar.gz | tar xvz
|
||||
```
|
||||
|
||||
### MacOS
|
||||
@@ -27,8 +27,8 @@ $ scoop install migrate
|
||||
### Linux (*.deb package)
|
||||
|
||||
```bash
|
||||
$ curl -L https://packagecloud.io/golang-migrate/migrate/gpgkey | apt-key add -
|
||||
$ echo "deb https://packagecloud.io/golang-migrate/migrate/ubuntu/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/migrate.list
|
||||
$ curl -fsSL https://packagecloud.io/golang-migrate/migrate/gpgkey | sudo gpg --dearmor -o /etc/apt/keyrings/migrate.gpg
|
||||
$ echo "deb [signed-by=/etc/apt/keyrings/migrate.gpg] https://packagecloud.io/golang-migrate/migrate/ubuntu/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/migrate.list
|
||||
$ apt-get update
|
||||
$ apt-get install -y migrate
|
||||
```
|
||||
@@ -63,7 +63,7 @@ $ go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@l
|
||||
to build the cli for use with other databases, replace the `postgres` build tag
|
||||
with the appropriate database tag(s) for the databases desired. The tags
|
||||
correspond to the names of the sub-packages underneath the
|
||||
[`database`](../database) package.
|
||||
[`database`](../../database) package.
|
||||
1. Similarly to the database build tags, if you need to support other sources, use the appropriate build tag(s).
|
||||
1. Support for build constraints will be removed in the future: https://github.com/golang-migrate/migrate/issues/60
|
||||
1. For versions of Go 1.15 and lower, [make sure](https://github.com/golang-migrate/migrate/pull/257#issuecomment-705249902) you're not installing the `migrate` CLI from a module. e.g. there should not be any `go.mod` files in your current directory or any directory from your current directory to the root
|
||||
@@ -86,14 +86,18 @@ Options:
|
||||
-help Print usage
|
||||
|
||||
Commands:
|
||||
create [-ext E] [-dir D] [-seq] [-digits N] [-format] NAME
|
||||
Create a set of timestamped up/down migrations titled NAME, in directory D with extension E.
|
||||
Use -seq option to generate sequential up/down migrations with N digits.
|
||||
Use -format option to specify a Go time format string.
|
||||
create [-ext E] [-dir D] [-seq] [-digits N] [-format] [-tz] NAME
|
||||
Create a set of timestamped up/down migrations titled NAME, in directory D with extension E.
|
||||
Use -seq option to generate sequential up/down migrations with N digits.
|
||||
Use -format option to specify a Go time format string. Note: migrations with the same time cause "duplicate migration version" error.
|
||||
Use -tz option to specify the timezone that will be used when generating non-sequential migrations (defaults: UTC).
|
||||
|
||||
goto V Migrate to version V
|
||||
up [N] Apply all or N up migrations
|
||||
down [N] Apply all or N down migrations
|
||||
drop Drop everything inside database
|
||||
down [N] [-all] Apply all or N down migrations
|
||||
Use -all to apply all down migrations
|
||||
drop [-f] Drop everything inside database
|
||||
Use -f to bypass confirmation
|
||||
force V Set version V but don't run migration (ignores dirty state)
|
||||
version Print current migration version
|
||||
```
|
||||
@@ -127,7 +131,7 @@ $ migrate -database "$MY_MIGRATE_DATABASE"
|
||||
Check out https://stedolan.github.io/jq/
|
||||
|
||||
```bash
|
||||
$ migrate -database "$(cat config.json | jq '.database')"
|
||||
$ migrate -database "$(cat config.json | jq -r '.database')"
|
||||
```
|
||||
|
||||
### YAML files
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
# Cassandra
|
||||
# Cassandra / ScyllaDB
|
||||
|
||||
* Drop command will not work on Cassandra 2.X because it rely on
|
||||
* `Drop()` method will not work on Cassandra 2.X because it rely on
|
||||
system_schema table which comes with 3.X
|
||||
* Other commands should work properly but are **not tested**
|
||||
* Other methods should work properly but are **not tested**
|
||||
* The Cassandra driver (gocql) does not natively support executing multiple statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats:
|
||||
* This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon.
|
||||
* The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations.
|
||||
|
||||
**ScyllaDB**
|
||||
|
||||
* No additional configuration is required since it is a drop-in replacement for Cassandra.
|
||||
* The `Drop()` method` works for ScyllaDB 5.1
|
||||
|
||||
|
||||
## Usage
|
||||
`cassandra://host:port/keyspace?param1=value¶m2=value2`
|
||||
|
||||
@@ -3,18 +3,16 @@ package cassandra
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/golang-migrate/migrate/v4/database/multistmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -199,14 +197,14 @@ func (c *Cassandra) Close() error {
|
||||
}
|
||||
|
||||
func (c *Cassandra) Lock() error {
|
||||
if !c.isLocked.CAS(false, true) {
|
||||
if !c.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cassandra) Unlock() error {
|
||||
if !c.isLocked.CAS(true, false) {
|
||||
if !c.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
@@ -231,7 +229,7 @@ func (c *Cassandra) Run(migration io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -316,11 +314,7 @@ func (c *Cassandra) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := c.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -342,7 +336,7 @@ func parseConsistency(consistencyStr string) (consistency gocql.Consistency, err
|
||||
var ok bool
|
||||
err, ok = r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("Failed to parse consistency \"%s\": %v", consistencyStr, r)
|
||||
err = fmt.Errorf("failed to parse consistency \"%s\": %v", consistencyStr, r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -25,9 +25,11 @@ var (
|
||||
// Although Cassandra 2.x is supported by the Apache Foundation,
|
||||
// the migrate db driver only supports Cassandra 3.x since it uses
|
||||
// the system_schema keyspace.
|
||||
// last ScyllaDB version tested is 5.1.11
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "cassandra:3.0", Options: opts},
|
||||
{ImageName: "cassandra:3.11", Options: opts},
|
||||
{ImageName: "scylladb/scylla:5.1.11", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -59,6 +61,20 @@ func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Run("test", test)
|
||||
t.Run("testMigrate", testMigrate)
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, spec := range specs {
|
||||
t.Log("Cleaning up ", spec.ImageName)
|
||||
if err := spec.Cleanup(); err != nil {
|
||||
t.Error("Error removing ", spec.ImageName, "error:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(9042)
|
||||
if err != nil {
|
||||
@@ -79,7 +95,7 @@ func Test(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
func testMigrate(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(9042)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# ClickHouse
|
||||
|
||||
`clickhouse://username:password@host:port/database=clicks?x-multi-statement=true`
|
||||
|
||||
| URL Query | Description |
|
||||
|------------|-------------|
|
||||
| `x-migrations-table`| Name of the migrations table |
|
||||
| `x-migrations-table-engine`| Engine to use for the migrations table, defaults to TinyLog |
|
||||
| `x-cluster-name` | Name of cluster for creating `schema_migrations` table cluster wide |
|
||||
| `database` | The name of the database to connect to |
|
||||
| `username` | The user to sign in as |
|
||||
| `password` | The user's password |
|
||||
| `host` | The host to connect to. |
|
||||
| `port` | The port to bind to. |
|
||||
| `x-multi-statement` | false | Enable multiple statements to be ran in a single migration (See note below) |
|
||||
|
||||
## Notes
|
||||
|
||||
* The Clickhouse driver does not natively support executing multipe statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats:
|
||||
* This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon.
|
||||
* The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations.
|
||||
* Using the default TinyLog table engine for the schema_versions table prevents backing up the table if using the [clickhouse-backup](https://github.com/AlexAkulov/clickhouse-backup) tool. If backing up the database with make sure the migrations are run with `x-migrations-table-engine=MergeTree`.
|
||||
* Clickhouse cluster mode is not officially supported, since it's not tested right now, but you can try enabling `schema_migrations` table replication by specifying a `x-cluster-name`:
|
||||
* When `x-cluster-name` is specified, `x-migrations-table-engine` also should be specified. See the docs regarding [replicated table engines](https://clickhouse.tech/docs/en/engines/table-engines/mergetree-family/replication/#table_engines-replication).
|
||||
* When `x-cluster-name` is specified, only the `schema_migrations` table is replicated across the cluster. You still need to write your migrations so that the application tables are replicated within the cluster.
|
||||
@@ -15,7 +15,7 @@ CREATE USER IF NOT EXISTS cockroach;
|
||||
GRANT ALL ON DATABASE example TO cockroach;
|
||||
```
|
||||
|
||||
When using Migrate CLI we need to pass to database URL. Let's export it to a variable for convienience:
|
||||
When using Migrate CLI we need to pass to database URL. Let's export it to a variable for convenience:
|
||||
```
|
||||
export COCKROACHDB_URL='cockroachdb://cockroach:@localhost:26257/example?sslmode=disable'
|
||||
```
|
||||
@@ -50,7 +50,7 @@ And in the `.down.sql` let's delete it:
|
||||
```
|
||||
DROP TABLE IF EXISTS example.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)
|
||||
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
|
||||
```
|
||||
@@ -139,4 +139,4 @@ func main() {
|
||||
}
|
||||
}
|
||||
```
|
||||
You can find details [here](README.md#use-in-your-go-project)
|
||||
You can find details [here](README.md#use-in-your-go-project)
|
||||
|
||||
@@ -3,24 +3,18 @@ package cockroachdb
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
"sync/atomic"
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/cockroach-go/v2/crdb"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -167,7 +161,7 @@ func (c *CockroachDb) Lock() error {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := rows.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -217,7 +211,7 @@ func (c *CockroachDb) Unlock() error {
|
||||
}
|
||||
|
||||
func (c *CockroachDb) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -282,7 +276,7 @@ func (c *CockroachDb) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -324,11 +318,7 @@ func (c *CockroachDb) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := c.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@ import (
|
||||
const defaultPort = 26257
|
||||
|
||||
var (
|
||||
opts = dktest.Options{Cmd: []string{"start", "--insecure"}, PortRequired: true, ReadyFunc: isReady}
|
||||
// Released versions: https://www.cockroachlabs.com/docs/releases/
|
||||
opts = dktest.Options{Cmd: []string{"start-single-node", "--insecure"}, PortRequired: true, ReadyFunc: isReady}
|
||||
// Supported versions: https://www.cockroachlabs.com/docs/releases/release-support-policy#supported-versions
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "cockroachdb/cockroach:v1.0.7", Options: opts},
|
||||
{ImageName: "cockroachdb/cockroach:v1.1.9", Options: opts},
|
||||
{ImageName: "cockroachdb/cockroach:v2.0.7", Options: opts},
|
||||
{ImageName: "cockroachdb/cockroach:v2.1.3", Options: opts},
|
||||
{ImageName: "cockroachdb/cockroach:latest-v24.3", Options: opts},
|
||||
{ImageName: "cockroachdb/cockroach:latest-v24.1", Options: opts},
|
||||
{ImageName: "cockroachdb/cockroach:latest-v23.2", Options: opts},
|
||||
{ImageName: "cockroachdb/cockroach:latest-v23.1", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Datastore
|
||||
|
||||
`datastore://host:port?username=user&password=password&database=clicks&x-multi-statement=true`
|
||||
|
||||
Hanzo Datastore is a ClickHouse-wire-compatible database engine. This driver
|
||||
connects through the native `github.com/hanzo-ds/go` driver, which registers the
|
||||
`database/sql` driver name `datastore`.
|
||||
|
||||
| URL Query | Description |
|
||||
|------------|-------------|
|
||||
| `x-migrations-table`| Name of the migrations table |
|
||||
| `x-migrations-table-engine`| Engine to use for the migrations table, defaults to TinyLog |
|
||||
| `x-cluster-name` | Name of cluster for creating `schema_migrations` table cluster wide |
|
||||
| `x-multi-statement` | Enable multiple statements to be ran in a single migration (See note below) |
|
||||
| `x-multi-statement-max-size` | Maximum size of a single migration in bytes when `x-multi-statement` is set (defaults to 10 MB) |
|
||||
| `database` | The name of the database to connect to |
|
||||
| `username` | The user to sign in as |
|
||||
| `password` | The user's password |
|
||||
| `host` | The host to connect to. |
|
||||
| `port` | The port to bind to. |
|
||||
|
||||
## Notes
|
||||
|
||||
* The Datastore driver does not natively support executing multiple statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats:
|
||||
* This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon.
|
||||
* The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations.
|
||||
* Using the default TinyLog table engine for the `schema_migrations` table prevents backing up the table. If you need to back up the database, run the migrations with `x-migrations-table-engine=MergeTree`.
|
||||
* Datastore cluster mode is not officially supported, but you can try enabling `schema_migrations` table replication by specifying a `x-cluster-name`:
|
||||
* When `x-cluster-name` is specified, `x-migrations-table-engine` also should be specified.
|
||||
* When `x-cluster-name` is specified, only the `schema_migrations` table is replicated across the cluster. You still need to write your migrations so that the application tables are replicated within the cluster.
|
||||
* If you want to create a database inside a migration, note that the `schema_migrations` table will live in the `default` database, so you cannot use `USE <database_name>` inside a migration. In this case you may omit the database in the connection string (example [here](examples/migrations/003_create_database.up.sql)).
|
||||
@@ -1,26 +1,43 @@
|
||||
package clickhouse
|
||||
// Package datastore implements the golang-migrate database.Driver interface for
|
||||
// Hanzo Datastore (a ClickHouse-wire-compatible database engine). It registers
|
||||
// the "datastore://" URL scheme and connects through the native
|
||||
// github.com/hanzo-ds/go driver, which registers the database/sql driver name
|
||||
// "datastore".
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// import _ "github.com/golang-migrate/migrate/v4/database/datastore"
|
||||
//
|
||||
// Then use a URL like:
|
||||
//
|
||||
// datastore://host:9000?username=default&database=default&x-multi-statement=true
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"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"
|
||||
|
||||
// Registers the "datastore" database/sql driver (ClickHouse-wire native).
|
||||
_ "github.com/hanzo-ds/go"
|
||||
)
|
||||
|
||||
var (
|
||||
multiStmtDelimiter = []byte(";")
|
||||
|
||||
// DriverName is the database/sql driver registered by github.com/hanzo-ds/go.
|
||||
DriverName = "datastore"
|
||||
|
||||
DefaultMigrationsTable = "schema_migrations"
|
||||
DefaultMigrationsTableEngine = "TinyLog"
|
||||
DefaultMultiStatementMaxSize = 10 * 1 << 20 // 10 MB
|
||||
@@ -38,9 +55,11 @@ type Config struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
database.Register("clickhouse", &ClickHouse{})
|
||||
database.Register("datastore", &Datastore{})
|
||||
}
|
||||
|
||||
// WithInstance creates a Datastore driver from an existing *sql.DB opened
|
||||
// against the "datastore" driver.
|
||||
func WithInstance(conn *sql.DB, config *Config) (database.Driver, error) {
|
||||
if config == nil {
|
||||
return nil, ErrNilConfig
|
||||
@@ -50,32 +69,34 @@ func WithInstance(conn *sql.DB, config *Config) (database.Driver, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := &ClickHouse{
|
||||
ds := &Datastore{
|
||||
conn: conn,
|
||||
config: config,
|
||||
}
|
||||
|
||||
if err := ch.init(); err != nil {
|
||||
if err := ds.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
type ClickHouse struct {
|
||||
// Datastore is a golang-migrate database.Driver backed by the native
|
||||
// github.com/hanzo-ds/go ClickHouse-wire driver.
|
||||
type Datastore struct {
|
||||
conn *sql.DB
|
||||
config *Config
|
||||
isLocked atomic.Bool
|
||||
}
|
||||
|
||||
func (ch *ClickHouse) Open(dsn string) (database.Driver, error) {
|
||||
func (ds *Datastore) Open(dsn string) (database.Driver, error) {
|
||||
purl, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := migrate.FilterCustomQuery(purl)
|
||||
q.Scheme = "tcp"
|
||||
conn, err := sql.Open("clickhouse", q.String())
|
||||
q.Scheme = DriverName
|
||||
conn, err := sql.Open(DriverName, q.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -93,7 +114,7 @@ func (ch *ClickHouse) Open(dsn string) (database.Driver, error) {
|
||||
migrationsTableEngine = s
|
||||
}
|
||||
|
||||
ch = &ClickHouse{
|
||||
ds = &Datastore{
|
||||
conn: conn,
|
||||
config: &Config{
|
||||
MigrationsTable: purl.Query().Get("x-migrations-table"),
|
||||
@@ -105,44 +126,44 @@ func (ch *ClickHouse) Open(dsn string) (database.Driver, error) {
|
||||
},
|
||||
}
|
||||
|
||||
if err := ch.init(); err != nil {
|
||||
if err := ds.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func (ch *ClickHouse) init() error {
|
||||
if len(ch.config.DatabaseName) == 0 {
|
||||
if err := ch.conn.QueryRow("SELECT currentDatabase()").Scan(&ch.config.DatabaseName); err != nil {
|
||||
func (ds *Datastore) init() error {
|
||||
if len(ds.config.DatabaseName) == 0 {
|
||||
if err := ds.conn.QueryRow("SELECT currentDatabase()").Scan(&ds.config.DatabaseName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(ch.config.MigrationsTable) == 0 {
|
||||
ch.config.MigrationsTable = DefaultMigrationsTable
|
||||
if len(ds.config.MigrationsTable) == 0 {
|
||||
ds.config.MigrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
if ch.config.MultiStatementMaxSize <= 0 {
|
||||
ch.config.MultiStatementMaxSize = DefaultMultiStatementMaxSize
|
||||
if ds.config.MultiStatementMaxSize <= 0 {
|
||||
ds.config.MultiStatementMaxSize = DefaultMultiStatementMaxSize
|
||||
}
|
||||
|
||||
if len(ch.config.MigrationsTableEngine) == 0 {
|
||||
ch.config.MigrationsTableEngine = DefaultMigrationsTableEngine
|
||||
if len(ds.config.MigrationsTableEngine) == 0 {
|
||||
ds.config.MigrationsTableEngine = DefaultMigrationsTableEngine
|
||||
}
|
||||
|
||||
return ch.ensureVersionTable()
|
||||
return ds.ensureVersionTable()
|
||||
}
|
||||
|
||||
func (ch *ClickHouse) Run(r io.Reader) error {
|
||||
if ch.config.MultiStatementEnabled {
|
||||
func (ds *Datastore) Run(r io.Reader) error {
|
||||
if ds.config.MultiStatementEnabled {
|
||||
var err error
|
||||
if e := multistmt.Parse(r, multiStmtDelimiter, ch.config.MultiStatementMaxSize, func(m []byte) bool {
|
||||
if e := multistmt.Parse(r, multiStmtDelimiter, ds.config.MultiStatementMaxSize, func(m []byte) bool {
|
||||
tq := strings.TrimSpace(string(m))
|
||||
if tq == "" {
|
||||
return true
|
||||
}
|
||||
if _, e := ch.conn.Exec(string(m)); e != nil {
|
||||
if _, e := ds.conn.Exec(string(m)); e != nil {
|
||||
err = database.Error{OrigErr: e, Err: "migration failed", Query: m}
|
||||
return false
|
||||
}
|
||||
@@ -153,24 +174,25 @@ func (ch *ClickHouse) Run(r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
migration, err := ioutil.ReadAll(r)
|
||||
migration, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ch.conn.Exec(string(migration)); err != nil {
|
||||
if _, err := ds.conn.Exec(string(migration)); err != nil {
|
||||
return database.Error{OrigErr: err, Err: "migration failed", Query: migration}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (ch *ClickHouse) Version() (int, bool, error) {
|
||||
|
||||
func (ds *Datastore) Version() (int, bool, error) {
|
||||
var (
|
||||
version int
|
||||
dirty uint8
|
||||
query = "SELECT version, dirty FROM `" + ch.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1"
|
||||
query = "SELECT version, dirty FROM `" + ds.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1"
|
||||
)
|
||||
if err := ch.conn.QueryRow(query).Scan(&version, &dirty); err != nil {
|
||||
if err := ds.conn.QueryRow(query).Scan(&version, &dirty); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
@@ -179,7 +201,7 @@ func (ch *ClickHouse) Version() (int, bool, error) {
|
||||
return version, dirty == 1, nil
|
||||
}
|
||||
|
||||
func (ch *ClickHouse) SetVersion(version int, dirty bool) error {
|
||||
func (ds *Datastore) SetVersion(version int, dirty bool) error {
|
||||
var (
|
||||
bool = func(v bool) uint8 {
|
||||
if v {
|
||||
@@ -187,13 +209,13 @@ func (ch *ClickHouse) SetVersion(version int, dirty bool) error {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
tx, err = ch.conn.Begin()
|
||||
tx, err = ds.conn.Begin()
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := "INSERT INTO " + ch.config.MigrationsTable + " (version, dirty, sequence) VALUES (?, ?, ?)"
|
||||
query := "INSERT INTO " + ds.config.MigrationsTable + " (version, dirty, sequence) VALUES (?, ?, ?)"
|
||||
if _, err := tx.Exec(query, version, bool(dirty), time.Now().UnixNano()); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -203,28 +225,24 @@ func (ch *ClickHouse) SetVersion(version int, dirty bool) error {
|
||||
|
||||
// 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 ClickHouse type.
|
||||
func (ch *ClickHouse) ensureVersionTable() (err error) {
|
||||
if err = ch.Lock(); err != nil {
|
||||
// convention of "caller locks" in the Datastore type.
|
||||
func (ds *Datastore) ensureVersionTable() (err error) {
|
||||
if err = ds.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := ch.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
if e := ds.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
table string
|
||||
query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'"
|
||||
query = "SHOW TABLES FROM " + quoteIdentifier(ds.config.DatabaseName) + " LIKE '" + ds.config.MigrationsTable + "'"
|
||||
)
|
||||
// check if migration table exists
|
||||
if err := ch.conn.QueryRow(query).Scan(&table); err != nil {
|
||||
if err := ds.conn.QueryRow(query).Scan(&table); err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -233,42 +251,42 @@ func (ch *ClickHouse) ensureVersionTable() (err error) {
|
||||
}
|
||||
|
||||
// if not, create the empty migration table
|
||||
if len(ch.config.ClusterName) > 0 {
|
||||
if len(ds.config.ClusterName) > 0 {
|
||||
query = fmt.Sprintf(`
|
||||
CREATE TABLE %s ON CLUSTER %s (
|
||||
version Int64,
|
||||
dirty UInt8,
|
||||
sequence UInt64
|
||||
) Engine=%s`, ch.config.MigrationsTable, ch.config.ClusterName, ch.config.MigrationsTableEngine)
|
||||
) Engine=%s`, ds.config.MigrationsTable, ds.config.ClusterName, ds.config.MigrationsTableEngine)
|
||||
} else {
|
||||
query = fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
version Int64,
|
||||
dirty UInt8,
|
||||
sequence UInt64
|
||||
) Engine=%s`, ch.config.MigrationsTable, ch.config.MigrationsTableEngine)
|
||||
) Engine=%s`, ds.config.MigrationsTable, ds.config.MigrationsTableEngine)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(ch.config.MigrationsTableEngine, "Tree") {
|
||||
if strings.HasSuffix(ds.config.MigrationsTableEngine, "Tree") {
|
||||
query = fmt.Sprintf(`%s ORDER BY sequence`, query)
|
||||
}
|
||||
|
||||
if _, err := ch.conn.Exec(query); err != nil {
|
||||
if _, err := ds.conn.Exec(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ch *ClickHouse) Drop() (err error) {
|
||||
query := "SHOW TABLES FROM " + ch.config.DatabaseName
|
||||
tables, err := ch.conn.Query(query)
|
||||
func (ds *Datastore) Drop() (err error) {
|
||||
query := "SHOW TABLES FROM " + quoteIdentifier(ds.config.DatabaseName)
|
||||
tables, err := ds.conn.Query(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)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -278,9 +296,9 @@ func (ch *ClickHouse) Drop() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
query = "DROP TABLE IF EXISTS " + ch.config.DatabaseName + "." + table
|
||||
query = "DROP TABLE IF EXISTS " + quoteIdentifier(ds.config.DatabaseName) + "." + quoteIdentifier(table)
|
||||
|
||||
if _, err := ch.conn.Exec(query); err != nil {
|
||||
if _, err := ds.conn.Exec(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
}
|
||||
@@ -291,18 +309,29 @@ func (ch *ClickHouse) Drop() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ch *ClickHouse) Lock() error {
|
||||
if !ch.isLocked.CAS(false, true) {
|
||||
func (ds *Datastore) Lock() error {
|
||||
if !ds.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (ch *ClickHouse) Unlock() error {
|
||||
if !ch.isLocked.CAS(true, false) {
|
||||
|
||||
func (ds *Datastore) Unlock() error {
|
||||
if !ds.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (ch *ClickHouse) Close() error { return ch.conn.Close() }
|
||||
|
||||
func (ds *Datastore) Close() error { return ds.conn.Close() }
|
||||
|
||||
// Copied from lib/pq implementation: https://github.com/lib/pq/blob/v1.9.0/conn.go#L1611
|
||||
func quoteIdentifier(name string) string {
|
||||
end := strings.IndexRune(name, 0)
|
||||
if end > -1 {
|
||||
name = name[:end]
|
||||
}
|
||||
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package clickhouse_test
|
||||
package datastore_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,13 +8,15 @@ import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
_ "github.com/ClickHouse/clickhouse-go"
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/clickhouse"
|
||||
"github.com/golang-migrate/migrate/v4/database/datastore"
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
|
||||
// Registers the "datastore" database/sql driver (ClickHouse-wire native).
|
||||
_ "github.com/hanzo-ds/go"
|
||||
)
|
||||
|
||||
const defaultPort = 9000
|
||||
@@ -26,19 +28,19 @@ var (
|
||||
PortRequired: true, ReadyFunc: isReady,
|
||||
}
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "yandex/clickhouse-server:21.3", Options: opts},
|
||||
{ImageName: "clickhouse:24.8", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
func clickhouseConnectionString(host, port, engine string) string {
|
||||
func datastoreConnectionString(host, port, engine string) string {
|
||||
if engine != "" {
|
||||
return fmt.Sprintf(
|
||||
"clickhouse://%v:%v?username=user&password=password&database=db&x-multi-statement=true&x-migrations-table-engine=%v&debug=false",
|
||||
"datastore://%v:%v?username=user&password=password&database=db&x-multi-statement=true&x-migrations-table-engine=%v&debug=false",
|
||||
host, port, engine)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"clickhouse://%v:%v?username=user&password=password&database=db&x-multi-statement=true&debug=false",
|
||||
"datastore://%v:%v?username=user&password=password&database=db&x-multi-statement=true&debug=false",
|
||||
host, port)
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
db, err := sql.Open("clickhouse", clickhouseConnectionString(ip, port, ""))
|
||||
db, err := sql.Open("datastore", datastoreConnectionString(ip, port, ""))
|
||||
|
||||
if err != nil {
|
||||
log.Println("open error", err)
|
||||
@@ -90,8 +92,8 @@ func testSimple(t *testing.T, engine string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := clickhouseConnectionString(ip, port, engine)
|
||||
p := &clickhouse.ClickHouse{}
|
||||
addr := datastoreConnectionString(ip, port, engine)
|
||||
p := &datastore.Datastore{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -113,12 +115,12 @@ func testSimpleWithInstanceDefaultConfigValues(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := clickhouseConnectionString(ip, port, "")
|
||||
conn, err := sql.Open("clickhouse", addr)
|
||||
addr := datastoreConnectionString(ip, port, "")
|
||||
conn, err := sql.Open("datastore", addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := clickhouse.WithInstance(conn, &clickhouse.Config{})
|
||||
d, err := datastore.WithInstance(conn, &datastore.Config{})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
t.Fatal(err)
|
||||
@@ -140,8 +142,8 @@ func testMigrate(t *testing.T, engine string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := clickhouseConnectionString(ip, port, engine)
|
||||
p := &clickhouse.ClickHouse{}
|
||||
addr := datastoreConnectionString(ip, port, engine)
|
||||
p := &datastore.Datastore{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -169,8 +171,8 @@ func testVersion(t *testing.T, engine string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := clickhouseConnectionString(ip, port, engine)
|
||||
p := &clickhouse.ClickHouse{}
|
||||
addr := datastoreConnectionString(ip, port, engine)
|
||||
p := &datastore.Datastore{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -204,8 +206,8 @@ func testDrop(t *testing.T, engine string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := clickhouseConnectionString(ip, port, engine)
|
||||
p := &clickhouse.ClickHouse{}
|
||||
addr := datastoreConnectionString(ip, port, engine)
|
||||
p := &datastore.Datastore{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
CREATE TABLE test_1 (
|
||||
Date Date
|
||||
Date Date
|
||||
) Engine=Memory;
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
CREATE TABLE test_2 (
|
||||
Date Date
|
||||
Date Date
|
||||
) Engine=Memory;
|
||||
@@ -0,0 +1,10 @@
|
||||
DROP TABLE IF EXISTS driver_ratings;
|
||||
DROP TABLE IF EXISTS user_ratings;
|
||||
DROP TABLE IF EXISTS orders;
|
||||
DROP TABLE IF EXISTS driver_ratings_queue;
|
||||
DROP TABLE IF EXISTS user_ratings_queue;
|
||||
DROP TABLE IF EXISTS orders_queue;
|
||||
DROP VIEW IF EXISTS user_ratings_queue_mv;
|
||||
DROP VIEW IF EXISTS driver_ratings_queue_mv;
|
||||
DROP VIEW IF EXISTS orders_queue_mv;
|
||||
DROP DATABASE IF EXISTS analytics;
|
||||
@@ -0,0 +1,81 @@
|
||||
CREATE DATABASE IF NOT EXISTS analytics;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.driver_ratings(
|
||||
rate UInt8,
|
||||
userID Int64,
|
||||
driverID String,
|
||||
orderID String,
|
||||
inserted_time DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree
|
||||
PARTITION BY driverID
|
||||
ORDER BY (inserted_time);
|
||||
|
||||
CREATE TABLE analytics.driver_ratings_queue(
|
||||
rate UInt8,
|
||||
userID Int64,
|
||||
driverID String,
|
||||
orderID String
|
||||
) ENGINE = Kafka
|
||||
SETTINGS kafka_broker_list = 'broker:9092',
|
||||
kafka_topic_list = 'driver-ratings',
|
||||
kafka_group_name = 'rating_readers',
|
||||
kafka_format = 'Avro',
|
||||
kafka_max_block_size = 1048576;
|
||||
|
||||
CREATE MATERIALIZED VIEW analytics.driver_ratings_queue_mv TO analytics.driver_ratings AS
|
||||
SELECT rate, userID, driverID, orderID
|
||||
FROM analytics.driver_ratings_queue;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.user_ratings(
|
||||
rate UInt8,
|
||||
userID Int64,
|
||||
driverID String,
|
||||
orderID String,
|
||||
inserted_time DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree
|
||||
PARTITION BY userID
|
||||
ORDER BY (inserted_time);
|
||||
|
||||
CREATE TABLE analytics.user_ratings_queue(
|
||||
rate UInt8,
|
||||
userID Int64,
|
||||
driverID String,
|
||||
orderID String
|
||||
) ENGINE = Kafka
|
||||
SETTINGS kafka_broker_list = 'broker:9092',
|
||||
kafka_topic_list = 'user-ratings',
|
||||
kafka_group_name = 'rating_readers',
|
||||
kafka_format = 'JSON',
|
||||
kafka_max_block_size = 1048576;
|
||||
|
||||
CREATE MATERIALIZED VIEW analytics.user_ratings_queue_mv TO analytics.user_ratings AS
|
||||
SELECT rate, userID, driverID, orderID
|
||||
FROM analytics.user_ratings_queue;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.orders(
|
||||
from_place String,
|
||||
to_place String,
|
||||
userID Int64,
|
||||
driverID String,
|
||||
orderID String,
|
||||
inserted_time DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree
|
||||
PARTITION BY driverID
|
||||
ORDER BY (inserted_time);
|
||||
|
||||
CREATE TABLE analytics.orders_queue(
|
||||
from_place String,
|
||||
to_place String,
|
||||
userID Int64,
|
||||
driverID String,
|
||||
orderID String
|
||||
) ENGINE = Kafka
|
||||
SETTINGS kafka_broker_list = 'broker:9092',
|
||||
kafka_topic_list = 'orders',
|
||||
kafka_group_name = 'order_readers',
|
||||
kafka_format = 'Avro',
|
||||
kafka_max_block_size = 1048576;
|
||||
|
||||
CREATE MATERIALIZED VIEW analytics.orders_queue_mv TO orders AS
|
||||
SELECT from_place, to_place, userID, driverID, orderID
|
||||
FROM analytics.orders_queue;
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
// Package database provides the Database interface.
|
||||
// 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.
|
||||
@@ -25,22 +25,22 @@ 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
|
||||
// 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.
|
||||
// - 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()
|
||||
// - 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
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package firebird
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
_ "github.com/nakagami/firebirdsql"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -108,21 +107,21 @@ func (f *Firebird) Close() error {
|
||||
}
|
||||
|
||||
func (f *Firebird) Lock() error {
|
||||
if !f.isLocked.CAS(false, true) {
|
||||
if !f.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firebird) Unlock() error {
|
||||
if !f.isLocked.CAS(true, false) {
|
||||
if !f.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firebird) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -181,7 +180,7 @@ func (f *Firebird) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -224,11 +223,7 @@ func (f *Firebird) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := f.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -38,8 +38,9 @@ var (
|
||||
},
|
||||
}
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "jacobalberty/firebird:2.5-ss", Options: opts},
|
||||
{ImageName: "jacobalberty/firebird:3.0", Options: opts},
|
||||
{ImageName: "jacobalberty/firebird:v3.0", Options: opts},
|
||||
{ImageName: "jacobalberty/firebird:v4.0", Options: opts},
|
||||
{ImageName: "jacobalberty/firebird:v5.0", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+15
-19
@@ -2,21 +2,21 @@ package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
os "os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -114,7 +114,7 @@ func WithInstance(instance *mongo.Client, config *Config) (database.Driver, erro
|
||||
}
|
||||
|
||||
func (m *Mongo) Open(dsn string) (database.Driver, error) {
|
||||
//connstring is experimental package, but it used for parse connection string in mongo.Connect function
|
||||
// connstring is experimental package, but it used for parse connection string in mongo.Connect function
|
||||
uri, err := connstring.Parse(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -182,7 +182,7 @@ func (m *Mongo) Open(dsn string) (database.Driver, error) {
|
||||
return mc, nil
|
||||
}
|
||||
|
||||
//Parse the url param, convert it to boolean
|
||||
// Parse the url param, convert it to boolean
|
||||
// returns error if param invalid. returns defaultValue if param not present
|
||||
func parseBoolean(urlParam string, defaultValue bool) (bool, error) {
|
||||
|
||||
@@ -199,7 +199,7 @@ func parseBoolean(urlParam string, defaultValue bool) (bool, error) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
//Parse the url param, convert it to int
|
||||
// Parse the url param, convert it to int
|
||||
// returns error if param invalid. returns defaultValue if param not present
|
||||
func parseInt(urlParam string, defaultValue int) (int, error) {
|
||||
|
||||
@@ -241,7 +241,7 @@ func (m *Mongo) Version() (version int, dirty bool, err error) {
|
||||
}
|
||||
|
||||
func (m *Mongo) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -268,8 +268,8 @@ func (m *Mongo) executeCommandsWithTransaction(ctx context.Context, cmds []bson.
|
||||
return &database.Error{OrigErr: err, Err: "failed to start transaction"}
|
||||
}
|
||||
if err := m.executeCommands(sessionContext, cmds); err != nil {
|
||||
//When command execution is failed, it's aborting transaction
|
||||
//If you tried to call abortTransaction, it`s return error that transaction already aborted
|
||||
// When command execution is failed, it's aborting transaction
|
||||
// If you tried to call abortTransaction, it`s return error that transaction already aborted
|
||||
return err
|
||||
}
|
||||
if err := sessionContext.CommitTransaction(sessionContext); err != nil {
|
||||
@@ -325,11 +325,7 @@ func (m *Mongo) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ var (
|
||||
opts = dktest.Options{PortRequired: true, ReadyFunc: isReady}
|
||||
// Supported versions: https://www.mongodb.com/support-policy
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "mongo:3.4", Options: opts},
|
||||
{ImageName: "mongo:3.6", Options: opts},
|
||||
{ImageName: "mongo:4.0", Options: opts},
|
||||
{ImageName: "mongo:4.2", Options: opts},
|
||||
{ImageName: "mongo:5.0", Options: opts},
|
||||
{ImageName: "mongo:6.0", Options: opts},
|
||||
{ImageName: "mongo:7.0", Options: opts},
|
||||
{ImageName: "mongo:8.0", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -74,6 +74,22 @@ func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Run("test", test)
|
||||
t.Run("testMigrate", testMigrate)
|
||||
t.Run("testWithAuth", testWithAuth)
|
||||
t.Run("testLockWorks", testLockWorks)
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, spec := range specs {
|
||||
t.Log("Cleaning up ", spec.ImageName)
|
||||
if err := spec.Cleanup(); err != nil {
|
||||
t.Error("Error removing ", spec.ImageName, "error:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -99,7 +115,7 @@ func Test(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
func testMigrate(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -125,7 +141,7 @@ func TestMigrate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithAuth(t *testing.T) {
|
||||
func testWithAuth(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -180,7 +196,7 @@ func TestWithAuth(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockWorks(t *testing.T) {
|
||||
func testLockWorks(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -241,6 +257,15 @@ func TestTransaction(t *testing.T) {
|
||||
{ImageName: "mongo:4", Options: dktest.Options{PortRequired: true, ReadyFunc: isReady,
|
||||
Cmd: []string{"mongod", "--bind_ip_all", "--replSet", "rs0"}}},
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
for _, spec := range transactionSpecs {
|
||||
t.Log("Cleaning up ", spec.ImageName)
|
||||
if err := spec.Cleanup(); err != nil {
|
||||
t.Error("Error removing ", spec.ImageName, "error:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dktesting.ParallelTest(t, transactionSpecs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|------------|---------------------|-------------|
|
||||
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
|
||||
| `x-no-lock` | `NoLock` | Set to `true` to skip `GET_LOCK`/`RELEASE_LOCK` statements. Useful for [multi-master MySQL flavors](https://www.percona.com/doc/percona-xtradb-cluster/LATEST/features/pxc-strict-mode.html#explicit-table-locking). Only run migrations from one host when this is enabled. |
|
||||
| `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds, functionally similar to [Server-side SELECT statement timeouts](https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/) but enforced by the client. Available for all versions of MySQL, not just >=5.7. |
|
||||
| `dbname` | `DatabaseName` | The name of the database to connect to |
|
||||
| `user` | | The user to sign in as |
|
||||
| `password` | | The user's password |
|
||||
@@ -13,7 +14,7 @@
|
||||
| `port` | | The port to bind to. |
|
||||
| `tls` | | TLS / SSL encrypted connection parameter; see [go-sql-driver](https://github.com/go-sql-driver/mysql#tls). Use any name (e.g. `migrate`) if you want to use a custom TLS config (`x-tls-` queries). |
|
||||
| `x-tls-ca` | | The location of the CA (certificate authority) file. |
|
||||
| `x-tls-cert` | | The location of the client certicicate file. Must be used with `x-tls-key`. |
|
||||
| `x-tls-cert` | | The location of the client certificate file. Must be used with `x-tls-key`. |
|
||||
| `x-tls-key` | | The location of the private key file. Must be used with `x-tls-cert`. |
|
||||
| `x-tls-insecure-skip-verify` | | Whether or not to use SSL (true\|false) |
|
||||
|
||||
|
||||
+37
-23
@@ -1,5 +1,4 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package mysql
|
||||
|
||||
@@ -8,17 +7,18 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
var _ database.Driver = (*Mysql)(nil) // explicit compile time type check
|
||||
@@ -34,13 +34,14 @@ var (
|
||||
ErrNilConfig = fmt.Errorf("no config")
|
||||
ErrNoDatabaseName = fmt.Errorf("no database name")
|
||||
ErrAppendPEM = fmt.Errorf("failed to append PEM")
|
||||
ErrTLSCertKeyConfig = fmt.Errorf("To use TLS client authentication, both x-tls-cert and x-tls-key must not be empty")
|
||||
ErrTLSCertKeyConfig = fmt.Errorf("to use TLS client authentication, both x-tls-cert and x-tls-key must not be empty")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MigrationsTable string
|
||||
DatabaseName string
|
||||
NoLock bool
|
||||
MigrationsTable string
|
||||
DatabaseName string
|
||||
NoLock bool
|
||||
StatementTimeout time.Duration
|
||||
}
|
||||
|
||||
type Mysql struct {
|
||||
@@ -154,7 +155,7 @@ func urlToMySQLConfig(url string) (*mysql.Config, error) {
|
||||
if len(ctls) > 0 {
|
||||
if _, isBool := readBool(ctls); !isBool && strings.ToLower(ctls) != "skip-verify" {
|
||||
rootCertPool := x509.NewCertPool()
|
||||
pem, err := ioutil.ReadFile(parsedParams.Get("x-tls-ca"))
|
||||
pem, err := os.ReadFile(parsedParams.Get("x-tls-ca"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -241,15 +242,25 @@ func (m *Mysql) Open(url string) (database.Driver, error) {
|
||||
}
|
||||
}
|
||||
|
||||
statementTimeoutParam := customParams["x-statement-timeout"]
|
||||
statementTimeout := 0
|
||||
if statementTimeoutParam != "" {
|
||||
statementTimeout, err = strconv.Atoi(statementTimeoutParam)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse x-statement-timeout as float: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", config.FormatDSN())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mx, err := WithInstance(db, &Config{
|
||||
DatabaseName: config.DBName,
|
||||
MigrationsTable: customParams["x-migrations-table"],
|
||||
NoLock: noLock,
|
||||
DatabaseName: config.DBName,
|
||||
MigrationsTable: customParams["x-migrations-table"],
|
||||
NoLock: noLock,
|
||||
StatementTimeout: time.Duration(statementTimeout) * time.Millisecond,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -322,13 +333,20 @@ func (m *Mysql) Unlock() error {
|
||||
}
|
||||
|
||||
func (m *Mysql) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if m.config.StatementTimeout != 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, m.config.StatementTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
query := string(migr[:])
|
||||
if _, err := m.conn.ExecContext(context.Background(), query); err != nil {
|
||||
if _, err := m.conn.ExecContext(ctx, query); err != nil {
|
||||
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
|
||||
}
|
||||
|
||||
@@ -341,10 +359,10 @@ func (m *Mysql) SetVersion(version int, dirty bool) error {
|
||||
return &database.Error{OrigErr: err, Err: "transaction start failed"}
|
||||
}
|
||||
|
||||
query := "DELETE FROM `" + m.config.MigrationsTable + "`"
|
||||
query := "DELETE FROM `" + m.config.MigrationsTable + "` LIMIT 1"
|
||||
if _, err := tx.ExecContext(context.Background(), query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -356,7 +374,7 @@ func (m *Mysql) SetVersion(version int, dirty bool) error {
|
||||
query := "INSERT INTO `" + m.config.MigrationsTable + "` (version, dirty) VALUES (?, ?)"
|
||||
if _, err := tx.ExecContext(context.Background(), query, version, dirty); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -398,7 +416,7 @@ func (m *Mysql) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -451,11 +469,7 @@ func (m *Mysql) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
@@ -17,19 +16,14 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const defaultPort = 3306
|
||||
@@ -46,16 +40,14 @@ var (
|
||||
}
|
||||
// Supported versions: https://www.mysql.com/support/supportedplatforms/database.html
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "mysql:5.5", Options: opts},
|
||||
{ImageName: "mysql:5.6", Options: opts},
|
||||
{ImageName: "mysql:5.7", Options: opts},
|
||||
{ImageName: "mysql:8", Options: opts},
|
||||
{ImageName: "mysql:8.0", Options: opts},
|
||||
{ImageName: "mysql:8.4", Options: opts},
|
||||
{ImageName: "mysql:9.0", Options: opts},
|
||||
}
|
||||
specsAnsiQuotes = []dktesting.ContainerSpec{
|
||||
{ImageName: "mysql:5.5", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:5.6", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:5.7", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:8", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:8.0", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:8.4", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:9.0", Options: optsAnsiQuotes},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -88,7 +80,7 @@ func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
// mysql.SetLogger(mysql.Logger(log.New(ioutil.Discard, "", log.Ltime)))
|
||||
// mysql.SetLogger(mysql.Logger(log.New(io.Discard, "", log.Ltime)))
|
||||
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -121,7 +113,7 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
// mysql.SetLogger(mysql.Logger(log.New(ioutil.Discard, "", log.Ltime)))
|
||||
// mysql.SetLogger(mysql.Logger(log.New(io.Discard, "", log.Ltime)))
|
||||
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -159,7 +151,7 @@ func TestMigrate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrateAnsiQuotes(t *testing.T) {
|
||||
// mysql.SetLogger(mysql.Logger(log.New(ioutil.Discard, "", log.Ltime)))
|
||||
// mysql.SetLogger(mysql.Logger(log.New(io.Discard, "", log.Ltime)))
|
||||
|
||||
dktesting.ParallelTest(t, specsAnsiQuotes, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -342,7 +334,7 @@ func TestExtractCustomQueryParams(t *testing.T) {
|
||||
}
|
||||
|
||||
func createTmpCert(t *testing.T) string {
|
||||
tmpCertFile, err := ioutil.TempFile("", "migrate_test_cert")
|
||||
tmpCertFile, err := os.CreateTemp("", "migrate_test_cert")
|
||||
if err != nil {
|
||||
t.Fatal("Failed to create temp cert file:", err)
|
||||
}
|
||||
@@ -397,7 +389,7 @@ func TestURLToMySQLConfig(t *testing.T) {
|
||||
// Not supported yet: https://github.com/go-sql-driver/mysql/issues/591
|
||||
// {name: "user/password - user with encoded :",
|
||||
// urlStr: "mysql://username%3A:password@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
|
||||
// expectedDSN: "username::pasword@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
|
||||
// expectedDSN: "username::password@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
|
||||
{name: "user/password - user with encoded @",
|
||||
urlStr: "mysql://username%40:password@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
|
||||
expectedDSN: "username@:password@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
|
||||
|
||||
@@ -2,16 +2,15 @@ package neo4j
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
neturl "net/url"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/golang-migrate/migrate/v4/database/multistmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/neo4j/neo4j-go-driver/neo4j"
|
||||
)
|
||||
|
||||
@@ -142,7 +141,7 @@ func (n *Neo4j) Run(migration io.Reader) (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = multierror.Append(err, cerr)
|
||||
err = errors.Join(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -173,7 +172,7 @@ func (n *Neo4j) Run(migration io.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(migration)
|
||||
body, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -189,7 +188,7 @@ func (n *Neo4j) SetVersion(version int, dirty bool) (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = multierror.Append(err, cerr)
|
||||
err = errors.Join(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -214,7 +213,7 @@ func (n *Neo4j) Version() (version int, dirty bool, err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = multierror.Append(err, cerr)
|
||||
err = errors.Join(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -262,7 +261,7 @@ func (n *Neo4j) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = multierror.Append(err, cerr)
|
||||
err = errors.Join(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -279,7 +278,7 @@ func (n *Neo4j) ensureVersionConstraint() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = multierror.Append(err, cerr)
|
||||
err = errors.Join(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -8,13 +8,15 @@ import (
|
||||
)
|
||||
|
||||
const reservedChars = "!#$%&'()*+,/:;=?@[]"
|
||||
const reservedCharTestNamePrefix = "reserved char "
|
||||
|
||||
const baseUsername = "username"
|
||||
|
||||
const scheme = "database://"
|
||||
|
||||
// TestUserUnencodedReservedURLChars documents the behavior of using unencoded reserved characters in usernames with
|
||||
// net/url Parse()
|
||||
func TestUserUnencodedReservedURLChars(t *testing.T) {
|
||||
scheme := "database://"
|
||||
urlSuffix := "password@localhost:12345/myDB?someParam=true"
|
||||
urlSuffixAndSep := ":" + urlSuffix
|
||||
|
||||
@@ -64,7 +66,7 @@ func TestUserUnencodedReservedURLChars(t *testing.T) {
|
||||
testedChars := make([]string, 0, len(reservedChars))
|
||||
for _, tc := range testcases {
|
||||
testedChars = append(testedChars, tc.char)
|
||||
t.Run("reserved char "+tc.char, func(t *testing.T) {
|
||||
t.Run(reservedCharTestNamePrefix+tc.char, func(t *testing.T) {
|
||||
s := scheme + baseUsername + tc.char + urlSuffixAndSep
|
||||
u, err := url.Parse(s)
|
||||
if err == nil {
|
||||
@@ -98,13 +100,12 @@ func TestUserUnencodedReservedURLChars(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUserEncodedReservedURLChars(t *testing.T) {
|
||||
scheme := "database://"
|
||||
urlSuffix := "password@localhost:12345/myDB?someParam=true"
|
||||
urlSuffixAndSep := ":" + urlSuffix
|
||||
|
||||
for _, c := range reservedChars {
|
||||
c := string(c)
|
||||
t.Run("reserved char "+c, func(t *testing.T) {
|
||||
t.Run(reservedCharTestNamePrefix+c, func(t *testing.T) {
|
||||
encodedChar := "%" + hex.EncodeToString([]byte(c))
|
||||
s := scheme + baseUsername + encodedChar + urlSuffixAndSep
|
||||
expectedUsername := baseUsername + c
|
||||
@@ -126,7 +127,7 @@ func TestUserEncodedReservedURLChars(t *testing.T) {
|
||||
// with net/url Parse()
|
||||
func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
username := baseUsername
|
||||
schemeAndUsernameAndSep := "database://" + username + ":"
|
||||
schemeAndUsernameAndSep := scheme + username + ":"
|
||||
basePassword := "password"
|
||||
urlSuffixAndSep := "@localhost:12345/myDB?someParam=true"
|
||||
|
||||
@@ -174,7 +175,7 @@ func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
testedChars := make([]string, 0, len(reservedChars))
|
||||
for _, tc := range testcases {
|
||||
testedChars = append(testedChars, tc.char)
|
||||
t.Run("reserved char "+tc.char, func(t *testing.T) {
|
||||
t.Run(reservedCharTestNamePrefix+tc.char, func(t *testing.T) {
|
||||
s := schemeAndUsernameAndSep + basePassword + tc.char + urlSuffixAndSep
|
||||
u, err := url.Parse(s)
|
||||
if err == nil {
|
||||
@@ -213,13 +214,13 @@ func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
|
||||
func TestPasswordEncodedReservedURLChars(t *testing.T) {
|
||||
username := baseUsername
|
||||
schemeAndUsernameAndSep := "database://" + username + ":"
|
||||
schemeAndUsernameAndSep := scheme + username + ":"
|
||||
basePassword := "password"
|
||||
urlSuffixAndSep := "@localhost:12345/myDB?someParam=true"
|
||||
|
||||
for _, c := range reservedChars {
|
||||
c := string(c)
|
||||
t.Run("reserved char "+c, func(t *testing.T) {
|
||||
t.Run(reservedCharTestNamePrefix+c, func(t *testing.T) {
|
||||
encodedChar := "%" + hex.EncodeToString([]byte(c))
|
||||
s := schemeAndUsernameAndSep + basePassword + encodedChar + urlSuffixAndSep
|
||||
expectedPassword := basePassword + c
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# pgx
|
||||
|
||||
This package is for [pgx/v4](https://pkg.go.dev/github.com/jackc/pgx/v4). A backend for the newer [pgx/v5](https://pkg.go.dev/github.com/jackc/pgx/v5) is [also available](v5).
|
||||
|
||||
`pgx://user:password@host:port/dbname?query`
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
@@ -9,6 +11,8 @@
|
||||
| `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) |
|
||||
| `x-lock-strategy` | `LockStrategy` | Strategy used for locking during migration (default: advisory) |
|
||||
| `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock (default: schema_lock) |
|
||||
| `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 |
|
||||
|
||||
+168
-38
@@ -1,33 +1,38 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package pgx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/golang-migrate/migrate/v4/database/multistmt"
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
"github.com/jackc/pgconn"
|
||||
"github.com/jackc/pgerrcode"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
const (
|
||||
LockStrategyAdvisory = "advisory"
|
||||
LockStrategyTable = "table"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db := Postgres{}
|
||||
database.Register("pgx", &db)
|
||||
database.Register("pgx4", &db)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -35,6 +40,8 @@ var (
|
||||
|
||||
DefaultMigrationsTable = "schema_migrations"
|
||||
DefaultMultiStatementMaxSize = 10 * 1 << 20 // 10 MB
|
||||
DefaultLockTable = "schema_lock"
|
||||
DefaultLockStrategy = LockStrategyAdvisory
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -48,6 +55,8 @@ type Config struct {
|
||||
MigrationsTable string
|
||||
DatabaseName string
|
||||
SchemaName string
|
||||
LockTable string
|
||||
LockStrategy string
|
||||
migrationsSchemaName string
|
||||
migrationsTableName string
|
||||
StatementTimeout time.Duration
|
||||
@@ -107,6 +116,14 @@ func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
|
||||
config.MigrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
if len(config.LockTable) == 0 {
|
||||
config.LockTable = DefaultLockTable
|
||||
}
|
||||
|
||||
if len(config.LockStrategy) == 0 {
|
||||
config.LockStrategy = DefaultLockStrategy
|
||||
}
|
||||
|
||||
config.migrationsSchemaName = config.SchemaName
|
||||
config.migrationsTableName = config.MigrationsTable
|
||||
if config.MigrationsTableQuoted {
|
||||
@@ -132,6 +149,10 @@ func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
|
||||
config: config,
|
||||
}
|
||||
|
||||
if err := px.ensureLockTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := px.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -150,7 +171,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
// i.e. pgx://user:password@host:port/db => postgres://user:password@host:port/db
|
||||
purl.Scheme = "postgres"
|
||||
|
||||
db, err := sql.Open("pgx", migrate.FilterCustomQuery(purl).String())
|
||||
db, err := sql.Open("pgx/v4", migrate.FilterCustomQuery(purl).String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -160,7 +181,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
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)
|
||||
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] != '"')) {
|
||||
@@ -191,10 +212,13 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
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)
|
||||
return nil, fmt.Errorf("unable to parse option x-multi-statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
lockStrategy := purl.Query().Get("x-lock-strategy")
|
||||
lockTable := purl.Query().Get("x-lock-table")
|
||||
|
||||
px, err := WithInstance(db, &Config{
|
||||
DatabaseName: purl.Path,
|
||||
MigrationsTable: migrationsTable,
|
||||
@@ -202,6 +226,8 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
StatementTimeout: time.Duration(statementTimeout) * time.Millisecond,
|
||||
MultiStatementEnabled: multiStatementEnabled,
|
||||
MultiStatementMaxSize: multiStatementMaxSize,
|
||||
LockStrategy: lockStrategy,
|
||||
LockTable: lockTable,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -220,38 +246,118 @@ func (p *Postgres) Close() error {
|
||||
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
|
||||
switch p.config.LockStrategy {
|
||||
case LockStrategyAdvisory:
|
||||
return p.applyAdvisoryLock()
|
||||
case LockStrategyTable:
|
||||
return p.applyTableLock()
|
||||
default:
|
||||
return fmt.Errorf("unknown lock strategy \"%s\"", p.config.LockStrategy)
|
||||
}
|
||||
|
||||
// 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
|
||||
switch p.config.LockStrategy {
|
||||
case LockStrategyAdvisory:
|
||||
return p.releaseAdvisoryLock()
|
||||
case LockStrategyTable:
|
||||
return p.releaseTableLock()
|
||||
default:
|
||||
return fmt.Errorf("unknown lock strategy \"%s\"", p.config.LockStrategy)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS
|
||||
func (p *Postgres) applyAdvisoryLock() 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) applyTableLock() error {
|
||||
tx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{})
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Err: "transaction start failed"}
|
||||
}
|
||||
defer func() {
|
||||
errRollback := tx.Rollback()
|
||||
if errRollback != nil {
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
}()
|
||||
|
||||
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := "SELECT * FROM " + pq.QuoteIdentifier(p.config.LockTable) + " WHERE lock_id = $1"
|
||||
rows, err := tx.Query(query, aid)
|
||||
if err != nil {
|
||||
return database.Error{OrigErr: err, Err: "failed to fetch migration lock", Query: []byte(query)}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if errClose := rows.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
// If row exists at all, lock is present
|
||||
locked := rows.Next()
|
||||
if locked {
|
||||
return database.ErrLocked
|
||||
}
|
||||
|
||||
query = "INSERT INTO " + pq.QuoteIdentifier(p.config.LockTable) + " (lock_id) VALUES ($1)"
|
||||
if _, err := tx.Exec(query, aid); err != nil {
|
||||
return database.Error{OrigErr: err, Err: "failed to set migration lock", Query: []byte(query)}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (p *Postgres) releaseAdvisoryLock() 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) releaseTableLock() error {
|
||||
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := "DELETE FROM " + pq.QuoteIdentifier(p.config.LockTable) + " WHERE lock_id = $1"
|
||||
if _, err := p.db.Exec(query, aid); err != nil {
|
||||
return database.Error{OrigErr: err, Err: "failed to release migration lock", Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Run(migration io.Reader) error {
|
||||
if p.config.MultiStatementEnabled {
|
||||
var err error
|
||||
@@ -265,7 +371,7 @@ func (p *Postgres) Run(migration io.Reader) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -306,7 +412,7 @@ func (p *Postgres) runStatement(statement []byte) error {
|
||||
|
||||
func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
|
||||
// replace crlf with lf
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
// 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) {
|
||||
@@ -348,7 +454,7 @@ func (p *Postgres) SetVersion(version int, dirty bool) error {
|
||||
query := `TRUNCATE ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + quoteIdentifier(p.config.migrationsTableName)
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -360,7 +466,7 @@ func (p *Postgres) SetVersion(version int, dirty bool) error {
|
||||
query = `INSERT INTO ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + 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)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -402,7 +508,7 @@ func (p *Postgres) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -413,6 +519,12 @@ func (p *Postgres) Drop() (err error) {
|
||||
if err := tables.Scan(&tableName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// do not drop lock table
|
||||
if tableName == p.config.LockTable && p.config.LockStrategy == LockStrategyTable {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(tableName) > 0 {
|
||||
tableNames = append(tableNames, tableName)
|
||||
}
|
||||
@@ -444,11 +556,7 @@ func (p *Postgres) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -477,11 +585,33 @@ func (p *Postgres) ensureVersionTable() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Postgres) ensureLockTable() error {
|
||||
if p.config.LockStrategy != LockStrategyTable {
|
||||
return nil
|
||||
}
|
||||
|
||||
var count int
|
||||
query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1`
|
||||
if err := p.db.QueryRow(query, p.config.LockTable).Scan(&count); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
if count == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
query = `CREATE TABLE ` + pq.QuoteIdentifier(p.config.LockTable) + ` (lock_id BIGINT NOT NULL PRIMARY KEY)`
|
||||
if _, err := p.db.Exec(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copied from lib/pq implementation: https://github.com/lib/pq/blob/v1.9.0/conn.go#L1611
|
||||
func quoteIdentifier(name string) string {
|
||||
end := strings.IndexRune(name, 0)
|
||||
if end > -1 {
|
||||
name = name[:end]
|
||||
}
|
||||
return `"` + strings.Replace(name, `"`, `""`, -1) + `"`
|
||||
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
|
||||
}
|
||||
|
||||
+36
-12
@@ -8,19 +8,17 @@ import (
|
||||
sqldriver "database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"io"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
@@ -36,11 +34,11 @@ var (
|
||||
PortRequired: true, ReadyFunc: isReady}
|
||||
// Supported versions: https://www.postgresql.org/support/versioning/
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "postgres:9.5", Options: opts},
|
||||
{ImageName: "postgres:9.6", Options: opts},
|
||||
{ImageName: "postgres:10", Options: opts},
|
||||
{ImageName: "postgres:11", Options: opts},
|
||||
{ImageName: "postgres:12", Options: opts},
|
||||
{ImageName: "postgres:13", Options: opts},
|
||||
{ImageName: "postgres:14", Options: opts},
|
||||
{ImageName: "postgres:15", Options: opts},
|
||||
{ImageName: "postgres:16", Options: opts},
|
||||
{ImageName: "postgres:17", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -133,6 +131,32 @@ func TestMigrate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrateLockTable(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port, "x-lock-strategy=table", "x-lock-table=lock_table")
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "pgx", d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dt.TestMigrate(t, m)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleStatements(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
@@ -731,10 +755,10 @@ func Test_computeLineFromPos(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
input := tc.input
|
||||
if crlf {
|
||||
input = strings.Replace(input, "\n", "\r\n", -1)
|
||||
input = strings.ReplaceAll(input, "\n", "\r\n")
|
||||
}
|
||||
if nonASCII {
|
||||
input = strings.Replace(input, "FROM", "FRÖM", -1)
|
||||
input = strings.ReplaceAll(input, "FROM", "FRÖM")
|
||||
}
|
||||
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# pgx
|
||||
|
||||
This package is for [pgx/v5](https://pkg.go.dev/github.com/jackc/pgx/v5). A backend for the older [pgx/v4](https://pkg.go.dev/github.com/jackc/pgx/v4). is [also available](..).
|
||||
|
||||
`pgx5://user:password@host:port/dbname?query`
|
||||
|
||||
| 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.
|
||||
@@ -0,0 +1,480 @@
|
||||
//go:build go1.9
|
||||
|
||||
package pgx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/golang-migrate/migrate/v4/database/multistmt"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db := Postgres{}
|
||||
database.Register("pgx5", &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")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MigrationsTable string
|
||||
DatabaseName string
|
||||
SchemaName string
|
||||
migrationsSchemaName string
|
||||
migrationsTableName string
|
||||
StatementTimeout time.Duration
|
||||
MigrationsTableQuoted bool
|
||||
MultiStatementEnabled bool
|
||||
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 WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
|
||||
if config == nil {
|
||||
return nil, ErrNilConfig
|
||||
}
|
||||
|
||||
if err := instance.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.DatabaseName == "" {
|
||||
query := `SELECT CURRENT_DATABASE()`
|
||||
var databaseName string
|
||||
if err := instance.QueryRow(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 string
|
||||
if err := instance.QueryRow(query).Scan(&schemaName); err != nil {
|
||||
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if len(schemaName) == 0 {
|
||||
return nil, ErrNoSchema
|
||||
}
|
||||
|
||||
config.SchemaName = schemaName
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := instance.Conn(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
px := &Postgres{
|
||||
conn: conn,
|
||||
db: instance,
|
||||
config: config,
|
||||
}
|
||||
|
||||
if err := px.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
purl, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Driver is registered as pgx, but connection string must use postgres schema
|
||||
// when making actual connection
|
||||
// i.e. pgx://user:password@host:port/db => postgres://user:password@host:port/db
|
||||
purl.Scheme = "postgres"
|
||||
|
||||
db, err := sql.Open("pgx/v5", 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()
|
||||
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.(*pgconn.PgError); ok {
|
||||
var line uint
|
||||
var col uint
|
||||
var lineColOK bool
|
||||
line, col, lineColOK = computeLineFromPos(query, int(pgErr.Position))
|
||||
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.ReplaceAll(s, "\r\n", "\n")
|
||||
// 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 ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + quoteIdentifier(p.config.migrationsTableName)
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = errors.Join(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 ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + 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 = errors.Join(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 ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + 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.(*pgconn.PgError); ok {
|
||||
if e.SQLState() == pgerrcode.UndefinedTable {
|
||||
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 = errors.Join(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 ` + 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 {
|
||||
err = errors.Join(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 ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + 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
|
||||
}
|
||||
|
||||
// Copied from lib/pq implementation: https://github.com/lib/pq/blob/v1.9.0/conn.go#L1611
|
||||
func quoteIdentifier(name string) string {
|
||||
end := strings.IndexRune(name, 0)
|
||||
if end > -1 {
|
||||
name = name[:end]
|
||||
}
|
||||
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
|
||||
}
|
||||
@@ -0,0 +1,761 @@
|
||||
package pgx
|
||||
|
||||
// error codes https://github.com/jackc/pgerrcode/blob/master/errcode.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
sqldriver "database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
const (
|
||||
pgPassword = "postgres"
|
||||
)
|
||||
|
||||
var (
|
||||
opts = dktest.Options{
|
||||
Env: map[string]string{"POSTGRES_PASSWORD": pgPassword},
|
||||
PortRequired: true, ReadyFunc: isReady}
|
||||
// Supported versions: https://www.postgresql.org/support/versioning/
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "postgres:13", Options: opts},
|
||||
{ImageName: "postgres:14", Options: opts},
|
||||
{ImageName: "postgres:15", Options: opts},
|
||||
{ImageName: "postgres:16", Options: opts},
|
||||
{ImageName: "postgres:17", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
func pgConnectionString(host, port string, options ...string) string {
|
||||
options = append(options, "sslmode=disable")
|
||||
return fmt.Sprintf("postgres://postgres:%s@%s:%s/postgres?%s", pgPassword, host, port, strings.Join(options, "&"))
|
||||
}
|
||||
|
||||
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", pgConnectionString(ip, port))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
log.Println("close error:", err)
|
||||
}
|
||||
}()
|
||||
if err = db.PingContext(ctx); err != nil {
|
||||
switch err {
|
||||
case sqldriver.ErrBadConn, io.EOF:
|
||||
return false
|
||||
default:
|
||||
log.Println(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func mustRun(t *testing.T, d database.Driver, statements []string) {
|
||||
for _, statement := range statements {
|
||||
if err := d.Run(strings.NewReader(statement)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
dt.Test(t, d, []byte("SELECT 1"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
m, err := migrate.NewWithDatabaseInstance("file://../examples/migrations", "pgx", d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dt.TestMigrate(t, m)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleStatements(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);")); err != nil {
|
||||
t.Fatalf("expected err to be nil, got %v", err)
|
||||
}
|
||||
|
||||
// make sure second table exists
|
||||
var exists bool
|
||||
if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("expected table bar to exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleStatementsInMultiStatementMode(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port, "x-multi-statement=true")
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE INDEX CONCURRENTLY idx_foo ON foo (foo);")); err != nil {
|
||||
t.Fatalf("expected err to be nil, got %v", err)
|
||||
}
|
||||
|
||||
// make sure created index exists
|
||||
var exists bool
|
||||
if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = (SELECT current_schema()) AND indexname = 'idx_foo')").Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("expected table bar to exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorParsing(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
wantErr := `migration failed: syntax error at or near "TABLEE" (column 37) in line 1: CREATE TABLE foo ` +
|
||||
`(foo text); CREATE TABLEE bar (bar text); (details: ERROR: syntax error at or near "TABLEE" (SQLSTATE 42601))`
|
||||
if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text);")); err == nil {
|
||||
t.Fatal("expected err but got nil")
|
||||
} else if err.Error() != wantErr {
|
||||
t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterCustomQuery(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port, "x-custom=foobar")
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithSchema(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create foobar schema
|
||||
if err := d.Run(strings.NewReader("CREATE SCHEMA foobar AUTHORIZATION postgres")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.SetVersion(1, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// re-connect using that schema
|
||||
d2, err := p.Open(pgConnectionString(ip, port, "search_path=foobar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d2.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
version, _, err := d2.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != database.NilVersion {
|
||||
t.Fatal("expected NilVersion")
|
||||
}
|
||||
|
||||
// now update version and compare
|
||||
if err := d2.SetVersion(2, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
version, _, err = d2.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Fatal("expected version 2")
|
||||
}
|
||||
|
||||
// meanwhile, the public schema still has the other version
|
||||
version, _, err = d.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 1 {
|
||||
t.Fatal("expected version 2")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrationTableOption(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, _ := p.Open(addr)
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create migrate schema
|
||||
if err := d.Run(strings.NewReader("CREATE SCHEMA migrate AUTHORIZATION postgres")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// bad unquoted x-migrations-table parameter
|
||||
wantErr := "x-migrations-table must be quoted (for instance '\"migrate\".\"schema_migrations\"') when x-migrations-table-quoted is enabled, current value is: migrate.schema_migrations"
|
||||
d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=migrate.schema_migrations&x-migrations-table-quoted=1",
|
||||
pgPassword, ip, port))
|
||||
if (err != nil) && (err.Error() != wantErr) {
|
||||
t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error())
|
||||
}
|
||||
|
||||
// too many quoted x-migrations-table parameters
|
||||
wantErr = "\"\"migrate\".\"schema_migrations\".\"toomany\"\" MigrationsTable contains too many dot characters"
|
||||
d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"migrate\".\"schema_migrations\".\"toomany\"&x-migrations-table-quoted=1",
|
||||
pgPassword, ip, port))
|
||||
if (err != nil) && (err.Error() != wantErr) {
|
||||
t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error())
|
||||
}
|
||||
|
||||
// good quoted x-migrations-table parameter
|
||||
d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"migrate\".\"schema_migrations\"&x-migrations-table-quoted=1",
|
||||
pgPassword, ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// make sure migrate.schema_migrations table exists
|
||||
var exists bool
|
||||
if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'schema_migrations' AND table_schema = 'migrate')").Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("expected table migrate.schema_migrations to exist")
|
||||
}
|
||||
|
||||
d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=migrate.schema_migrations",
|
||||
pgPassword, ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'migrate.schema_migrations' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("expected table 'migrate.schema_migrations' to exist")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailToCreateTableWithoutPermissions(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
|
||||
// Check that opening the postgres connection returns NilVersion
|
||||
p := &Postgres{}
|
||||
|
||||
d, err := p.Open(addr)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create user who is not the owner. Although we're concatenating strings in an sql statement it should be fine
|
||||
// since this is a test environment and we're not expecting to the pgPassword to be malicious
|
||||
mustRun(t, d, []string{
|
||||
"CREATE USER not_owner WITH ENCRYPTED PASSWORD '" + pgPassword + "'",
|
||||
"CREATE SCHEMA barfoo AUTHORIZATION postgres",
|
||||
"GRANT USAGE ON SCHEMA barfoo TO not_owner",
|
||||
"REVOKE CREATE ON SCHEMA barfoo FROM PUBLIC",
|
||||
"REVOKE CREATE ON SCHEMA barfoo FROM not_owner",
|
||||
})
|
||||
|
||||
// re-connect using that schema
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo",
|
||||
pgPassword, ip, port))
|
||||
|
||||
defer func() {
|
||||
if d2 == nil {
|
||||
return
|
||||
}
|
||||
if err := d2.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
var e *database.Error
|
||||
if !errors.As(err, &e) || err == nil {
|
||||
t.Fatal("Unexpected error, want permission denied error. Got: ", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(e.OrigErr.Error(), "permission denied for schema barfoo") {
|
||||
t.Fatal(e)
|
||||
}
|
||||
|
||||
// re-connect using that x-migrations-table and x-migrations-table-quoted
|
||||
d2, err = p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"barfoo\".\"schema_migrations\"&x-migrations-table-quoted=1",
|
||||
pgPassword, ip, port))
|
||||
|
||||
if !errors.As(err, &e) || err == nil {
|
||||
t.Fatal("Unexpected error, want permission denied error. Got: ", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(e.OrigErr.Error(), "permission denied for schema barfoo") {
|
||||
t.Fatal(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckBeforeCreateTable(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
|
||||
// Check that opening the postgres connection returns NilVersion
|
||||
p := &Postgres{}
|
||||
|
||||
d, err := p.Open(addr)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create user who is not the owner. Although we're concatenating strings in an sql statement it should be fine
|
||||
// since this is a test environment and we're not expecting to the pgPassword to be malicious
|
||||
mustRun(t, d, []string{
|
||||
"CREATE USER not_owner WITH ENCRYPTED PASSWORD '" + pgPassword + "'",
|
||||
"CREATE SCHEMA barfoo AUTHORIZATION postgres",
|
||||
"GRANT USAGE ON SCHEMA barfoo TO not_owner",
|
||||
"GRANT CREATE ON SCHEMA barfoo TO not_owner",
|
||||
})
|
||||
|
||||
// re-connect using that schema
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo",
|
||||
pgPassword, ip, port))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := d2.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// revoke privileges
|
||||
mustRun(t, d, []string{
|
||||
"REVOKE CREATE ON SCHEMA barfoo FROM PUBLIC",
|
||||
"REVOKE CREATE ON SCHEMA barfoo FROM not_owner",
|
||||
})
|
||||
|
||||
// re-connect using that schema
|
||||
d3, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo",
|
||||
pgPassword, ip, port))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
version, _, err := d3.Version()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if version != database.NilVersion {
|
||||
t.Fatal("Unexpected version, want database.NilVersion. Got: ", version)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := d3.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func TestParallelSchema(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create foo and bar schemas
|
||||
if err := d.Run(strings.NewReader("CREATE SCHEMA foo AUTHORIZATION postgres")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.Run(strings.NewReader("CREATE SCHEMA bar AUTHORIZATION postgres")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// re-connect using that schemas
|
||||
dfoo, err := p.Open(pgConnectionString(ip, port, "search_path=foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := dfoo.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
dbar, err := p.Open(pgConnectionString(ip, port, "search_path=bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := dbar.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := dfoo.Lock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := dbar.Lock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := dbar.Unlock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := dfoo.Unlock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostgres_Lock(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dt.Test(t, d, []byte("SELECT 1"))
|
||||
|
||||
ps := d.(*Postgres)
|
||||
|
||||
err = ps.Lock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ps.Unlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ps.Lock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ps.Unlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithInstance_Concurrent(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The number of concurrent processes running WithInstance
|
||||
const concurrency = 30
|
||||
|
||||
// We can instantiate a single database handle because it is
|
||||
// actually a connection pool, and so, each of the below go
|
||||
// routines will have a high probability of using a separate
|
||||
// connection, which is something we want to exercise.
|
||||
db, err := sql.Open("pgx", pgConnectionString(ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
db.SetMaxIdleConns(concurrency)
|
||||
db.SetMaxOpenConns(concurrency)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
_, err := WithInstance(db, &Config{})
|
||||
if err != nil {
|
||||
t.Errorf("process %d error: %s", i, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
func Test_computeLineFromPos(t *testing.T) {
|
||||
testcases := []struct {
|
||||
pos int
|
||||
wantLine uint
|
||||
wantCol uint
|
||||
input string
|
||||
wantOk bool
|
||||
}{
|
||||
{
|
||||
15, 2, 6, "SELECT *\nFROM foo", true, // foo table does not exists
|
||||
},
|
||||
{
|
||||
16, 3, 6, "SELECT *\n\nFROM foo", true, // foo table does not exists, empty line
|
||||
},
|
||||
{
|
||||
25, 3, 7, "SELECT *\nFROM foo\nWHERE x", true, // x column error
|
||||
},
|
||||
{
|
||||
27, 5, 7, "SELECT *\n\nFROM foo\n\nWHERE x", true, // x column error, empty lines
|
||||
},
|
||||
{
|
||||
10, 2, 1, "SELECT *\nFROMM foo", true, // FROMM typo
|
||||
},
|
||||
{
|
||||
11, 3, 1, "SELECT *\n\nFROMM foo", true, // FROMM typo, empty line
|
||||
},
|
||||
{
|
||||
17, 2, 8, "SELECT *\nFROM foo", true, // last character
|
||||
},
|
||||
{
|
||||
18, 0, 0, "SELECT *\nFROM foo", false, // invalid position
|
||||
},
|
||||
}
|
||||
for i, tc := range testcases {
|
||||
t.Run("tc"+strconv.Itoa(i), func(t *testing.T) {
|
||||
run := func(crlf bool, nonASCII bool) {
|
||||
var name string
|
||||
if crlf {
|
||||
name = "crlf"
|
||||
} else {
|
||||
name = "lf"
|
||||
}
|
||||
if nonASCII {
|
||||
name += "-nonascii"
|
||||
} else {
|
||||
name += "-ascii"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
input := tc.input
|
||||
if crlf {
|
||||
input = strings.ReplaceAll(input, "\n", "\r\n")
|
||||
}
|
||||
if nonASCII {
|
||||
input = strings.ReplaceAll(input, "FROM", "FRÖM")
|
||||
}
|
||||
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
|
||||
|
||||
if tc.wantOk {
|
||||
t.Logf("pos %d, want %d:%d, %#v", tc.pos, tc.wantLine, tc.wantCol, input)
|
||||
}
|
||||
|
||||
if gotOK != tc.wantOk {
|
||||
t.Fatalf("expected ok %v but got %v", tc.wantOk, gotOK)
|
||||
}
|
||||
if gotLine != tc.wantLine {
|
||||
t.Fatalf("expected line %d but got %d", tc.wantLine, gotLine)
|
||||
}
|
||||
if gotCol != tc.wantCol {
|
||||
t.Fatalf("expected col %d but got %d", tc.wantCol, gotCol)
|
||||
}
|
||||
})
|
||||
}
|
||||
run(false, false)
|
||||
run(true, false)
|
||||
run(false, true)
|
||||
run(true, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ If there were no errors, we should have two files available under `db/migrations
|
||||
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,
|
||||
@@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
);
|
||||
```
|
||||
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)
|
||||
@@ -79,7 +79,7 @@ Again, it should create for us two migrations files:
|
||||
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 (
|
||||
@@ -92,7 +92,7 @@ ALTER TABLE users ADD COLUMN mood enum_mood;
|
||||
COMMIT;
|
||||
```
|
||||
Migration down:
|
||||
```
|
||||
```sql
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE users DROP COLUMN mood;
|
||||
@@ -124,7 +124,7 @@ Indexes:
|
||||
|
||||
## Optional: Run migrations within your Go app
|
||||
Here is a very simple app running migrations for the above configuration:
|
||||
```
|
||||
```go
|
||||
import (
|
||||
"log"
|
||||
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -91,16 +88,16 @@ func WithConnection(ctx context.Context, conn *sql.Conn, config *Config) (*Postg
|
||||
|
||||
if config.SchemaName == "" {
|
||||
query := `SELECT CURRENT_SCHEMA()`
|
||||
var schemaName string
|
||||
var schemaName sql.NullString
|
||||
if err := conn.QueryRowContext(ctx, query).Scan(&schemaName); err != nil {
|
||||
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if len(schemaName) == 0 {
|
||||
if !schemaName.Valid {
|
||||
return nil, ErrNoSchema
|
||||
}
|
||||
|
||||
config.SchemaName = schemaName
|
||||
config.SchemaName = schemaName.String
|
||||
}
|
||||
|
||||
if len(config.MigrationsTable) == 0 {
|
||||
@@ -168,7 +165,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
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)
|
||||
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] != '"')) {
|
||||
@@ -199,7 +196,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
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)
|
||||
return nil, fmt.Errorf("unable to parse option x-multi-statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +275,7 @@ func (p *Postgres) Run(migration io.Reader) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -322,7 +319,7 @@ func (p *Postgres) runStatement(statement []byte) error {
|
||||
|
||||
func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
|
||||
// replace crlf with lf
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
// 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) {
|
||||
@@ -364,7 +361,7 @@ func (p *Postgres) SetVersion(version int, dirty bool) error {
|
||||
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)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -376,7 +373,7 @@ func (p *Postgres) SetVersion(version int, dirty bool) error {
|
||||
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)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -418,7 +415,7 @@ func (p *Postgres) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -460,11 +457,7 @@ func (p *Postgres) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -35,11 +35,11 @@ var (
|
||||
PortRequired: true, ReadyFunc: isReady}
|
||||
// Supported versions: https://www.postgresql.org/support/versioning/
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "postgres:9.5", Options: opts},
|
||||
{ImageName: "postgres:9.6", Options: opts},
|
||||
{ImageName: "postgres:10", Options: opts},
|
||||
{ImageName: "postgres:11", Options: opts},
|
||||
{ImageName: "postgres:12", Options: opts},
|
||||
{ImageName: "postgres:13", Options: opts},
|
||||
{ImageName: "postgres:14", Options: opts},
|
||||
{ImageName: "postgres:15", Options: opts},
|
||||
{ImageName: "postgres:16", Options: opts},
|
||||
{ImageName: "postgres:17", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -85,6 +85,32 @@ func mustRun(t *testing.T, d database.Driver, statements []string) {
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Run("test", test)
|
||||
t.Run("testMigrate", testMigrate)
|
||||
t.Run("testMultipleStatements", testMultipleStatements)
|
||||
t.Run("testMultipleStatementsInMultiStatementMode", testMultipleStatementsInMultiStatementMode)
|
||||
t.Run("testErrorParsing", testErrorParsing)
|
||||
t.Run("testFilterCustomQuery", testFilterCustomQuery)
|
||||
t.Run("testWithSchema", testWithSchema)
|
||||
t.Run("testMigrationTableOption", testMigrationTableOption)
|
||||
t.Run("testFailToCreateTableWithoutPermissions", testFailToCreateTableWithoutPermissions)
|
||||
t.Run("testCheckBeforeCreateTable", testCheckBeforeCreateTable)
|
||||
t.Run("testParallelSchema", testParallelSchema)
|
||||
t.Run("testPostgresLock", testPostgresLock)
|
||||
t.Run("testWithInstanceConcurrent", testWithInstanceConcurrent)
|
||||
t.Run("testWithConnection", testWithConnection)
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, spec := range specs {
|
||||
t.Log("Cleaning up ", spec.ImageName)
|
||||
if err := spec.Cleanup(); err != nil {
|
||||
t.Error("Error removing ", spec.ImageName, "error:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -106,7 +132,7 @@ func Test(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
func testMigrate(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -132,7 +158,7 @@ func TestMigrate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleStatements(t *testing.T) {
|
||||
func testMultipleStatements(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -165,7 +191,7 @@ func TestMultipleStatements(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleStatementsInMultiStatementMode(t *testing.T) {
|
||||
func testMultipleStatementsInMultiStatementMode(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -198,7 +224,7 @@ func TestMultipleStatementsInMultiStatementMode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorParsing(t *testing.T) {
|
||||
func testErrorParsing(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -227,7 +253,7 @@ func TestErrorParsing(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterCustomQuery(t *testing.T) {
|
||||
func testFilterCustomQuery(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -249,7 +275,7 @@ func TestFilterCustomQuery(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithSchema(t *testing.T) {
|
||||
func testWithSchema(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -319,7 +345,7 @@ func TestWithSchema(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrationTableOption(t *testing.T) {
|
||||
func testMigrationTableOption(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -387,7 +413,7 @@ func TestMigrationTableOption(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailToCreateTableWithoutPermissions(t *testing.T) {
|
||||
func testFailToCreateTableWithoutPermissions(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -457,7 +483,7 @@ func TestFailToCreateTableWithoutPermissions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckBeforeCreateTable(t *testing.T) {
|
||||
func testCheckBeforeCreateTable(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -534,7 +560,7 @@ func TestCheckBeforeCreateTable(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestParallelSchema(t *testing.T) {
|
||||
func testParallelSchema(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -602,7 +628,7 @@ func TestParallelSchema(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostgres_Lock(t *testing.T) {
|
||||
func testPostgresLock(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -642,7 +668,7 @@ func TestPostgres_Lock(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithInstance_Concurrent(t *testing.T) {
|
||||
func testWithInstanceConcurrent(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -685,7 +711,7 @@ func TestWithInstance_Concurrent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithConnection(t *testing.T) {
|
||||
func testWithConnection(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
@@ -772,10 +798,10 @@ func Test_computeLineFromPos(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
input := tc.input
|
||||
if crlf {
|
||||
input = strings.Replace(input, "\n", "\r\n", -1)
|
||||
input = strings.ReplaceAll(input, "\n", "\r\n")
|
||||
}
|
||||
if nonASCII {
|
||||
input = strings.Replace(input, "FROM", "FRÖM", -1)
|
||||
input = strings.ReplaceAll(input, "FROM", "FRÖM")
|
||||
}
|
||||
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
|
||||
|
||||
|
||||
+10
-16
@@ -2,14 +2,12 @@ package ql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
nurl "net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
@@ -73,11 +71,7 @@ func (m *Ql) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -134,7 +128,7 @@ func (m *Ql) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -167,19 +161,19 @@ func (m *Ql) Drop() (err error) {
|
||||
return nil
|
||||
}
|
||||
func (m *Ql) Lock() error {
|
||||
if !m.isLocked.CAS(false, true) {
|
||||
if !m.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *Ql) Unlock() error {
|
||||
if !m.isLocked.CAS(true, false) {
|
||||
if !m.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *Ql) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,7 +188,7 @@ func (m *Ql) executeQuery(query string) error {
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -222,7 +216,7 @@ func (m *Ql) SetVersion(version int, dirty bool) error {
|
||||
m.config.MigrationsTable)
|
||||
if _, err := tx.Exec(query, version, dirty); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package redshift
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
@@ -128,21 +126,21 @@ func (p *Redshift) Close() error {
|
||||
|
||||
// Redshift does not support advisory lock functions: https://docs.aws.amazon.com/redshift/latest/dg/c_unsupported-postgresql-functions.html
|
||||
func (p *Redshift) Lock() error {
|
||||
if !p.isLocked.CAS(false, true) {
|
||||
if !p.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Redshift) Unlock() error {
|
||||
if !p.isLocked.CAS(true, false) {
|
||||
if !p.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Redshift) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,7 +174,7 @@ func (p *Redshift) Run(migration io.Reader) error {
|
||||
|
||||
func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
|
||||
// replace crlf with lf
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
// 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) {
|
||||
@@ -218,7 +216,7 @@ func (p *Redshift) SetVersion(version int, dirty bool) error {
|
||||
query := `DELETE FROM "` + p.config.MigrationsTable + `"`
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -230,7 +228,7 @@ func (p *Redshift) SetVersion(version int, dirty bool) error {
|
||||
query = `INSERT INTO "` + p.config.MigrationsTable + `" (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)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -272,7 +270,7 @@ func (p *Redshift) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -314,11 +312,7 @@ func (p *Redshift) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -28,10 +28,19 @@ import (
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
const (
|
||||
pgPassword = "redshift"
|
||||
)
|
||||
|
||||
var (
|
||||
opts = dktest.Options{PortRequired: true, ReadyFunc: isReady}
|
||||
opts = dktest.Options{
|
||||
Env: map[string]string{"POSTGRES_PASSWORD": pgPassword},
|
||||
PortRequired: true,
|
||||
ReadyFunc: isReady,
|
||||
}
|
||||
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "postgres:8", Options: opts},
|
||||
{ImageName: "migrate/postgres8:8", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -44,7 +53,7 @@ func pgConnectionString(host, port string) string {
|
||||
}
|
||||
|
||||
func connectionString(schema, host, port string) string {
|
||||
return fmt.Sprintf("%s://postgres@%s:%s/postgres?sslmode=disable", schema, host, port)
|
||||
return fmt.Sprintf("%s://postgres:%s@%s:%s/postgres?sslmode=disable", schema, pgPassword, host, port)
|
||||
}
|
||||
|
||||
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
@@ -192,7 +201,7 @@ func TestFilterCustomQuery(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&x-custom=foobar", ip, port)
|
||||
addr := fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-custom=foobar", pgPassword, ip, port)
|
||||
p := &Redshift{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
@@ -234,7 +243,7 @@ func TestWithSchema(t *testing.T) {
|
||||
}
|
||||
|
||||
// re-connect using that schema
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&search_path=foobar", ip, port))
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&search_path=foobar", pgPassword, ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -369,10 +378,10 @@ func Test_computeLineFromPos(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
input := tc.input
|
||||
if crlf {
|
||||
input = strings.Replace(input, "\n", "\r\n", -1)
|
||||
input = strings.ReplaceAll(input, "\n", "\r\n")
|
||||
}
|
||||
if nonASCII {
|
||||
input = strings.Replace(input, "FROM", "FRÖM", -1)
|
||||
input = strings.ReplaceAll(input, "FROM", "FRÖM")
|
||||
}
|
||||
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# rqlite
|
||||
|
||||
`rqlite://admin:secret@server1.example.com:4001/?level=strong&timeout=5`
|
||||
|
||||
The `rqlite` url scheme is used for both secure and insecure connections. If connecting to an insecure database, pass `x-connect-insecure` in your URL query, or use `WithInstance` to pass an established connection.
|
||||
|
||||
The migrations table name is configurable through the `x-migrations-table` URL query parameter, or by using `WithInstance` and passing `MigrationsTable` through `Config`.
|
||||
|
||||
Other connect parameters are directly passed through to the database driver. For examples of connection strings, see https://github.com/rqlite/gorqlite#examples.
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| `x-connect-insecure` | n/a: set on instance | Boolean to indicate whether to use an insecure connection. Defaults to `false`. |
|
||||
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table. Defaults to `schema_migrations`. |
|
||||
|
||||
## Notes
|
||||
|
||||
* Uses the https://github.com/rqlite/gorqlite driver
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS pets;
|
||||
@@ -0,0 +1,3 @@
|
||||
CREATE TABLE pets (
|
||||
name string
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS pets;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE pets ADD predator bool;
|
||||
@@ -0,0 +1,328 @@
|
||||
package rqlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/rqlite/gorqlite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
database.Register("rqlite", &Rqlite{})
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultMigrationsTable defines the default rqlite migrations table
|
||||
DefaultMigrationsTable = "schema_migrations"
|
||||
|
||||
// DefaultConnectInsecure defines the default setting for connect insecure
|
||||
DefaultConnectInsecure = false
|
||||
)
|
||||
|
||||
// ErrNilConfig is returned if no configuration was passed to WithInstance
|
||||
var ErrNilConfig = fmt.Errorf("no config")
|
||||
|
||||
// ErrBadConfig is returned if configuration was invalid
|
||||
var ErrBadConfig = fmt.Errorf("bad parameter")
|
||||
|
||||
// Config defines the driver configuration
|
||||
type Config struct {
|
||||
// ConnectInsecure sets whether the connection uses TLS. Ineffectual when using WithInstance
|
||||
ConnectInsecure bool
|
||||
// MigrationsTable configures the migrations table name
|
||||
MigrationsTable string
|
||||
}
|
||||
|
||||
type Rqlite struct {
|
||||
db *gorqlite.Connection
|
||||
isLocked atomic.Bool
|
||||
|
||||
config *Config
|
||||
}
|
||||
|
||||
// WithInstance creates a rqlite database driver with an existing gorqlite database connection
|
||||
// and a Config struct
|
||||
func WithInstance(instance *gorqlite.Connection, config *Config) (database.Driver, error) {
|
||||
if config == nil {
|
||||
return nil, ErrNilConfig
|
||||
}
|
||||
|
||||
// we use the consistency level check as a database ping
|
||||
if _, err := instance.ConsistencyLevel(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(config.MigrationsTable) == 0 {
|
||||
config.MigrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
driver := &Rqlite{
|
||||
db: instance,
|
||||
config: config,
|
||||
}
|
||||
|
||||
if err := driver.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
// OpenURL creates a rqlite database driver from a connect URL
|
||||
func OpenURL(url string) (database.Driver, error) {
|
||||
d := &Rqlite{}
|
||||
return d.Open(url)
|
||||
}
|
||||
|
||||
func (r *Rqlite) ensureVersionTable() (err error) {
|
||||
if err = r.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := r.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
stmts := []string{
|
||||
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (version uint64, dirty bool)`, r.config.MigrationsTable),
|
||||
fmt.Sprintf(`CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version)`, r.config.MigrationsTable),
|
||||
}
|
||||
|
||||
if _, err := r.db.Write(stmts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open returns a new driver instance configured with parameters
|
||||
// coming from the URL string. Migrate will call this function
|
||||
// only once per instance.
|
||||
func (r *Rqlite) Open(url string) (database.Driver, error) {
|
||||
dburl, config, err := parseUrl(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.config = config
|
||||
|
||||
r.db, err = gorqlite.Open(dburl.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying database instance managed by the driver.
|
||||
// Migrate will call this function only once per instance.
|
||||
func (r *Rqlite) Close() error {
|
||||
r.db.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (r *Rqlite) Lock() error {
|
||||
if !r.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock should release the lock. Migrate will call this function after
|
||||
// all migrations have been run.
|
||||
func (r *Rqlite) Unlock() error {
|
||||
if !r.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run applies a migration to the database. migration is guaranteed to be not nil.
|
||||
func (r *Rqlite) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := string(migr[:])
|
||||
if _, err := r.db.WriteOne(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (r *Rqlite) SetVersion(version int, dirty bool) error {
|
||||
deleteQuery := fmt.Sprintf(`DELETE FROM %s`, r.config.MigrationsTable)
|
||||
statements := []gorqlite.ParameterizedStatement{
|
||||
{
|
||||
Query: deleteQuery,
|
||||
},
|
||||
}
|
||||
|
||||
// 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
|
||||
insertQuery := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, r.config.MigrationsTable)
|
||||
if version >= 0 || (version == database.NilVersion && dirty) {
|
||||
statements = append(statements, gorqlite.ParameterizedStatement{
|
||||
Query: insertQuery,
|
||||
Arguments: []interface{}{
|
||||
version,
|
||||
dirty,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
wr, err := r.db.WriteParameterized(statements)
|
||||
if err != nil {
|
||||
for i, res := range wr {
|
||||
if res.Err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(statements[i].Query)}
|
||||
}
|
||||
}
|
||||
|
||||
// if somehow we're still here, return the original error with combined queries
|
||||
return &database.Error{OrigErr: err, Query: []byte(deleteQuery + "\n" + insertQuery)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (r *Rqlite) Version() (version int, dirty bool, err error) {
|
||||
query := "SELECT version, dirty FROM " + r.config.MigrationsTable + " LIMIT 1"
|
||||
|
||||
qr, err := r.db.QueryOne(query)
|
||||
if err != nil {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
|
||||
if !qr.Next() {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
|
||||
if err := qr.Scan(&version, &dirty); err != nil {
|
||||
return database.NilVersion, false, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
return version, dirty, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (r *Rqlite) Drop() error {
|
||||
query := `SELECT name FROM sqlite_master WHERE type = 'table'`
|
||||
|
||||
tables, err := r.db.QueryOne(query)
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
statements := make([]string, 0)
|
||||
for tables.Next() {
|
||||
var tableName string
|
||||
if err := tables.Scan(&tableName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tableName) > 0 {
|
||||
statement := fmt.Sprintf(`DROP TABLE %s`, tableName)
|
||||
statements = append(statements, statement)
|
||||
}
|
||||
}
|
||||
|
||||
// return if nothing to do
|
||||
if len(statements) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
wr, err := r.db.Write(statements)
|
||||
if err != nil {
|
||||
for i, res := range wr {
|
||||
if res.Err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(statements[i])}
|
||||
}
|
||||
}
|
||||
|
||||
// if somehow we're still here, return the original error with combined queries
|
||||
return &database.Error{OrigErr: err, Query: []byte(strings.Join(statements, "\n"))}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseUrl(url string) (*nurl.URL, *Config, error) {
|
||||
parsedUrl, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
config, err := parseConfigFromQuery(parsedUrl.Query())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if parsedUrl.Scheme != "rqlite" {
|
||||
return nil, nil, fmt.Errorf("bad scheme: %w", ErrBadConfig)
|
||||
}
|
||||
|
||||
// adapt from rqlite to http/https schemes
|
||||
if config.ConnectInsecure {
|
||||
parsedUrl.Scheme = "http"
|
||||
} else {
|
||||
parsedUrl.Scheme = "https"
|
||||
}
|
||||
|
||||
filteredUrl := migrate.FilterCustomQuery(parsedUrl)
|
||||
|
||||
return filteredUrl, config, nil
|
||||
}
|
||||
|
||||
func parseConfigFromQuery(queryVals nurl.Values) (*Config, error) {
|
||||
c := Config{
|
||||
ConnectInsecure: DefaultConnectInsecure,
|
||||
MigrationsTable: DefaultMigrationsTable,
|
||||
}
|
||||
|
||||
migrationsTable := queryVals.Get("x-migrations-table")
|
||||
if migrationsTable != "" {
|
||||
if strings.HasPrefix(migrationsTable, "sqlite_") {
|
||||
return nil, fmt.Errorf("invalid value for x-migrations-table: %w", ErrBadConfig)
|
||||
}
|
||||
c.MigrationsTable = migrationsTable
|
||||
}
|
||||
|
||||
connectInsecureStr := queryVals.Get("x-connect-insecure")
|
||||
if connectInsecureStr != "" {
|
||||
connectInsecure, err := strconv.ParseBool(connectInsecureStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value for x-connect-insecure: %w", ErrBadConfig)
|
||||
}
|
||||
c.ConnectInsecure = connectInsecure
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package rqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/rqlite/gorqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
var defaultPort uint16 = 4001
|
||||
|
||||
var opts = dktest.Options{
|
||||
Env: map[string]string{"NODE_ID": "1"},
|
||||
PortRequired: true,
|
||||
ReadyFunc: isReady,
|
||||
}
|
||||
var specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "rqlite/rqlite:7.21.4", Options: opts},
|
||||
{ImageName: "rqlite/rqlite:8.0.6", Options: opts},
|
||||
{ImageName: "rqlite/rqlite:8.11.1", Options: opts},
|
||||
{ImageName: "rqlite/rqlite:8.12.3", Options: opts},
|
||||
}
|
||||
|
||||
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
fmt.Println("error getting port")
|
||||
return false
|
||||
}
|
||||
|
||||
statusString := fmt.Sprintf("http://%s:%s/status", ip, port)
|
||||
fmt.Println(statusString)
|
||||
|
||||
var readyResp struct {
|
||||
Store struct {
|
||||
Ready bool `json:"ready"`
|
||||
} `json:"store"`
|
||||
}
|
||||
|
||||
resp, err := http.Get(statusString)
|
||||
if err != nil {
|
||||
fmt.Println("error getting status")
|
||||
return false
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
fmt.Println("statusCode != 200")
|
||||
return false
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("error reading body")
|
||||
return false
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &readyResp); err != nil {
|
||||
fmt.Println("error unmarshaling body")
|
||||
return false
|
||||
}
|
||||
|
||||
return readyResp.Store.Ready
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
connectString := fmt.Sprintf("rqlite://%s:%s?level=strong&disableClusterDiscovery=true&x-connect-insecure=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
|
||||
r := &Rqlite{}
|
||||
d, err := r.Open(connectString)
|
||||
assert.NoError(t, err)
|
||||
|
||||
dt.Test(t, d, []byte("CREATE TABLE t (Qty int, Name string);"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
connectString := fmt.Sprintf("rqlite://%s:%s?level=strong&disableClusterDiscovery=true&x-connect-insecure=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
|
||||
driver, err := OpenURL(connectString)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
if err := driver.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://./examples/migrations",
|
||||
"ql", driver)
|
||||
assert.NoError(t, err)
|
||||
|
||||
dt.TestMigrate(t, m)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBadConnectInsecureParam(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
connectString := fmt.Sprintf("rqlite://%s:%s?x-connect-insecure=foo", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
|
||||
_, err = OpenURL(connectString)
|
||||
assert.ErrorIs(t, err, ErrBadConfig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBadProtocol(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
connectString := fmt.Sprintf("postgres://%s:%s/database", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
|
||||
_, err = OpenURL(connectString)
|
||||
assert.ErrorIs(t, err, ErrBadConfig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoConfig(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// gorqlite expects http(s) schemes
|
||||
connectString := fmt.Sprintf("http://%s:%s?level=strong&disableClusterDiscovery=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
db, err := gorqlite.Open(connectString)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = WithInstance(db, nil)
|
||||
assert.ErrorIs(t, err, ErrNilConfig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithInstanceEmptyConfig(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// gorqlite expects http(s) schemes
|
||||
connectString := fmt.Sprintf("http://%s:%s?level=strong&disableClusterDiscovery=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
db, err := gorqlite.Open(connectString)
|
||||
assert.NoError(t, err)
|
||||
|
||||
driver, err := WithInstance(db, &Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
if err := driver.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://./examples/migrations",
|
||||
"ql", driver)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("UP")
|
||||
err = m.Up()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.QueryOne(fmt.Sprintf("SELECT * FROM %s", DefaultMigrationsTable))
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("DOWN")
|
||||
err = m.Down()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrationTable(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// gorqlite expects http(s) schemes
|
||||
connectString := fmt.Sprintf("http://%s:%s?level=strong&disableClusterDiscovery=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
db, err := gorqlite.Open(connectString)
|
||||
assert.NoError(t, err)
|
||||
|
||||
config := Config{MigrationsTable: "my_migration_table"}
|
||||
driver, err := WithInstance(db, &config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
if err := driver.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://./examples/migrations",
|
||||
"ql", driver)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("UP")
|
||||
err = m.Up()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.QueryOne(fmt.Sprintf("SELECT * FROM %s", config.MigrationsTable))
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.WriteOne(`INSERT INTO pets (name, predator) VALUES ("franklin", true)`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := db.QueryOne(`SELECT name, predator FROM pets LIMIT 1`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_ = res.Next()
|
||||
|
||||
// make sure we can use the migrated table
|
||||
var petName string
|
||||
var petPredator int
|
||||
err = res.Scan(&petName, &petPredator)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, petName, "franklin")
|
||||
assert.Equal(t, petPredator, 1)
|
||||
|
||||
t.Log("DOWN")
|
||||
err = m.Down()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.QueryOne(fmt.Sprintf("SELECT * FROM %s", config.MigrationsTable))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseUrl(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
passedUrl string
|
||||
expectedUrl string
|
||||
expectedConfig *Config
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
"defaults",
|
||||
"rqlite://localhost:4001",
|
||||
"https://localhost:4001",
|
||||
&Config{ConnectInsecure: DefaultConnectInsecure, MigrationsTable: DefaultMigrationsTable},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"configure migration table",
|
||||
"rqlite://localhost:4001?x-migrations-table=foo",
|
||||
"https://localhost:4001",
|
||||
&Config{ConnectInsecure: DefaultConnectInsecure, MigrationsTable: "foo"},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"configure connect insecure",
|
||||
"rqlite://localhost:4001?x-connect-insecure=true",
|
||||
"http://localhost:4001",
|
||||
&Config{ConnectInsecure: true, MigrationsTable: DefaultMigrationsTable},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"invalid migration table",
|
||||
"rqlite://localhost:4001?x-migrations-table=sqlite_bar",
|
||||
"",
|
||||
nil,
|
||||
"invalid value for x-migrations-table: bad parameter",
|
||||
},
|
||||
{
|
||||
"invalid connect insecure",
|
||||
"rqlite://localhost:4001?x-connect-insecure=baz",
|
||||
"",
|
||||
nil,
|
||||
"invalid value for x-connect-insecure: bad parameter",
|
||||
},
|
||||
{
|
||||
"invalid url",
|
||||
string([]byte{0x7f}),
|
||||
"",
|
||||
nil,
|
||||
"parse \"\\x7f\": net/url: invalid control character in URL",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actualUrl, actualConfig, actualErr := parseUrl(tt.passedUrl)
|
||||
if tt.expectedUrl != "" {
|
||||
assert.Equal(t, tt.expectedUrl, actualUrl.String())
|
||||
} else {
|
||||
assert.Nil(t, actualUrl)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedConfig, actualConfig)
|
||||
|
||||
if tt.expectedErr == "" {
|
||||
assert.NoError(t, actualErr)
|
||||
} else {
|
||||
assert.EqualError(t, actualErr, tt.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,15 @@ package snowflake
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/lib/pq"
|
||||
sf "github.com/snowflakedb/gosnowflake"
|
||||
)
|
||||
@@ -159,21 +158,21 @@ func (p *Snowflake) Close() error {
|
||||
}
|
||||
|
||||
func (p *Snowflake) Lock() error {
|
||||
if !p.isLocked.CAS(false, true) {
|
||||
if !p.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Snowflake) Unlock() error {
|
||||
if !p.isLocked.CAS(true, false) {
|
||||
if !p.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Snowflake) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -207,7 +206,7 @@ func (p *Snowflake) Run(migration io.Reader) error {
|
||||
|
||||
func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
|
||||
// replace crlf with lf
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
// 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) {
|
||||
@@ -249,7 +248,7 @@ func (p *Snowflake) SetVersion(version int, dirty bool) error {
|
||||
query := `DELETE FROM "` + p.config.MigrationsTable + `"`
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -263,7 +262,7 @@ func (p *Snowflake) SetVersion(version int, dirty bool) error {
|
||||
` + strconv.FormatBool(dirty) + `)`
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -305,7 +304,7 @@ func (p *Snowflake) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -347,11 +346,7 @@ func (p *Snowflake) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ as described in [README.md#database-urls](../../README.md#database-urls)
|
||||
## DDL with comments
|
||||
|
||||
At the moment the GCP Spanner backed does not seem to allow for comments (See https://issuetracker.google.com/issues/159730604)
|
||||
so in order to be able to use migration with DDL containing comments `x-clean-stamements` is required
|
||||
so in order to be able to use migration with DDL containing comments `x-clean-statements` is required
|
||||
|
||||
## Multiple statements
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package spanner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
nurl "net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"cloud.google.com/go/spanner"
|
||||
sdb "cloud.google.com/go/spanner/admin/database/apiv1"
|
||||
@@ -20,10 +19,8 @@ import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
uatomic "go.uber.org/atomic"
|
||||
adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
|
||||
"google.golang.org/api/iterator"
|
||||
adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -34,11 +31,6 @@ func init() {
|
||||
// DefaultMigrationsTable is used if no custom table is specified
|
||||
const DefaultMigrationsTable = "SchemaMigrations"
|
||||
|
||||
const (
|
||||
unlockedVal = 0
|
||||
lockedVal = 1
|
||||
)
|
||||
|
||||
// Driver errors
|
||||
var (
|
||||
ErrNilConfig = errors.New("no config")
|
||||
@@ -66,7 +58,7 @@ type Spanner struct {
|
||||
|
||||
config *Config
|
||||
|
||||
lock *uatomic.Uint32
|
||||
lock atomic.Bool
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
@@ -98,7 +90,6 @@ func WithInstance(instance *DB, config *Config) (database.Driver, error) {
|
||||
sx := &Spanner{
|
||||
db: instance,
|
||||
config: config,
|
||||
lock: uatomic.NewUint32(unlockedVal),
|
||||
}
|
||||
|
||||
if err := sx.ensureVersionTable(); err != nil {
|
||||
@@ -155,7 +146,7 @@ func (s *Spanner) Close() error {
|
||||
// Lock implements database.Driver but doesn't do anything because Spanner only
|
||||
// enqueues the UpdateDatabaseDdlRequest.
|
||||
func (s *Spanner) Lock() error {
|
||||
if swapped := s.lock.CAS(unlockedVal, lockedVal); swapped {
|
||||
if swapped := s.lock.CompareAndSwap(false, true); swapped {
|
||||
return nil
|
||||
}
|
||||
return ErrLockHeld
|
||||
@@ -163,7 +154,7 @@ func (s *Spanner) Lock() error {
|
||||
|
||||
// Unlock implements database.Driver but no action required, see Lock.
|
||||
func (s *Spanner) Unlock() error {
|
||||
if swapped := s.lock.CAS(lockedVal, unlockedVal); swapped {
|
||||
if swapped := s.lock.CompareAndSwap(true, false); swapped {
|
||||
return nil
|
||||
}
|
||||
return ErrLockNotHeld
|
||||
@@ -171,7 +162,7 @@ func (s *Spanner) Unlock() error {
|
||||
|
||||
// Run implements database.Driver
|
||||
func (s *Spanner) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -307,11 +298,7 @@ func (s *Spanner) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := s.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -2,17 +2,16 @@ package sqlcipher
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
_ "github.com/mutecomm/go-sqlcipher/v4"
|
||||
)
|
||||
|
||||
@@ -73,11 +72,7 @@ func (m *Sqlite) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -141,7 +136,7 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -178,21 +173,21 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
|
||||
func (m *Sqlite) Lock() error {
|
||||
if !m.isLocked.CAS(false, true) {
|
||||
if !m.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Unlock() error {
|
||||
if !m.isLocked.CAS(true, false) {
|
||||
if !m.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -211,7 +206,7 @@ func (m *Sqlite) executeQuery(query string) error {
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -246,7 +241,7 @@ func (m *Sqlite) SetVersion(version int, dirty bool) error {
|
||||
query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, m.config.MigrationsTable)
|
||||
if _, err := tx.Exec(query, version, dirty); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
@@ -73,11 +72,7 @@ func (m *Sqlite) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -141,7 +136,7 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -178,21 +173,21 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
|
||||
func (m *Sqlite) Lock() error {
|
||||
if !m.isLocked.CAS(false, true) {
|
||||
if !m.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Unlock() error {
|
||||
if !m.isLocked.CAS(true, false) {
|
||||
if !m.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -211,7 +206,7 @@ func (m *Sqlite) executeQuery(query string) error {
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -246,7 +241,7 @@ func (m *Sqlite) SetVersion(version int, dirty bool) error {
|
||||
query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, m.config.MigrationsTable)
|
||||
if _, err := tx.Exec(query, version, dirty); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ package sqlite3
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
@@ -73,11 +72,7 @@ func (m *Sqlite) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -141,7 +136,7 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -178,21 +173,21 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
|
||||
func (m *Sqlite) Lock() error {
|
||||
if !m.isLocked.CAS(false, true) {
|
||||
if !m.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Unlock() error {
|
||||
if !m.isLocked.CAS(true, false) {
|
||||
if !m.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -211,7 +206,7 @@ func (m *Sqlite) executeQuery(query string) error {
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -246,7 +241,7 @@ func (m *Sqlite) SetVersion(version int, dirty bool) error {
|
||||
query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, m.config.MigrationsTable)
|
||||
if _, err := tx.Exec(query, version, dirty); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
| `app+name` || The application name (default is go-mssqldb). |
|
||||
| `useMsi` | | `true` - Use Azure MSI Authentication for connecting to Sql Server. Must be running from an Azure VM/an instance with MSI enabled. `false` - Use password authentication (Default). See [here for Azure MSI Auth details](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-connect-msi). NOTE: Since this cannot be tested locally, this is not officially supported.
|
||||
|
||||
See https://github.com/denisenkom/go-mssqldb for full parameter list.
|
||||
See https://github.com/microsoft/go-mssqldb for full parameter list.
|
||||
|
||||
## Driver Support
|
||||
|
||||
### Which go-mssqldb driver to us?
|
||||
|
||||
Please note that the deprecated `mssql` driver is not supported. Please use the newer `sqlserver` driver.
|
||||
See https://github.com/denisenkom/go-mssqldb#deprecated for more information.
|
||||
See https://github.com/microsoft/go-mssqldb#deprecated for more information.
|
||||
|
||||
### Official Support by migrate
|
||||
|
||||
|
||||
@@ -3,20 +3,18 @@ package sqlserver
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
mssql "github.com/denisenkom/go-mssqldb" // mssql support
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
mssql "github.com/microsoft/go-mssqldb" // mssql support
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -31,10 +29,10 @@ var (
|
||||
ErrNoDatabaseName = fmt.Errorf("no database name")
|
||||
ErrNoSchema = fmt.Errorf("no schema")
|
||||
ErrDatabaseDirty = fmt.Errorf("database is dirty")
|
||||
ErrMultipleAuthOptionsPassed = fmt.Errorf("both password and useMsi=true were passed.")
|
||||
ErrMultipleAuthOptionsPassed = fmt.Errorf("both password and useMsi=true were passed")
|
||||
)
|
||||
|
||||
var lockErrorMap = map[mssql.ReturnStatus]string{
|
||||
var lockErrorMap = map[int]string{
|
||||
-1: "The lock request timed out.",
|
||||
-2: "The lock request was canceled.",
|
||||
-3: "The lock request was chosen as a deadlock victim.",
|
||||
@@ -199,18 +197,24 @@ func (ss *SQLServer) Lock() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// This will either obtain the lock immediately and return true,
|
||||
// or return false if the lock cannot be acquired immediately.
|
||||
// This will block until the lock is acquired.
|
||||
// MS Docs: sp_getapplock: https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-getapplock-transact-sql?view=sql-server-2017
|
||||
query := `EXEC sp_getapplock @Resource = @p1, @LockMode = 'Update', @LockOwner = 'Session', @LockTimeout = 0`
|
||||
query := `
|
||||
DECLARE @lockResult int;
|
||||
EXEC @lockResult = sp_getapplock @Resource = @p1, @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = -1;
|
||||
SELECT @lockResult;`
|
||||
|
||||
var status mssql.ReturnStatus
|
||||
if _, err = ss.conn.ExecContext(context.Background(), query, aid, &status); err == nil && status > -1 {
|
||||
var status int
|
||||
if err = ss.conn.QueryRowContext(context.Background(), query, aid).Scan(&status); err == nil && status > -1 {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)}
|
||||
} else {
|
||||
return &database.Error{Err: fmt.Sprintf("try lock failed with error %v: %v", status, lockErrorMap[status]), Query: []byte(query)}
|
||||
errorDescription, ok := lockErrorMap[status]
|
||||
if !ok {
|
||||
errorDescription = "Unknown error"
|
||||
}
|
||||
return &database.Error{Err: fmt.Sprintf("try lock failed with error %v: %v", status, errorDescription), Query: []byte(query)}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -235,7 +239,7 @@ func (ss *SQLServer) Unlock() error {
|
||||
|
||||
// Run the migrations for the database
|
||||
func (ss *SQLServer) Run(migration io.Reader) error {
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -264,10 +268,10 @@ func (ss *SQLServer) SetVersion(version int, dirty bool) error {
|
||||
return &database.Error{OrigErr: err, Err: "transaction start failed"}
|
||||
}
|
||||
|
||||
query := `TRUNCATE TABLE "` + ss.config.MigrationsTable + `"`
|
||||
query := `TRUNCATE TABLE ` + ss.getMigrationTable()
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -280,10 +284,10 @@ func (ss *SQLServer) SetVersion(version int, dirty bool) error {
|
||||
if dirty {
|
||||
dirtyBit = 1
|
||||
}
|
||||
query = `INSERT INTO "` + ss.config.MigrationsTable + `" (version, dirty) VALUES (@p1, @p2)`
|
||||
query = `INSERT INTO ` + ss.getMigrationTable() + ` (version, dirty) VALUES (@p1, @p2)`
|
||||
if _, err := tx.Exec(query, version, dirtyBit); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
err = errors.Join(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -298,7 +302,7 @@ func (ss *SQLServer) SetVersion(version int, dirty bool) error {
|
||||
|
||||
// Version of the current database state
|
||||
func (ss *SQLServer) Version() (version int, dirty bool, err error) {
|
||||
query := `SELECT TOP 1 version, dirty FROM "` + ss.config.MigrationsTable + `"`
|
||||
query := `SELECT TOP 1 version, dirty FROM ` + ss.getMigrationTable()
|
||||
err = ss.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
@@ -355,21 +359,17 @@ func (ss *SQLServer) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := ss.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
query := `IF NOT EXISTS
|
||||
(SELECT *
|
||||
FROM sysobjects
|
||||
WHERE id = object_id(N'[dbo].[` + ss.config.MigrationsTable + `]')
|
||||
WHERE id = object_id(N'` + ss.getMigrationTable() + `')
|
||||
AND OBJECTPROPERTY(id, N'IsUserTable') = 1
|
||||
)
|
||||
CREATE TABLE ` + ss.config.MigrationsTable + ` ( version BIGINT PRIMARY KEY NOT NULL, dirty BIT NOT NULL );`
|
||||
CREATE TABLE ` + ss.getMigrationTable() + ` ( version BIGINT PRIMARY KEY NOT NULL, dirty BIT NOT NULL );`
|
||||
|
||||
if _, err = ss.conn.ExecContext(context.Background(), query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
@@ -378,6 +378,10 @@ func (ss *SQLServer) ensureVersionTable() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *SQLServer) getMigrationTable() string {
|
||||
return fmt.Sprintf("[%s].[%s]", ss.config.SchemaName, ss.config.MigrationsTable)
|
||||
}
|
||||
|
||||
func getMSITokenProvider(resource string) (func() (string, error), error) {
|
||||
msi, err := adal.NewServicePrincipalTokenFromManagedIdentity(resource, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
@@ -25,27 +24,16 @@ const defaultPort = 1433
|
||||
const saPassword = "Root1234"
|
||||
|
||||
var (
|
||||
sqlEdgeOpts = dktest.Options{
|
||||
Env: map[string]string{"ACCEPT_EULA": "Y", "MSSQL_SA_PASSWORD": saPassword},
|
||||
PortBindings: map[nat.Port][]nat.PortBinding{
|
||||
nat.Port(fmt.Sprintf("%d/tcp", defaultPort)): {
|
||||
nat.PortBinding{
|
||||
HostIP: "0.0.0.0",
|
||||
HostPort: "0/tcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
PortRequired: true, ReadyFunc: isReady, PullTimeout: 2 * time.Minute,
|
||||
}
|
||||
sqlServerOpts = dktest.Options{
|
||||
Env: map[string]string{"ACCEPT_EULA": "Y", "MSSQL_SA_PASSWORD": saPassword, "MSSQL_PID": "Express"},
|
||||
PortRequired: true, ReadyFunc: isReady, PullTimeout: 2 * time.Minute,
|
||||
}
|
||||
// Container versions: https://mcr.microsoft.com/v2/mssql/server/tags/list
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "mcr.microsoft.com/azure-sql-edge:latest", Options: sqlEdgeOpts},
|
||||
{ImageName: "mcr.microsoft.com/mssql/server:2017-latest", Options: sqlServerOpts},
|
||||
{ImageName: "mcr.microsoft.com/mssql/server:2022-latest", Options: sqlServerOpts},
|
||||
{ImageName: "mcr.microsoft.com/mssql/server:2019-latest", Options: sqlServerOpts},
|
||||
// Add back support for 2017 version once the image is fixed: https://github.com/microsoft/mssql-docker/issues/899
|
||||
// {ImageName: "mcr.microsoft.com/mssql/server:2017-latest", Options: sqlServerOpts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -91,11 +79,31 @@ func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
|
||||
func SkipIfUnsupportedArch(t *testing.T, c dktest.ContainerInfo) {
|
||||
if strings.Contains(c.ImageName, "mssql") && !strings.HasPrefix(runtime.GOARCH, "amd") {
|
||||
t.Skip(fmt.Sprintf("Image %s is not supported on arch %s", c.ImageName, runtime.GOARCH))
|
||||
t.Skipf("Image %s is not supported on arch %s", c.ImageName, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Run("test", test)
|
||||
t.Run("testMigrate", testMigrate)
|
||||
t.Run("testMultiStatement", testMultiStatement)
|
||||
t.Run("testErrorParsing", testErrorParsing)
|
||||
t.Run("testLockWorks", testLockWorks)
|
||||
t.Run("testMsiTrue", testMsiTrue)
|
||||
t.Run("testOpenWithPasswordAndMSI", testOpenWithPasswordAndMSI)
|
||||
t.Run("testMsiFalse", testMsiFalse)
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, spec := range specs {
|
||||
t.Log("Cleaning up ", spec.ImageName)
|
||||
if err := spec.Cleanup(); err != nil {
|
||||
t.Error("Error removing ", spec.ImageName, "error:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -120,7 +128,7 @@ func Test(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
func testMigrate(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -149,7 +157,7 @@ func TestMigrate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultiStatement(t *testing.T) {
|
||||
func testMultiStatement(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -183,7 +191,7 @@ func TestMultiStatement(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorParsing(t *testing.T) {
|
||||
func testErrorParsing(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -215,7 +223,7 @@ func TestErrorParsing(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockWorks(t *testing.T) {
|
||||
func testLockWorks(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -254,7 +262,7 @@ func TestLockWorks(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsiTrue(t *testing.T) {
|
||||
func testMsiTrue(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -271,7 +279,7 @@ func TestMsiTrue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenWithPasswordAndMSI(t *testing.T) {
|
||||
func testOpenWithPasswordAndMSI(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -303,7 +311,7 @@ func TestOpenWithPasswordAndMSI(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsiFalse(t *testing.T) {
|
||||
func testMsiFalse(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package stub
|
||||
|
||||
import (
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
)
|
||||
@@ -50,21 +49,21 @@ func (s *Stub) Close() error {
|
||||
}
|
||||
|
||||
func (s *Stub) Lock() error {
|
||||
if !s.isLocked.CAS(false, true) {
|
||||
if !s.isLocked.CompareAndSwap(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) Unlock() error {
|
||||
if !s.isLocked.CAS(true, false) {
|
||||
if !s.isLocked.CompareAndSwap(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) Run(migration io.Reader) error {
|
||||
m, err := ioutil.ReadAll(migration)
|
||||
m, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestLockAndUnlock(t *testing.T, d database.Driver) {
|
||||
case <-done:
|
||||
return
|
||||
case <-timeout:
|
||||
errs <- fmt.Errorf("Timeout after 15 seconds. Looks like a deadlock in Lock/UnLock.\n%#v", d)
|
||||
errs <- fmt.Errorf("timeout after 15 seconds, looks like a deadlock in Lock/UnLock\n%#v", d)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,6 @@ func TestDrop(t *testing.T, d database.Driver) {
|
||||
}
|
||||
|
||||
func TestSetVersion(t *testing.T, d database.Driver) {
|
||||
// nolint:maligned
|
||||
testCases := []struct {
|
||||
name string
|
||||
version int
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@ package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go.uber.org/atomic"
|
||||
"hash/crc32"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const advisoryLockIDSalt uint = 1486364155
|
||||
@@ -21,7 +21,7 @@ func GenerateAdvisoryLockId(databaseName string, additionalNames ...string) (str
|
||||
|
||||
// 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) {
|
||||
if !lock.CompareAndSwap(o, n) {
|
||||
return casErr
|
||||
}
|
||||
if err := f(); err != nil {
|
||||
|
||||
@@ -2,7 +2,7 @@ package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go.uber.org/atomic"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
lock *atomic.Bool
|
||||
lock bool
|
||||
from bool
|
||||
to bool
|
||||
expectLock bool
|
||||
@@ -63,7 +63,7 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Test positive CAS lock",
|
||||
lock: atomic.NewBool(false),
|
||||
lock: false,
|
||||
from: false,
|
||||
to: true,
|
||||
expectLock: true,
|
||||
@@ -72,7 +72,7 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Test negative CAS lock",
|
||||
lock: atomic.NewBool(true),
|
||||
lock: true,
|
||||
from: false,
|
||||
to: true,
|
||||
expectLock: true,
|
||||
@@ -81,7 +81,7 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Test negative with callback lock",
|
||||
lock: atomic.NewBool(false),
|
||||
lock: false,
|
||||
from: false,
|
||||
to: true,
|
||||
expectLock: false,
|
||||
@@ -92,13 +92,15 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := CasRestoreOnErr(tc.lock, tc.from, tc.to, casErr, func() error {
|
||||
var lock atomic.Bool
|
||||
lock.Store(tc.lock)
|
||||
if err := CasRestoreOnErr(&lock, tc.from, tc.to, casErr, func() error {
|
||||
return tc.fErr
|
||||
}); err != tc.expectError {
|
||||
t.Error("Incorrect error value returned")
|
||||
}
|
||||
|
||||
if tc.lock.Load() != tc.expectLock {
|
||||
if lock.Load() != tc.expectLock {
|
||||
t.Error("Incorrect state of lock")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# yugabytedb
|
||||
|
||||
`yugabytedb://user:password@host:port/dbname?query` (`yugabyte://`, and `ysql://` work, too)
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
|
||||
| `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock |
|
||||
| `x-force-lock` | `ForceLock` | Force lock acquisition to fix faulty migrations which may not have released the schema lock (Boolean, default is `false`) |
|
||||
| `x-max-retries` | `MaxRetries` | How many times retry queries on retryable errors (40001, 40P01, 08006, XX000). Default is 10 |
|
||||
| `x-max-retry-interval` | `MaxRetryInterval` | Interval between retries increases exponentially. This option specifies maximum duration between retries. Default is 15s |
|
||||
| `x-max-retry-elapsed-time` | `MaxRetryElapsedTime` | Total retries timeout. Default is 30s |
|
||||
| `dbname` | `DatabaseName` | The name of the database to connect to |
|
||||
| `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) |
|
||||
| `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) |
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE users (
|
||||
user_id integer unique,
|
||||
name varchar(40),
|
||||
email varchar(40)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD COLUMN city varchar(100);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS users_email_index;
|
||||
@@ -0,0 +1,3 @@
|
||||
CREATE UNIQUE INDEX users_email_index ON users (email);
|
||||
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS books;
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE books (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
author varchar(40)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE movies (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
director varchar(40)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1,475 @@
|
||||
package yugabytedb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/jackc/pgconn"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMaxRetryInterval = time.Second * 15
|
||||
DefaultMaxRetryElapsedTime = time.Second * 30
|
||||
DefaultMaxRetries = 10
|
||||
DefaultMigrationsTable = "migrations"
|
||||
DefaultLockTable = "migrations_locks"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNilConfig = errors.New("no config")
|
||||
ErrNoDatabaseName = errors.New("no database name")
|
||||
ErrMaxRetriesExceeded = errors.New("max retries exceeded")
|
||||
)
|
||||
|
||||
func init() {
|
||||
db := YugabyteDB{}
|
||||
database.Register("yugabyte", &db)
|
||||
database.Register("yugabytedb", &db)
|
||||
database.Register("ysql", &db)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
MigrationsTable string
|
||||
LockTable string
|
||||
ForceLock bool
|
||||
DatabaseName string
|
||||
MaxRetryInterval time.Duration
|
||||
MaxRetryElapsedTime time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
type YugabyteDB struct {
|
||||
db *sql.DB
|
||||
isLocked atomic.Bool
|
||||
|
||||
// Open and WithInstance need to guarantee that config is never nil
|
||||
config *Config
|
||||
}
|
||||
|
||||
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
|
||||
if config == nil {
|
||||
return nil, ErrNilConfig
|
||||
}
|
||||
|
||||
if err := instance.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.DatabaseName == "" {
|
||||
query := `SELECT current_database()`
|
||||
var databaseName string
|
||||
if err := instance.QueryRow(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 len(config.MigrationsTable) == 0 {
|
||||
config.MigrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
if len(config.LockTable) == 0 {
|
||||
config.LockTable = DefaultLockTable
|
||||
}
|
||||
|
||||
if config.MaxRetryInterval == 0 {
|
||||
config.MaxRetryInterval = DefaultMaxRetryInterval
|
||||
}
|
||||
|
||||
if config.MaxRetryElapsedTime == 0 {
|
||||
config.MaxRetryElapsedTime = DefaultMaxRetryElapsedTime
|
||||
}
|
||||
|
||||
if config.MaxRetries == 0 {
|
||||
config.MaxRetries = DefaultMaxRetries
|
||||
}
|
||||
|
||||
px := &YugabyteDB{
|
||||
db: instance,
|
||||
config: config,
|
||||
}
|
||||
|
||||
// ensureVersionTable is a locking operation, so we need to ensureLockTable before we ensureVersionTable.
|
||||
if err := px.ensureLockTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := px.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) Open(dbURL string) (database.Driver, error) {
|
||||
purl, err := url.Parse(dbURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// As YugabyteDB uses the postgres protocol, and 'postgres' is already a registered database, we need to replace the
|
||||
// connect prefix, with the actual protocol, so that the library can differentiate between the implementations
|
||||
re := regexp.MustCompile("^(yugabyte(db)?|ysql)")
|
||||
connectString := re.ReplaceAllString(migrate.FilterCustomQuery(purl).String(), "postgres")
|
||||
|
||||
db, err := sql.Open("postgres", connectString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
migrationsTable := purl.Query().Get("x-migrations-table")
|
||||
if len(migrationsTable) == 0 {
|
||||
migrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
lockTable := purl.Query().Get("x-lock-table")
|
||||
if len(lockTable) == 0 {
|
||||
lockTable = DefaultLockTable
|
||||
}
|
||||
|
||||
forceLockQuery := purl.Query().Get("x-force-lock")
|
||||
forceLock, err := strconv.ParseBool(forceLockQuery)
|
||||
if err != nil {
|
||||
forceLock = false
|
||||
}
|
||||
|
||||
maxIntervalStr := purl.Query().Get("x-max-retry-interval")
|
||||
maxInterval, err := time.ParseDuration(maxIntervalStr)
|
||||
if err != nil {
|
||||
maxInterval = DefaultMaxRetryInterval
|
||||
}
|
||||
|
||||
maxElapsedTimeStr := purl.Query().Get("x-max-retry-elapsed-time")
|
||||
maxElapsedTime, err := time.ParseDuration(maxElapsedTimeStr)
|
||||
if err != nil {
|
||||
maxElapsedTime = DefaultMaxRetryElapsedTime
|
||||
}
|
||||
|
||||
maxRetriesStr := purl.Query().Get("x-max-retries")
|
||||
maxRetries, err := strconv.Atoi(maxRetriesStr)
|
||||
if err != nil {
|
||||
maxRetries = DefaultMaxRetries
|
||||
}
|
||||
|
||||
px, err := WithInstance(db, &Config{
|
||||
DatabaseName: purl.Path,
|
||||
MigrationsTable: migrationsTable,
|
||||
LockTable: lockTable,
|
||||
ForceLock: forceLock,
|
||||
MaxRetryInterval: maxInterval,
|
||||
MaxRetryElapsedTime: maxElapsedTime,
|
||||
MaxRetries: maxRetries,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) Close() error {
|
||||
return c.db.Close()
|
||||
}
|
||||
|
||||
// Locking is done manually with a separate lock table. Implementing advisory locks in YugabyteDB is being discussed
|
||||
// See: https://github.com/yugabyte/yugabyte-db/issues/3642
|
||||
func (c *YugabyteDB) Lock() error {
|
||||
return database.CasRestoreOnErr(&c.isLocked, false, true, database.ErrLocked, func() (err error) {
|
||||
return c.doTxWithRetry(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable}, func(tx *sql.Tx) (err error) {
|
||||
aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := "SELECT * FROM " + c.config.LockTable + " WHERE lock_id = $1"
|
||||
rows, err := tx.Query(query, aid)
|
||||
if err != nil {
|
||||
return database.Error{OrigErr: err, Err: "failed to fetch migration lock", Query: []byte(query)}
|
||||
}
|
||||
defer func() {
|
||||
if errClose := rows.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
// If row exists at all, lock is present
|
||||
locked := rows.Next()
|
||||
if locked && !c.config.ForceLock {
|
||||
return database.ErrLocked
|
||||
}
|
||||
|
||||
query = "INSERT INTO " + c.config.LockTable + " (lock_id) VALUES ($1)"
|
||||
if _, err := tx.Exec(query, aid); err != nil {
|
||||
return database.Error{OrigErr: err, Err: "failed to set migration lock", Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Locking is done manually with a separate lock table. Implementing advisory locks in YugabyteDB is being discussed
|
||||
// See: https://github.com/yugabyte/yugabyte-db/issues/3642
|
||||
func (c *YugabyteDB) Unlock() error {
|
||||
return database.CasRestoreOnErr(&c.isLocked, true, false, database.ErrNotLocked, func() (err error) {
|
||||
aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// In the event of an implementation (non-migration) error, it is possible for the lock to not be released. Until
|
||||
// a better locking mechanism is added, a manual purging of the lock table may be required in such circumstances
|
||||
query := "DELETE FROM " + c.config.LockTable + " WHERE lock_id = $1"
|
||||
if _, err := c.db.Exec(query, aid); err != nil {
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
// 42P01 is "UndefinedTableError" in YugabyteDB
|
||||
// https://github.com/yugabyte/yugabyte-db/blob/9c6b8e6beb56eed8eeb357178c0c6b837eb49896/src/postgres/src/backend/utils/errcodes.txt#L366
|
||||
if e.Code == "42P01" {
|
||||
// On drops, the lock table is fully removed; This is fine, and is a valid "unlocked" state for the schema
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return database.Error{OrigErr: err, Err: "failed to release migration lock", Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run migration
|
||||
query := string(migr[:])
|
||||
if _, err := c.db.Exec(query); err != nil {
|
||||
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) SetVersion(version int, dirty bool) error {
|
||||
return c.doTxWithRetry(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable}, func(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(`DELETE FROM "` + c.config.MigrationsTable + `"`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if _, err := tx.Exec(`INSERT INTO "`+c.config.MigrationsTable+`" (version, dirty) VALUES ($1, $2)`, version, dirty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) Version() (version int, dirty bool, err error) {
|
||||
query := `SELECT version, dirty FROM "` + c.config.MigrationsTable + `" LIMIT 1`
|
||||
err = c.db.QueryRow(query).Scan(&version, &dirty)
|
||||
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return database.NilVersion, false, nil
|
||||
|
||||
case err != nil:
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
// 42P01 is "UndefinedTableError" in YugabyteDB
|
||||
// https://github.com/yugabyte/yugabyte-db/blob/9c6b8e6beb56eed8eeb357178c0c6b837eb49896/src/postgres/src/backend/utils/errcodes.txt#L366
|
||||
if e.Code == "42P01" {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
}
|
||||
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
|
||||
default:
|
||||
return version, dirty, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) Drop() (err error) {
|
||||
query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_type='BASE TABLE'`
|
||||
tables, err := c.db.Query(query)
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(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 {
|
||||
for _, t := range tableNames {
|
||||
query = `DROP TABLE IF EXISTS ` + t + ` CASCADE`
|
||||
if _, err := c.db.Exec(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
|
||||
func (c *YugabyteDB) ensureVersionTable() (err error) {
|
||||
if err = c.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := c.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
}()
|
||||
|
||||
// check if migration table exists
|
||||
var count int
|
||||
query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1`
|
||||
if err := c.db.QueryRow(query, c.config.MigrationsTable).Scan(&count); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
if count == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if not, create the empty migration table
|
||||
query = `CREATE TABLE "` + c.config.MigrationsTable + `" (version INT NOT NULL PRIMARY KEY, dirty BOOL NOT NULL)`
|
||||
if _, err := c.db.Exec(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) ensureLockTable() error {
|
||||
// check if lock table exists
|
||||
var count int
|
||||
query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1`
|
||||
if err := c.db.QueryRow(query, c.config.LockTable).Scan(&count); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
if count == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if not, create the empty lock table
|
||||
query = `CREATE TABLE "` + c.config.LockTable + `" (lock_id TEXT NOT NULL PRIMARY KEY)`
|
||||
if _, err := c.db.Exec(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) doTxWithRetry(
|
||||
ctx context.Context,
|
||||
txOpts *sql.TxOptions,
|
||||
fn func(tx *sql.Tx) error,
|
||||
) error {
|
||||
backOff := c.newBackoff(ctx)
|
||||
|
||||
return backoff.Retry(func() error {
|
||||
tx, err := c.db.BeginTx(ctx, txOpts)
|
||||
if err != nil {
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
|
||||
// If we've tried to commit the transaction Rollback just returns sql.ErrTxDone.
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
if errIsRetryable(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
if errIsRetryable(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, backOff)
|
||||
}
|
||||
|
||||
func (c *YugabyteDB) newBackoff(ctx context.Context) backoff.BackOff {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
retrier := backoff.WithMaxRetries(backoff.WithContext(&backoff.ExponentialBackOff{
|
||||
InitialInterval: backoff.DefaultInitialInterval,
|
||||
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
||||
Multiplier: backoff.DefaultMultiplier,
|
||||
MaxInterval: c.config.MaxRetryInterval,
|
||||
MaxElapsedTime: c.config.MaxRetryElapsedTime,
|
||||
Stop: backoff.Stop,
|
||||
Clock: backoff.SystemClock,
|
||||
}, ctx), uint64(c.config.MaxRetries))
|
||||
|
||||
retrier.Reset()
|
||||
|
||||
return retrier
|
||||
}
|
||||
|
||||
func errIsRetryable(err error) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
if !errors.As(err, &pgErr) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Assume that it's safe to retry 08006 and XX000 because we check for lock existence
|
||||
// before creating and lock ID is primary key. Version field in migrations table is primary key too
|
||||
// and delete all versions is an idempotent operation.
|
||||
return pgErr.Code == pgerrcode.SerializationFailure || // optimistic locking conflict
|
||||
pgErr.Code == pgerrcode.DeadlockDetected ||
|
||||
pgErr.Code == pgerrcode.ConnectionFailure || // node down, need to reconnect
|
||||
pgErr.Code == pgerrcode.InternalError // may happen during HA
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package yugabytedb
|
||||
|
||||
// error codes https://github.com/lib/pq/blob/master/error.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
const defaultPort = 5433
|
||||
|
||||
var (
|
||||
opts = dktest.Options{
|
||||
Cmd: []string{"bin/yugabyted", "start", "--daemon=false"},
|
||||
PortRequired: true,
|
||||
ReadyFunc: isReady,
|
||||
Timeout: time.Duration(60) * time.Second,
|
||||
}
|
||||
// Released versions: https://docs.yugabyte.com/preview/releases/release-notes/
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "yugabytedb/yugabyte:2.14.15.0-b57", Options: opts},
|
||||
{ImageName: "yugabytedb/yugabyte:2.20.2.1-b3", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
log.Println("port error:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", fmt.Sprintf("postgres://yugabyte:yugabyte@%v:%v?sslmode=disable", ip, port))
|
||||
if err != nil {
|
||||
log.Println("open error:", err)
|
||||
return false
|
||||
}
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
log.Println("ping error:", err)
|
||||
return false
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
log.Println("close error:", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func createDB(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", fmt.Sprintf("postgres://yugabyte:yugabyte@%v:%v?sslmode=disable", ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err = db.Exec("CREATE DATABASE migrate"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getConnectionString(ip, port string, options ...string) string {
|
||||
options = append(options, "sslmode=disable")
|
||||
|
||||
return fmt.Sprintf("yugabyte://yugabyte:yugabyte@%v:%v/migrate?%s", ip, port, strings.Join(options, "&"))
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Run("test", test)
|
||||
t.Run("testMigrate", testMigrate)
|
||||
t.Run("testMultiStatement", testMultiStatement)
|
||||
t.Run("testFilterCustomQuery", testFilterCustomQuery)
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, spec := range specs {
|
||||
t.Log("Cleaning up ", spec.ImageName)
|
||||
if err := spec.Cleanup(); err != nil {
|
||||
t.Error("Error removing ", spec.ImageName, "error:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, ci dktest.ContainerInfo) {
|
||||
createDB(t, ci)
|
||||
|
||||
ip, port, err := ci.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := getConnectionString(ip, port)
|
||||
c := &YugabyteDB{}
|
||||
d, err := c.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dt.Test(t, d, []byte("SELECT 1"))
|
||||
})
|
||||
}
|
||||
|
||||
func testMigrate(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, ci dktest.ContainerInfo) {
|
||||
createDB(t, ci)
|
||||
|
||||
ip, port, err := ci.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := getConnectionString(ip, port)
|
||||
c := &YugabyteDB{}
|
||||
d, err := c.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "migrate", d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dt.TestMigrate(t, m)
|
||||
})
|
||||
}
|
||||
|
||||
func testMultiStatement(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, ci dktest.ContainerInfo) {
|
||||
createDB(t, ci)
|
||||
|
||||
ip, port, err := ci.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := getConnectionString(ip, port)
|
||||
c := &YugabyteDB{}
|
||||
d, err := c.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);")); err != nil {
|
||||
t.Fatalf("expected err to be nil, got %v", err)
|
||||
}
|
||||
|
||||
// make sure second table exists
|
||||
var exists bool
|
||||
if err := d.(*YugabyteDB).db.QueryRow("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatal("expected table bar to exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testFilterCustomQuery(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, ci dktest.ContainerInfo) {
|
||||
createDB(t, ci)
|
||||
|
||||
ip, port, err := ci.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := getConnectionString(ip, port, "x-custom=foobar")
|
||||
c := &YugabyteDB{}
|
||||
d, err := c.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dt.Test(t, d, []byte("SELECT 1"))
|
||||
})
|
||||
}
|
||||
+28
-2
@@ -1,11 +1,13 @@
|
||||
package dktesting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// ContainerSpec holds Docker testing setup specifications
|
||||
@@ -14,6 +16,30 @@ type ContainerSpec struct {
|
||||
Options dktest.Options
|
||||
}
|
||||
|
||||
// Cleanup cleanups the ContainerSpec after a test run by removing the ContainerSpec's image
|
||||
func (s *ContainerSpec) Cleanup() (retErr error) {
|
||||
// copied from dktest.RunContext()
|
||||
dc, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.41"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := dc.Close(); err != nil && retErr == nil {
|
||||
retErr = fmt.Errorf("error closing Docker client: %w", err)
|
||||
}
|
||||
}()
|
||||
cleanupTimeout := s.Options.CleanupTimeout
|
||||
if cleanupTimeout <= 0 {
|
||||
cleanupTimeout = dktest.DefaultCleanupTimeout
|
||||
}
|
||||
ctx, timeoutCancelFunc := context.WithTimeout(context.Background(), cleanupTimeout)
|
||||
defer timeoutCancelFunc()
|
||||
if _, err := dc.ImageRemove(ctx, s.ImageName, image.RemoveOptions{Force: true, PruneChildren: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParallelTest runs Docker tests in parallel
|
||||
func ParallelTest(t *testing.T, specs []ContainerSpec,
|
||||
testFunc func(*testing.T, dktest.ContainerInfo)) {
|
||||
|
||||
@@ -1,75 +1,225 @@
|
||||
module github.com/golang-migrate/migrate/v4
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.99.0 // indirect
|
||||
cloud.google.com/go/spanner v1.28.0
|
||||
cloud.google.com/go/storage v1.10.0
|
||||
cloud.google.com/go/spanner v1.85.0
|
||||
cloud.google.com/go/storage v1.56.0
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.16
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30 // indirect
|
||||
github.com/aws/aws-sdk-go v1.17.7
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
|
||||
github.com/aws/aws-sdk-go v1.49.6
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.10.0
|
||||
github.com/dhui/dktest v0.3.10
|
||||
github.com/docker/docker v20.10.13+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
|
||||
github.com/dhui/dktest v0.4.6
|
||||
github.com/docker/docker v28.3.3+incompatible
|
||||
github.com/fsouza/fake-gcs-server v1.17.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/gobuffalo/here v0.6.0
|
||||
github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556
|
||||
github.com/gofrs/uuid v4.0.0+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.1.0 // indirect
|
||||
github.com/google/go-github/v39 v39.2.0
|
||||
github.com/gorilla/mux v1.7.4 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jackc/pgconn v1.8.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451
|
||||
github.com/jackc/pgproto3/v2 v2.0.7 // indirect
|
||||
github.com/jackc/pgx/v4 v4.10.1
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/hanzo-ds/go v1.0.1
|
||||
github.com/jackc/pgconn v1.14.3
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
|
||||
github.com/jackc/pgx/v4 v4.18.2
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/ktrysmt/go-bitbucket v0.6.4
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/markbates/pkger v0.15.1
|
||||
github.com/mattn/go-sqlite3 v1.14.10
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/microsoft/go-mssqldb v1.0.0
|
||||
github.com/mutecomm/go-sqlcipher/v4 v4.4.0
|
||||
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8
|
||||
github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/snowflakedb/gosnowflake v1.6.3
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/snowflakedb/gosnowflake v1.6.19
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/xanzy/go-gitlab v0.15.0
|
||||
go.mongodb.org/mongo-driver v1.11.4
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated
|
||||
google.golang.org/api v0.247.0
|
||||
modernc.org/ql v1.0.0
|
||||
modernc.org/sqlite v1.18.1
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.4 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/hanzo-ds/native v0.71.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/montanaflynn/stats v0.6.6 // indirect
|
||||
github.com/paulmach/orb v0.12.0 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.121.6 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
||||
github.com/99designs/keyring v1.2.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/apache/arrow/go/v10 v10.0.1 // indirect
|
||||
github.com/apache/thrift v0.16.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 // indirect
|
||||
github.com/aws/smithy-go v1.13.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v2.0.8+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/handlers v1.4.2 // indirect
|
||||
github.com/gorilla/mux v1.7.4 // indirect
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/k0kubun/pp v2.3.0+incompatible // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/asmfmt v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.15.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.27 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.1 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.3 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect
|
||||
go.mongodb.org/mongo-driver v1.7.0
|
||||
go.uber.org/atomic v1.7.0
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
|
||||
golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf // indirect
|
||||
golang.org/x/tools v0.1.5
|
||||
google.golang.org/api v0.62.0
|
||||
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/b v1.0.0 // indirect
|
||||
modernc.org/cc/v3 v3.36.3 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||
modernc.org/db v1.0.0 // indirect
|
||||
modernc.org/file v1.0.0 // indirect
|
||||
modernc.org/fileutil v1.0.0 // indirect
|
||||
modernc.org/golex v1.0.0 // indirect
|
||||
modernc.org/internal v1.0.0 // indirect
|
||||
modernc.org/libc v1.17.1 // indirect
|
||||
modernc.org/lldb v1.0.0 // indirect
|
||||
modernc.org/ql v1.0.0
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.2.1 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sortutil v1.1.0 // indirect
|
||||
modernc.org/sqlite v1.10.6
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
modernc.org/zappy v1.0.0 // indirect
|
||||
)
|
||||
|
||||
go 1.16
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aws_s3
|
||||
// +build aws_s3
|
||||
|
||||
package cli
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build bitbucket
|
||||
// +build bitbucket
|
||||
|
||||
package cli
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build cassandra
|
||||
// +build cassandra
|
||||
|
||||
package cli
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build clickhouse
|
||||
// +build clickhouse
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
_ "github.com/ClickHouse/clickhouse-go"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/clickhouse"
|
||||
)
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build cockroachdb
|
||||
// +build cockroachdb
|
||||
|
||||
package cli
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user