Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ba9becc5c | ||
|
|
c81e147ad6 | ||
|
|
dbb574c92c | ||
|
|
79472544ff | ||
|
|
b9b66468f5 | ||
|
|
5eaf23f836 | ||
|
|
f25b303a88 | ||
|
|
41431fbdc3 | ||
|
|
780d5551e0 | ||
|
|
71106e5fd2 | ||
|
|
eea81e7876 | ||
|
|
1668f8e6a0 | ||
|
|
10a92d0cc8 | ||
|
|
0775323324 | ||
|
|
14f6d3a92f | ||
|
|
677b5deb09 | ||
|
|
b4047a1b47 | ||
|
|
9db2d011f3 | ||
|
|
aef2747761 | ||
|
|
aa119418b8 | ||
|
|
df6106d10e | ||
|
|
b62d267202 | ||
|
|
d30a9850da | ||
|
|
bea4b82719 | ||
|
|
ea0fa43397 | ||
|
|
82dd9d78ce | ||
|
|
dba334c74e | ||
|
|
784a2f89da | ||
|
|
5aa3b3ad30 | ||
|
|
ab5fe5e783 | ||
|
|
b2c89c743c | ||
|
|
a4c42d7390 | ||
|
|
823c2806a0 | ||
|
|
5a601b986d | ||
|
|
5bf05dc323 | ||
|
|
586cc866f5 | ||
|
|
614523948d | ||
|
|
aa47d3da8c | ||
|
|
94649eedd8 | ||
|
|
f1b7710148 | ||
|
|
31cedbb961 | ||
|
|
805f8c8f2d | ||
|
|
3dfb0ff538 | ||
|
|
3b3c1b6aa5 | ||
|
|
8a1a8534dc | ||
|
|
1695f40b3e | ||
|
|
2e7358e012 | ||
|
|
eac6684bf3 | ||
|
|
53720240b4 | ||
|
|
337ff632e3 | ||
|
|
c4d82aa89a | ||
|
|
7a631fe801 | ||
|
|
ffdcb52998 | ||
|
|
512097b404 | ||
|
|
af256da8ca | ||
|
|
d0af5824ad | ||
|
|
4720914286 | ||
|
|
3d5d577b61 | ||
|
|
d68a3abedb | ||
|
|
511ae9f5b6 | ||
|
|
63aff4b03d | ||
|
|
2839a3d6f2 | ||
|
|
4573d98dce | ||
|
|
a728e7a674 | ||
|
|
707fc52dfe | ||
|
|
6ecf7bcd0a | ||
|
|
2f0c0752ed | ||
|
|
aa2dad421f | ||
|
|
7dc03e3c0d | ||
|
|
6cdb8c1b23 | ||
|
|
e57a024960 | ||
|
|
8709f196bf | ||
|
|
70d6a03251 | ||
|
|
ff92da45f1 | ||
|
|
a185b9e78f | ||
|
|
42abb24ae0 | ||
|
|
38ffbfb778 | ||
|
|
dab829b564 | ||
|
|
eb62a386cc | ||
|
|
6d8df972da | ||
|
|
550193e3e0 | ||
|
|
5ed79873f7 | ||
|
|
20b5378936 | ||
|
|
3a5169ac6d | ||
|
|
a53e6fc425 | ||
|
|
b690b78e22 | ||
|
|
67546bfa67 | ||
|
|
15931649a2 | ||
|
|
fedbe42fc8 | ||
|
|
52e7117d37 | ||
|
|
8a56273871 | ||
|
|
110c0f74b4 | ||
|
|
f76b94ab7d | ||
|
|
91deba8df0 | ||
|
|
f18b16515a | ||
|
|
f8f021730e | ||
|
|
a17aa6e83d | ||
|
|
5d84bec1c8 | ||
|
|
5f932c05c8 | ||
|
|
0521ebeb71 | ||
|
|
99ca856a7c | ||
|
|
04a572114a | ||
|
|
8c531c7836 | ||
|
|
fa3e11943a | ||
|
|
d6881f1826 | ||
|
|
c6026053bc | ||
|
|
2c2f6910f4 | ||
|
|
73760ed4cb | ||
|
|
7208a16bf3 | ||
|
|
50439fee71 | ||
|
|
5d91d93b7e | ||
|
|
6819a17049 | ||
|
|
8a67f7e535 | ||
|
|
aefffb6f6c | ||
|
|
cb28f6bffb | ||
|
|
7845cb88da | ||
|
|
7129dfa5d9 | ||
|
|
84eea4aaf2 | ||
|
|
39cb3234a7 | ||
|
|
85d673f95f | ||
|
|
f1cf81e851 | ||
|
|
c6ebff6469 | ||
|
|
c904438dda | ||
|
|
ed3f30cf12 | ||
|
|
e5eb088eaf | ||
|
|
6035eb5469 | ||
|
|
ce34d50b16 | ||
|
|
0010bc8495 | ||
|
|
31c22870d3 | ||
|
|
49f67a8ad4 | ||
|
|
23608f1e63 | ||
|
|
d72c200a64 | ||
|
|
ea98a5bc9f | ||
|
|
07052cd704 | ||
|
|
a258a44f2a | ||
|
|
f34bc2cba0 | ||
|
|
f75042ac5d | ||
|
|
bf6c1ec3d3 | ||
|
|
271087a63b | ||
|
|
53ef02da13 | ||
|
|
620492183f | ||
|
|
8eb1d3031c | ||
|
|
e6faa29d36 | ||
|
|
8b6e3f1a2e | ||
|
|
1f973cc3ff | ||
|
|
64180398eb | ||
|
|
79396ffccc | ||
|
|
b00a0cc77b | ||
|
|
7236e82911 | ||
|
|
7b58ded3b3 | ||
|
|
3c7150850e | ||
|
|
cd9f7bce91 | ||
|
|
805c416251 | ||
|
|
371e18dde4 | ||
|
|
524edc9f0b | ||
|
|
306d4577a5 | ||
|
|
894821e1ef | ||
|
|
b88b868b91 | ||
|
|
b8ee1b9733 | ||
|
|
f5a22be0a0 | ||
|
|
423c2d35ee | ||
|
|
833d00711e | ||
|
|
ace4ca8556 | ||
|
|
8a7f164b67 | ||
|
|
5180ec47dc | ||
|
|
4598673138 | ||
|
|
2c5115907f | ||
|
|
3d2a99ec64 | ||
|
|
20a1dcfe80 | ||
|
|
43f6d84de7 | ||
|
|
00b39b2b6c | ||
|
|
3a349451e2 | ||
|
|
098a3e7efe | ||
|
|
ec26edc735 | ||
|
|
1f66249648 | ||
|
|
5cc28f061e | ||
|
|
6cdebb0871 | ||
|
|
8e142dfaa7 | ||
|
|
9b3db6cd7d | ||
|
|
fb3ffecbb5 | ||
|
|
1de555b72d | ||
|
|
39cc0970bd | ||
|
|
831d8c9ae6 | ||
|
|
d75ded973a | ||
|
|
583757ab2d | ||
|
|
6dc95bd201 | ||
|
|
b4872fb19d | ||
|
|
777550e4d7 | ||
|
|
51f9aabf68 | ||
|
|
cbadf63c13 | ||
|
|
d9d6d85ecd | ||
|
|
9dcc6f061f | ||
|
|
115177e679 | ||
|
|
67536b7f1c | ||
|
|
9e6ca5ccaa | ||
|
|
7291b60643 | ||
|
|
64db556342 | ||
|
|
607b1142fa | ||
|
|
22b2dbf747 | ||
|
|
e1085cfef2 | ||
|
|
1637f18132 | ||
|
|
453547cf81 | ||
|
|
a50ea56eda | ||
|
|
402f080149 | ||
|
|
a74470d619 | ||
|
|
df9067fec5 | ||
|
|
5d38bead9a | ||
|
|
c2d3db8c2b | ||
|
|
0f10e74f69 | ||
|
|
459f15b500 | ||
|
|
5439c3a702 | ||
|
|
69093192ef | ||
|
|
f29923306d | ||
|
|
8b62949069 | ||
|
|
8346ecefea | ||
|
|
4ff83b9d74 | ||
|
|
dde480b416 | ||
|
|
57c0e0d7c0 | ||
|
|
9ca70b31ef | ||
|
|
07bed0e214 | ||
|
|
3de5c9af24 | ||
|
|
c32255c13d | ||
|
|
407d0cec58 | ||
|
|
3061f4564a | ||
|
|
9e2a0cdc38 | ||
|
|
49afea4d86 | ||
|
|
4e593fd2a9 | ||
|
|
3001e743c3 | ||
|
|
f6d44b4b24 | ||
|
|
0eedf49c28 | ||
|
|
d993b8eaee | ||
|
|
4146594df3 | ||
|
|
8261b52d33 | ||
|
|
a4820755ab | ||
|
|
c27f930c6e | ||
|
|
6355956008 | ||
|
|
2f4bfa29e5 | ||
|
|
4100c1d952 | ||
|
|
7965ee37ab | ||
|
|
fb317bb597 | ||
|
|
6b1121a658 | ||
|
|
525949de9f | ||
|
|
09f532b773 | ||
|
|
dc7fa068e3 | ||
|
|
e57ac62141 | ||
|
|
516bfe1c27 | ||
|
|
0491d12e50 | ||
|
|
b7e05731ea | ||
|
|
0bea9205d9 | ||
|
|
bf817e5de6 | ||
|
|
bfa4bdf7a0 | ||
|
|
9e851de675 | ||
|
|
98a84cdaec | ||
|
|
46206b7330 | ||
|
|
cf23a7307e | ||
|
|
515a6d1664 | ||
|
|
1fc246e82d | ||
|
|
33cf17647b | ||
|
|
df5e294c5c | ||
|
|
29df549a5b | ||
|
|
49ae0330b0 | ||
|
|
20e6881fd8 | ||
|
|
1ed56cb468 | ||
|
|
73ba745097 | ||
|
|
7b6a813149 | ||
|
|
9c45ae5db0 | ||
|
|
9c9a49920d | ||
|
|
41dfe1d575 | ||
|
|
b5bd00dc8a | ||
|
|
89b51584c5 | ||
|
|
63f26f6fb9 | ||
|
|
41b578a45e | ||
|
|
9d1abc92fa | ||
|
|
ae62039386 | ||
|
|
a7c2a28bec | ||
|
|
dc9b4dab5c | ||
|
|
7f20f01a3b | ||
|
|
57b2ba7115 | ||
|
|
bbe4ec3cde | ||
|
|
84f30c64d0 | ||
|
|
241ad96665 | ||
|
|
fc060940b4 | ||
|
|
9ef241d273 | ||
|
|
002e0aced4 | ||
|
|
be590bf1b9 | ||
|
|
3dc81826fd | ||
|
|
b6df2cab18 | ||
|
|
0601111d88 | ||
|
|
0064ee83cf | ||
|
|
a354c6d446 | ||
|
|
e5b4be7771 | ||
|
|
d5960ade4a | ||
|
|
d068374972 | ||
|
|
947936279a | ||
|
|
42492c82ca | ||
|
|
97cfb4d2a6 | ||
|
|
d80e0e2f7f | ||
|
|
eb7d0dd6a1 | ||
|
|
9e406c40b5 | ||
|
|
b071731cc2 | ||
|
|
d3ec78d996 | ||
|
|
98c383cc42 | ||
|
|
12f7e22e36 | ||
|
|
6c7d3123c2 | ||
|
|
86e4e4965d | ||
|
|
d0c2e446b7 | ||
|
|
bb1e9dd3ae | ||
|
|
dc5b8a46c6 | ||
|
|
d90f7d51aa | ||
|
|
d6fa9be6e6 | ||
|
|
fb4fb978e1 | ||
|
|
604f1254ba | ||
|
|
b49854bde5 | ||
|
|
fb771c8dd8 | ||
|
|
44b07c5f7b | ||
|
|
e98f9f6a24 | ||
|
|
95623b991f | ||
|
|
1d402e46a8 | ||
|
|
d7e0e40e0a | ||
|
|
74b9fdb3cb | ||
|
|
01107cb677 | ||
|
|
e93eaeb3fe | ||
|
|
b55570703d | ||
|
|
fd16003059 | ||
|
|
71c6afaf98 | ||
|
|
bb99610517 | ||
|
|
ff1977cbd4 | ||
|
|
14a3bcdaa8 | ||
|
|
ffea024e7c | ||
|
|
d4e8e9ac07 | ||
|
|
5399892051 | ||
|
|
8d9d92b7b1 | ||
|
|
cde3cfc30d | ||
|
|
481bf0fb03 | ||
|
|
9f6c7e5fc9 | ||
|
|
7c76166697 | ||
|
|
0d13e794e4 | ||
|
|
8437fe6dc6 | ||
|
|
dd0ead011e | ||
|
|
ffbd6893f6 | ||
|
|
bad962cf21 | ||
|
|
293bfec844 | ||
|
|
bc0b686fc0 | ||
|
|
e08ae0e996 | ||
|
|
e877644ed1 | ||
|
|
5ed8a441d4 | ||
|
|
6ff5b4ed4c | ||
|
|
3d4dad5655 | ||
|
|
ec3ae16f2e | ||
|
|
9f282f4ee3 | ||
|
|
e85c5f51b9 | ||
|
|
2327ddb52d | ||
|
|
7b3cd164d7 | ||
|
|
aeb7f633f3 | ||
|
|
7817573893 | ||
|
|
923901c81f | ||
|
|
bd81e32d1a | ||
|
|
e211c5b5f5 | ||
|
|
98e5f88b9f | ||
|
|
5ac583ba7b | ||
|
|
3bd91a774e | ||
|
|
95290f2fbd | ||
|
|
14e6c50d2e | ||
|
|
c273636efe | ||
|
|
2fbec2f760 | ||
|
|
ae500dbe45 | ||
|
|
50e972912d | ||
|
|
e364488761 | ||
|
|
c7e9414bfe | ||
|
|
f2e5e31837 | ||
|
|
10e2545ceb | ||
|
|
89970b8951 | ||
|
|
66510b82c4 | ||
|
|
6c96ef02df | ||
|
|
ddc72468fc | ||
|
|
16197b67e3 | ||
|
|
6e2056f5b4 | ||
|
|
8d63b3f95b | ||
|
|
43e4503343 | ||
|
|
c1404f0f98 | ||
|
|
46ee425bac | ||
|
|
81f457f379 | ||
|
|
07c594d988 | ||
|
|
be4c129168 | ||
|
|
ef4a805b02 | ||
|
|
5059f0bee6 | ||
|
|
c31948caeb | ||
|
|
2758eb210f | ||
|
|
465060c9a7 | ||
|
|
8f6bfb0295 | ||
|
|
cac03318eb | ||
|
|
7bf5fd7859 | ||
|
|
2c9773ed89 | ||
|
|
46fb82ce3d | ||
|
|
144aaaa986 | ||
|
|
14f26d8818 | ||
|
|
f8f7beea54 | ||
|
|
c6c22b34c8 | ||
|
|
752cdc3b11 | ||
|
|
f5c4ccff1f | ||
|
|
5e004dbef4 | ||
|
|
4fba554b47 | ||
|
|
6593dc8dbf | ||
|
|
480a5a634a | ||
|
|
f213007d4d | ||
|
|
0f8263de2e | ||
|
|
9b449be538 | ||
|
|
3db0395e0a | ||
|
|
fe66cd7734 | ||
|
|
4190b85018 | ||
|
|
0a355bbee0 | ||
|
|
407d1e3982 | ||
|
|
9b551a1c10 | ||
|
|
c0f025d2f6 | ||
|
|
56620dc3dd | ||
|
|
598addc6d7 | ||
|
|
95e701da6a | ||
|
|
f6d624c729 | ||
|
|
5ad19c5071 | ||
|
|
41a595f62a | ||
|
|
f58da1d4dc | ||
|
|
b57dbceec3 | ||
|
|
06c80ae2da | ||
|
|
0beddcd2d0 | ||
|
|
94f5aa00fc | ||
|
|
5bf80d7ef2 | ||
|
|
09a1959309 | ||
|
|
15713e64cb | ||
|
|
519dae2639 | ||
|
|
ce7a2234ee | ||
|
|
a06a92d620 | ||
|
|
1ca4da9a33 | ||
|
|
52fc08fdf5 | ||
|
|
2feaaaddda | ||
|
|
9870a84b99 | ||
|
|
bbec63fda7 | ||
|
|
809c7f0ac8 | ||
|
|
fd47ba9a9a | ||
|
|
b1a199a272 | ||
|
|
fc8d57d128 | ||
|
|
f24ea4d6c9 | ||
|
|
12b93a3a07 | ||
|
|
3fa0df7553 | ||
|
|
13978a108a | ||
|
|
d549fd5265 | ||
|
|
30d2d94a22 | ||
|
|
caa5719a86 | ||
|
|
436b81b357 | ||
|
|
600f2eda93 | ||
|
|
309edb6da6 | ||
|
|
bcd4f6e7dd | ||
|
|
fd50054781 | ||
|
|
26064905d4 | ||
|
|
24176463f4 | ||
|
|
418d41c2a1 | ||
|
|
01291f7cb5 | ||
|
|
e8b8975ec3 | ||
|
|
6a53d7d10d | ||
|
|
65870ff3e8 | ||
|
|
6f1479fa41 | ||
|
|
f694b616aa | ||
|
|
1bd41c2a0b | ||
|
|
f1d88ac6c1 | ||
|
|
0d5d918563 | ||
|
|
547bc41026 | ||
|
|
d27dc63026 | ||
|
|
83f5b9f644 | ||
|
|
addb3c1a79 | ||
|
|
88e7eafcf9 | ||
|
|
5f88d4bf22 | ||
|
|
7f00868584 | ||
|
|
6ebc6c7dae | ||
|
|
f38fe38d45 | ||
|
|
323427e951 | ||
|
|
06d3ebde1c | ||
|
|
1df8057f97 | ||
|
|
d2d449ad78 | ||
|
|
16d63e3a76 | ||
|
|
cc573a74dc | ||
|
|
1d54cf5f18 | ||
|
|
4e098f74cd | ||
|
|
b0300df5e7 | ||
|
|
9c132fe622 | ||
|
|
22334834ac | ||
|
|
3373343f23 | ||
|
|
1d0bedc454 | ||
|
|
8381ea0307 | ||
|
|
2e0b820637 | ||
|
|
9f5e1bd505 | ||
|
|
7db5c9a274 | ||
|
|
4fad212a25 | ||
|
|
f6b3bf548d | ||
|
|
ca888b96e3 | ||
|
|
2fc1610248 | ||
|
|
bc28cee1dc | ||
|
|
ea1928c904 | ||
|
|
8c3f34e0c9 | ||
|
|
956441176f | ||
|
|
83d01b7e1c | ||
|
|
fc2b2e9c64 | ||
|
|
60b45ba428 | ||
|
|
107db3f407 | ||
|
|
4937cd088f | ||
|
|
5e96539f55 | ||
|
|
708d67fa02 | ||
|
|
a95b13d804 | ||
|
|
1e60261d3f | ||
|
|
0d4f1c0212 | ||
|
|
4d30c4c87f | ||
|
|
6c02d65563 | ||
|
|
16dea48c82 | ||
|
|
f517029483 | ||
|
|
6f233b3b58 | ||
|
|
f8c85735f4 | ||
|
|
1e4c50b70d | ||
|
|
93d53a5ae8 | ||
|
|
16f2b1736e | ||
|
|
4d60607b0b | ||
|
|
beb09d85fa | ||
|
|
8ed902364c | ||
|
|
02c83fb38b | ||
|
|
9049e49f9e | ||
|
|
9d36dd9841 | ||
|
|
9905791932 | ||
|
|
dd00ca926e | ||
|
|
4092e160a4 | ||
|
|
6092802b3a | ||
|
|
42f074062b | ||
|
|
112191042b | ||
|
|
1566dd06cd | ||
|
|
251bf76be4 | ||
|
|
a6eddccfe5 | ||
|
|
3bd43bedad | ||
|
|
4750e57ce1 | ||
|
|
df7ce674e9 | ||
|
|
e968e8a5f0 | ||
|
|
c8705dd876 | ||
|
|
c7a29e331f | ||
|
|
ffcbbd5fea | ||
|
|
ebf9c0bf91 | ||
|
|
df658e8fa8 | ||
|
|
b5edb8e50c | ||
|
|
97eb0d191b | ||
|
|
857d7a620d | ||
|
|
fad64ed381 | ||
|
|
4244ebea6a | ||
|
|
327822c29a | ||
|
|
feea45c205 | ||
|
|
c4b81d1210 | ||
|
|
3988074cae | ||
|
|
ad2b69e115 | ||
|
|
f9eb960848 | ||
|
|
5927d6e5f4 | ||
|
|
6bb3e494e5 | ||
|
|
5d5d15e7a3 | ||
|
|
b742ec3de4 | ||
|
|
75f0aade0d | ||
|
|
3872c8f41d | ||
|
|
ae2fd4fa7b | ||
|
|
3eb26a65d3 | ||
|
|
157433893c | ||
|
|
236dac0c96 | ||
|
|
78d696c1e5 | ||
|
|
48a4062f75 | ||
|
|
18583d5a91 | ||
|
|
642913dbf4 | ||
|
|
2ca5473e52 | ||
|
|
6070aa8281 | ||
|
|
78dc468555 | ||
|
|
bcd996f3df | ||
|
|
a1d959d0e9 | ||
|
|
71e97bccc7 | ||
|
|
3239b18671 | ||
|
|
c3794da4ed | ||
|
|
872a561495 | ||
|
|
d1adc8b15f | ||
|
|
e7d20b35ee | ||
|
|
21f566a60a | ||
|
|
7562e9ad5d | ||
|
|
0f16f7f6a2 | ||
|
|
dfa9fc05cb | ||
|
|
c4e855b58c | ||
|
|
0d9f636356 | ||
|
|
387f79be13 | ||
|
|
87f3e4a910 | ||
|
|
01ea635328 | ||
|
|
3d1bb8a4b4 | ||
|
|
046d1cb767 | ||
|
|
3d7b06e24c | ||
|
|
063b69db2f | ||
|
|
c0b0829d5a | ||
|
|
3cf42dd36d | ||
|
|
89e558ee4f | ||
|
|
f277ef7428 | ||
|
|
b7647a749e | ||
|
|
4cc6aeb274 | ||
|
|
3de903f51f | ||
|
|
4b4f49b1eb | ||
|
|
4407720b95 | ||
|
|
36dc939f91 | ||
|
|
40cd326ed7 | ||
|
|
a890d44d2f | ||
|
|
c98426505e | ||
|
|
6fa8039b55 | ||
|
|
faf4307fcc | ||
|
|
1512e41e41 | ||
|
|
55a25c5e0e | ||
|
|
f815731412 | ||
|
|
d607064fd2 | ||
|
|
8b0f876416 | ||
|
|
bbd8f410d9 | ||
|
|
fb3d809e81 | ||
|
|
95db494b30 | ||
|
|
5ebdb74ba8 | ||
|
|
07c20c5357 | ||
|
|
31daa8dad3 | ||
|
|
a0b03c3a68 | ||
|
|
fb7bb4f87a | ||
|
|
81e9fff605 | ||
|
|
e6baca2ffb | ||
|
|
2b28a0bc0a | ||
|
|
1f389f1422 | ||
|
|
8cdee40695 | ||
|
|
c6c6874d72 | ||
|
|
78c47074a3 | ||
|
|
22f249514d | ||
|
|
8f6826c9e3 | ||
|
|
1519c59188 | ||
|
|
a9176e9969 | ||
|
|
ab7dfb38ac | ||
|
|
aa21a21647 | ||
|
|
094bad431e | ||
|
|
a8ef2b8cfe | ||
|
|
636c911d33 | ||
|
|
53dd06057f | ||
|
|
1b840d9c79 | ||
|
|
012f051f2e | ||
|
|
8684cad808 | ||
|
|
15611d4559 | ||
|
|
d542e144dc | ||
|
|
f8fd159fbc | ||
|
|
2eb9570b52 | ||
|
|
f5ae4b42db | ||
|
|
24dd870559 | ||
|
|
97ef1b8393 | ||
|
|
bdb332a5f8 | ||
|
|
8cbdc28e66 | ||
|
|
4ebf080da8 | ||
|
|
53606261bf | ||
|
|
50d049d5f5 | ||
|
|
395391525b | ||
|
|
59f3f6d6c2 | ||
|
|
12cc536541 | ||
|
|
51ec421ad8 | ||
|
|
1cf095c51d | ||
|
|
7c9a261dc0 | ||
|
|
31367fbc97 | ||
|
|
7159f40304 | ||
|
|
042e307bef | ||
|
|
f584949470 | ||
|
|
303cd74e5f | ||
|
|
3b61abf20a | ||
|
|
d23f71b03c | ||
|
|
c156f5f3a1 | ||
|
|
155a8b241c | ||
|
|
69472d5f5c | ||
|
|
4ac3ee9d75 | ||
|
|
5b98c13eff | ||
|
|
28cb030ffe | ||
|
|
be1b075605 | ||
|
|
62cddb9e74 | ||
|
|
eabba5b316 | ||
|
|
a5e584ce22 | ||
|
|
d25732a53a | ||
|
|
701bd68731 | ||
|
|
51acd3f3ad | ||
|
|
804f9c2d4f | ||
|
|
81c0a17f20 | ||
|
|
2570d5866d | ||
|
|
2c0f4f588e | ||
|
|
8b5365e62c | ||
|
|
d837bcc60c | ||
|
|
bfe502654c | ||
|
|
6632d110b6 | ||
|
|
341c35b1b8 | ||
|
|
8e6084bc9c | ||
|
|
40a40613cb | ||
|
|
23a2745a3a | ||
|
|
88c6a0a752 | ||
|
|
1f62e3f142 | ||
|
|
8424e946e4 | ||
|
|
66a0448698 | ||
|
|
2cab359222 | ||
|
|
aebc0c3cdd | ||
|
|
c2925c40c7 | ||
|
|
88115dedbf | ||
|
|
1d8a881941 | ||
|
|
8439d7195f | ||
|
|
f0f8b2c85c | ||
|
|
35af42a0dd | ||
|
|
56a56527c6 | ||
|
|
1485e567f1 | ||
|
|
180c57747d | ||
|
|
2361a7aa24 | ||
|
|
4f9422ec85 | ||
|
|
9d17987b8c | ||
|
|
94056e51cc | ||
|
|
8cd9761d29 | ||
|
|
e86f4a201c | ||
|
|
9341404de0 | ||
|
|
b031b22c80 | ||
|
|
1440a070c3 | ||
|
|
14636b77fb | ||
|
|
da4160ce04 | ||
|
|
eccc3a26d2 | ||
|
|
51afcccb6b | ||
|
|
8f7d3efb1a | ||
|
|
13a818e3b5 | ||
|
|
5e049b9fb7 | ||
|
|
33d5038bd3 | ||
|
|
39bb76224d | ||
|
|
9632882b69 | ||
|
|
803598293e | ||
|
|
50068870a1 | ||
|
|
9a19439e31 | ||
|
|
d8dd1bc427 | ||
|
|
c1134be427 | ||
|
|
2aab3394cf | ||
|
|
39a5a37822 | ||
|
|
89879968bb | ||
|
|
6ecd671cfc | ||
|
|
2f50c9c52b | ||
|
|
386ce008d9 | ||
|
|
3d6c78875f | ||
|
|
a9bcd83ff0 | ||
|
|
bf3f0f133e | ||
|
|
2aa16f5f66 | ||
|
|
63f788f07e | ||
|
|
c3d61852ee | ||
|
|
2742b9c467 | ||
|
|
748ae8f06a | ||
|
|
28524d977c | ||
|
|
b5c2f0f2bb | ||
|
|
decee4abb2 | ||
|
|
8b4ce58a1d | ||
|
|
3682bcf9d2 | ||
|
|
857dc4b862 | ||
|
|
247ca90b33 | ||
|
|
00b22e527d | ||
|
|
83035ec925 | ||
|
|
035c07716c | ||
|
|
0d9689ab9f | ||
|
|
e696a861ba | ||
|
|
801e84d4d2 | ||
|
|
ff7e060269 | ||
|
|
d4d20346f2 | ||
|
|
1b48b97aa4 | ||
|
|
d1e665bac7 | ||
|
|
dc109fc118 | ||
|
|
fb8698d89a | ||
|
|
2821341551 | ||
|
|
bcdbe0f018 | ||
|
|
5f2a8b7f30 | ||
|
|
2831b4c0d8 | ||
|
|
c95cbb5931 | ||
|
|
da89656fdc | ||
|
|
4e5dc68b4e | ||
|
|
91ecd3cb6a | ||
|
|
448a36c5cb | ||
|
|
ed5a887579 | ||
|
|
e42058f15a | ||
|
|
a7e1cacd36 | ||
|
|
734803549d | ||
|
|
14f784961f | ||
|
|
5c8da71dea | ||
|
|
a6fe4b73b3 | ||
|
|
256e8f045b | ||
|
|
2e54bda0e1 | ||
|
|
2ae8e6291c | ||
|
|
651f78cba8 | ||
|
|
d5674ed411 | ||
|
|
4a2a810c20 | ||
|
|
365b0565de | ||
|
|
65d785d560 | ||
|
|
fff688c019 | ||
|
|
6f07ac731b | ||
|
|
f00c15d7b0 | ||
|
|
1676a902f2 | ||
|
|
576d323c52 | ||
|
|
b4adf71696 | ||
|
|
c0703e7a92 | ||
|
|
d789d50bd6 | ||
|
|
b83285bd59 | ||
|
|
bc93ed027f | ||
|
|
9f974b8996 | ||
|
|
c03b03c909 | ||
|
|
cecce18c51 | ||
|
|
1f37f41ee8 | ||
|
|
150ac7d708 | ||
|
|
be1ba9204a | ||
|
|
760bc3eb2d | ||
|
|
706f23115f | ||
|
|
171f9a256d | ||
|
|
e7b9b0d0ed | ||
|
|
5645143baa | ||
|
|
ec5c0ac3e0 | ||
|
|
511f9c1ffe | ||
|
|
6325327ba1 | ||
|
|
d60d590381 | ||
|
|
5aac011e49 | ||
|
|
320d573ccd | ||
|
|
c51bef38b6 | ||
|
|
28e0548cac | ||
|
|
d0acf6ecfe | ||
|
|
4b91f503eb | ||
|
|
eb02bc77fb | ||
|
|
55f54251eb | ||
|
|
e64b7cc233 | ||
|
|
4bb46a98c6 | ||
|
|
53b55288b3 | ||
|
|
f576a64f11 | ||
|
|
d574676702 | ||
|
|
f45821581c | ||
|
|
bb6ae0898d | ||
|
|
ea7fc641ba | ||
|
|
87e6482497 | ||
|
|
1e08e21470 | ||
|
|
ccb40ff3ff | ||
|
|
3291f3f891 | ||
|
|
2031939bfc | ||
|
|
6394299937 | ||
|
|
316f6c0cf3 | ||
|
|
53630f5ff1 | ||
|
|
3feee5958a | ||
|
|
209c056258 | ||
|
|
cd6e62049c | ||
|
|
526c0a918e | ||
|
|
de7ffa60cc | ||
|
|
81b9791178 | ||
|
|
c4ec6ac0f7 | ||
|
|
cfe3d2a83e | ||
|
|
b6bd4fe02d | ||
|
|
2da976155a | ||
|
|
e69e0de9c9 | ||
|
|
cfc9f10a6e | ||
|
|
8ba71b0754 | ||
|
|
46c16bb8f1 | ||
|
|
1b079a3cda | ||
|
|
c1b23fee45 | ||
|
|
199678e1bc | ||
|
|
d5daf6f6ae | ||
|
|
03d8d3233f | ||
|
|
5c6a21b014 | ||
|
|
3965df69d5 | ||
|
|
f47436e158 | ||
|
|
f474e4cae1 | ||
|
|
7979fdb832 | ||
|
|
b3f6bc47bb | ||
|
|
e6163082d7 | ||
|
|
fe1b5c06eb | ||
|
|
297a4227ef | ||
|
|
9f774d3e6a | ||
|
|
f7699f1dec | ||
|
|
3889c93b63 | ||
|
|
ee384ed8cc | ||
|
|
e92f038f40 | ||
|
|
0b3426bf5d | ||
|
|
0861ff957d | ||
|
|
32607ef715 | ||
|
|
c643ffd6d6 | ||
|
|
b8b3883950 | ||
|
|
3547fc54cb | ||
|
|
64c41a8af0 | ||
|
|
4191ac9cfe | ||
|
|
f1738e78db | ||
|
|
de494e4b98 | ||
|
|
585509f3bc | ||
|
|
f630dc5971 | ||
|
|
9a8f78ba3b | ||
|
|
63c6168c7f | ||
|
|
1993f0783b | ||
|
|
70238f448b | ||
|
|
914c38e379 | ||
|
|
0f28b01383 | ||
|
|
6909594c0b | ||
|
|
583921e8bf | ||
|
|
61ecf2c096 | ||
|
|
0eecb74230 | ||
|
|
b7b75f3c59 | ||
|
|
fe48ddfdcc | ||
|
|
70b23c3f84 | ||
|
|
223908f9d2 | ||
|
|
1838852d4d | ||
|
|
175550643c | ||
|
|
eb0eec4471 | ||
|
|
da6c505f41 | ||
|
|
966ba843e0 | ||
|
|
5773f75698 |
@@ -25,7 +25,7 @@ e.g. s3, github, go-bindata, gcs, file
|
||||
Obtained by running: `migrate -help`
|
||||
|
||||
**Loaded Database Drivers**
|
||||
e.g. spanner, stub, datastore, cockroachdb, crdb-postgres, postgres, postgresql, pgx, redshift, cassandra, cockroach, mysql
|
||||
e.g. spanner, stub, clickhouse, cockroachdb, crdb-postgres, postgres, postgresql, pgx, redshift, cassandra, cockroach, mysql
|
||||
Obtained by running: `migrate -help`
|
||||
|
||||
**Go Version**
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,97 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.16.x", "1.17.x"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Run test
|
||||
run: make test COVERAGE_DIR=/tmp/coverage
|
||||
|
||||
- name: Send goveralls coverage
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
path-to-profile: /tmp/coverage/combined.txt
|
||||
flag-name: Go-${{ matrix.go }}
|
||||
parallel: true
|
||||
|
||||
check-coverage:
|
||||
name: Check coverage
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
parallel-finished: true
|
||||
|
||||
goreleaser:
|
||||
name: Release a new version
|
||||
needs: [lint, test]
|
||||
runs-on: ubuntu-latest
|
||||
environment: GoReleaser
|
||||
# This job only runs when
|
||||
# 1. When the previous `lint` and `test` jobs has completed successfully
|
||||
# 2. When the repository is not a fork, i.e. it will only run on the official golang-migrate/migrate
|
||||
# 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
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17.x"
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: golangmigrate
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- run: echo "SOURCE=$(make echo-source)" >> $GITHUB_ENV
|
||||
- run: echo "DATABASE=$(make echo-database)" >> $GITHUB_ENV
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: gem install package_cloud
|
||||
- run: package_cloud push golang-migrate/migrate/ubuntu/bionic dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- 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/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 }}
|
||||
@@ -8,4 +8,3 @@ vendor/
|
||||
.vscode/
|
||||
.idea
|
||||
dist/
|
||||
migrate
|
||||
|
||||
+18
-29
@@ -1,37 +1,26 @@
|
||||
version: "2"
|
||||
run:
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
timeout: 5m
|
||||
linters:
|
||||
enable:
|
||||
#- golint
|
||||
- interfacer
|
||||
- unconvert
|
||||
#- dupl
|
||||
- goconst
|
||||
- gofmt
|
||||
- misspell
|
||||
- unparam
|
||||
- nakedret
|
||||
- prealloc
|
||||
- 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$
|
||||
#- gosec
|
||||
linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
max-issues-per-linter: 0
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
# gosec: Duplicated errcheck checks
|
||||
- G104
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.26.4-alpine3.21 AS builder
|
||||
FROM golang:1.16-alpine3.13 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.21
|
||||
FROM alpine:3.13
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.19
|
||||
FROM alpine:3.13
|
||||
|
||||
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 repository though.
|
||||
Yes, technically thats possible. We want to encourage you to contribute your driver to this respository 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 on
|
||||
Database-specific locking features are used by *some* database drivers to prevent multiple instances of migrate from running migrations at the same time
|
||||
the same database at the same time. For example, the MySQL driver uses the `GET_LOCK` function, while the Postgres driver uses
|
||||
the `pg_advisory_lock` function.
|
||||
|
||||
|
||||
+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 their 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 his 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 committing your migrations you should run your migrations up, down, and then up again to see if migrations are working properly both ways.
|
||||
Before commiting your migrations you should run your migrations up, down, and then up again to see if migrations are working properly both ways.
|
||||
(e.g. if you created a table in a migration but reverse migration did not delete it, you will encounter an error when running the forward migration again)
|
||||
It's also worth checking your migrations in a separate, containerized environment. You can find some tools at the [end of this document](#further-reading).
|
||||
It's also worth checking your migrations in a separate, containerized environment. You can find some tools in the end of this document.
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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 yugabytedb datastore mongodb sqlserver firebird neo4j pgx pgx5 rqlite
|
||||
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb sqlserver firebird neo4j pgx
|
||||
DATABASE_TEST ?= $(DATABASE) sqlite sqlite3 sqlcipher
|
||||
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
|
||||
TEST_FLAGS ?=
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<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://github.com/golang-migrate/migrate/actions/workflows/ci.yaml?query=branch%3Amaster)
|
||||
[](https://pkg.go.dev/github.com/golang-migrate/migrate/v4)
|
||||
[](https://coveralls.io/github/golang-migrate/migrate?branch=master)
|
||||
[](https://packagecloud.io/golang-migrate/migrate?filter=debs)
|
||||
[](https://hub.docker.com/r/migrate/migrate/)
|
||||

|
||||

|
||||
[](https://github.com/golang-migrate/migrate/releases)
|
||||
[](https://goreportcard.com/report/github.com/golang-migrate/migrate/v4)
|
||||
[](https://goreportcard.com/report/github.com/golang-migrate/migrate)
|
||||
|
||||
# migrate
|
||||
|
||||
@@ -26,26 +24,23 @@ Forked from [mattes/migrate](https://github.com/mattes/migrate)
|
||||
Database drivers run migrations. [Add a new database?](database/driver.go)
|
||||
|
||||
* [PostgreSQL](database/postgres)
|
||||
* [PGX v4](database/pgx)
|
||||
* [PGX v5](database/pgx/v5)
|
||||
* [PGX](database/pgx)
|
||||
* [Redshift](database/redshift)
|
||||
* [Ql](database/ql)
|
||||
* [Cassandra / ScyllaDB](database/cassandra)
|
||||
* [Cassandra](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)
|
||||
* [YugabyteDB](database/yugabytedb)
|
||||
* [Datastore](database/datastore)
|
||||
* [ClickHouse](database/clickhouse)
|
||||
* [Firebird](database/firebird)
|
||||
* [MS SQL Server](database/sqlserver)
|
||||
* [rqlite](database/rqlite)
|
||||
|
||||
### Database URLs
|
||||
|
||||
@@ -73,9 +68,7 @@ $
|
||||
Source drivers read migrations from local or remote sources. [Add a new source?](source/driver.go)
|
||||
|
||||
* [Filesystem](source/file) - read from filesystem
|
||||
* [io/fs](source/iofs) - read from a Go [io/fs](https://pkg.go.dev/io/fs#FS)
|
||||
* [Go-Bindata](source/go_bindata) - read from embedded binary data ([jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata))
|
||||
* [pkger](source/pkger) - read from embedded binary data ([markbates/pkger](https://github.com/markbates/pkger))
|
||||
* [GitHub](source/github) - read from remote GitHub repositories
|
||||
* [GitHub Enterprise](source/github_ee) - read from remote GitHub Enterprise repositories
|
||||
* [Bitbucket](source/bitbucket) - read from remote Bitbucket repositories
|
||||
@@ -89,7 +82,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) (includes CLI install instructions)
|
||||
__[CLI Documentation](cmd/migrate)__
|
||||
|
||||
### Basic usage
|
||||
|
||||
@@ -113,7 +106,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://pkg.go.dev/github.com/golang-migrate/migrate/v4)__
|
||||
__[Go Documentation](https://godoc.org/github.com/golang-migrate/migrate)__
|
||||
|
||||
```go
|
||||
import (
|
||||
@@ -147,7 +140,7 @@ func main() {
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file:///migrations",
|
||||
"postgres", driver)
|
||||
m.Up() // or m.Steps(2) if you want to explicitly set the number of migrations to run
|
||||
m.Steps(2)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -173,11 +166,6 @@ 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
@@ -1,16 +0,0 @@
|
||||
# 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.
|
||||
+11
-15
@@ -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.$os-$arch.tar.gz | tar xvz
|
||||
$ curl -L https://github.com/golang-migrate/migrate/releases/download/$version/migrate.$platform-amd64.tar.gz | tar xvz
|
||||
```
|
||||
|
||||
### MacOS
|
||||
@@ -27,8 +27,8 @@ $ scoop install migrate
|
||||
### Linux (*.deb package)
|
||||
|
||||
```bash
|
||||
$ 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
|
||||
$ 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
|
||||
$ 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,18 +86,14 @@ Options:
|
||||
-help Print usage
|
||||
|
||||
Commands:
|
||||
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).
|
||||
|
||||
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.
|
||||
goto V Migrate to version V
|
||||
up [N] Apply all or N up migrations
|
||||
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
|
||||
down [N] Apply all or N down migrations
|
||||
drop Drop everything inside database
|
||||
force V Set version V but don't run migration (ignores dirty state)
|
||||
version Print current migration version
|
||||
```
|
||||
@@ -131,7 +127,7 @@ $ migrate -database "$MY_MIGRATE_DATABASE"
|
||||
Check out https://stedolan.github.io/jq/
|
||||
|
||||
```bash
|
||||
$ migrate -database "$(cat config.json | jq -r '.database')"
|
||||
$ migrate -database "$(cat config.json | jq '.database')"
|
||||
```
|
||||
|
||||
### YAML files
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
# Cassandra / ScyllaDB
|
||||
# Cassandra
|
||||
|
||||
* `Drop()` method will not work on Cassandra 2.X because it rely on
|
||||
* Drop command will not work on Cassandra 2.X because it rely on
|
||||
system_schema table which comes with 3.X
|
||||
* Other methods should work properly but are **not tested**
|
||||
* Other commands 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`
|
||||
@@ -25,7 +20,6 @@ system_schema table which comes with 3.X
|
||||
| `consistency` | ALL | Migration consistency
|
||||
| `protocol` | | Cassandra protocol version (3 or 4)
|
||||
| `timeout` | 1 minute | Migration timeout
|
||||
| `connect-timeout` | 600ms | Initial connection timeout to the cluster |
|
||||
| `username` | nil | Username to use when authenticating. |
|
||||
| `password` | nil | Password to use when authenticating. |
|
||||
| `sslcert` | | Cert file location. The file must contain PEM encoded data. |
|
||||
|
||||
@@ -3,16 +3,18 @@ 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() {
|
||||
@@ -131,14 +133,6 @@ func (c *Cassandra) Open(url string) (database.Driver, error) {
|
||||
}
|
||||
cluster.Timeout = timeout
|
||||
}
|
||||
if len(u.Query().Get("connect-timeout")) > 0 {
|
||||
var connectTimeout time.Duration
|
||||
connectTimeout, err = time.ParseDuration(u.Query().Get("connect-timeout"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cluster.ConnectTimeout = connectTimeout
|
||||
}
|
||||
|
||||
if len(u.Query().Get("sslmode")) > 0 {
|
||||
if u.Query().Get("sslmode") != "disable" {
|
||||
@@ -197,14 +191,14 @@ func (c *Cassandra) Close() error {
|
||||
}
|
||||
|
||||
func (c *Cassandra) Lock() error {
|
||||
if !c.isLocked.CompareAndSwap(false, true) {
|
||||
if !c.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cassandra) Unlock() error {
|
||||
if !c.isLocked.CompareAndSwap(true, false) {
|
||||
if !c.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
@@ -229,7 +223,7 @@ func (c *Cassandra) Run(migration io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -314,7 +308,11 @@ func (c *Cassandra) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := c.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -336,7 +334,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,11 +25,9 @@ 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},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -61,20 +59,6 @@ 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 {
|
||||
@@ -95,7 +79,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 {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# ClickHouse
|
||||
|
||||
`clickhouse://host:port?username=user&password=qwerty&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.
|
||||
@@ -1,43 +1,26 @@
|
||||
// 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
|
||||
package clickhouse
|
||||
|
||||
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"
|
||||
|
||||
// Registers the "datastore" database/sql driver (ClickHouse-wire native).
|
||||
_ "github.com/hanzo-ds/go"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
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
|
||||
@@ -55,11 +38,9 @@ type Config struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
database.Register("datastore", &Datastore{})
|
||||
database.Register("clickhouse", &ClickHouse{})
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -69,34 +50,32 @@ func WithInstance(conn *sql.DB, config *Config) (database.Driver, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ds := &Datastore{
|
||||
ch := &ClickHouse{
|
||||
conn: conn,
|
||||
config: config,
|
||||
}
|
||||
|
||||
if err := ds.init(); err != nil {
|
||||
if err := ch.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// Datastore is a golang-migrate database.Driver backed by the native
|
||||
// github.com/hanzo-ds/go ClickHouse-wire driver.
|
||||
type Datastore struct {
|
||||
type ClickHouse struct {
|
||||
conn *sql.DB
|
||||
config *Config
|
||||
isLocked atomic.Bool
|
||||
}
|
||||
|
||||
func (ds *Datastore) Open(dsn string) (database.Driver, error) {
|
||||
func (ch *ClickHouse) Open(dsn string) (database.Driver, error) {
|
||||
purl, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := migrate.FilterCustomQuery(purl)
|
||||
q.Scheme = DriverName
|
||||
conn, err := sql.Open(DriverName, q.String())
|
||||
q.Scheme = "tcp"
|
||||
conn, err := sql.Open("clickhouse", q.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -114,7 +93,7 @@ func (ds *Datastore) Open(dsn string) (database.Driver, error) {
|
||||
migrationsTableEngine = s
|
||||
}
|
||||
|
||||
ds = &Datastore{
|
||||
ch = &ClickHouse{
|
||||
conn: conn,
|
||||
config: &Config{
|
||||
MigrationsTable: purl.Query().Get("x-migrations-table"),
|
||||
@@ -126,44 +105,44 @@ func (ds *Datastore) Open(dsn string) (database.Driver, error) {
|
||||
},
|
||||
}
|
||||
|
||||
if err := ds.init(); err != nil {
|
||||
if err := ch.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
return ch, 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 {
|
||||
func (ch *ClickHouse) init() error {
|
||||
if len(ch.config.DatabaseName) == 0 {
|
||||
if err := ch.conn.QueryRow("SELECT currentDatabase()").Scan(&ch.config.DatabaseName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(ds.config.MigrationsTable) == 0 {
|
||||
ds.config.MigrationsTable = DefaultMigrationsTable
|
||||
if len(ch.config.MigrationsTable) == 0 {
|
||||
ch.config.MigrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
if ds.config.MultiStatementMaxSize <= 0 {
|
||||
ds.config.MultiStatementMaxSize = DefaultMultiStatementMaxSize
|
||||
if ch.config.MultiStatementMaxSize <= 0 {
|
||||
ch.config.MultiStatementMaxSize = DefaultMultiStatementMaxSize
|
||||
}
|
||||
|
||||
if len(ds.config.MigrationsTableEngine) == 0 {
|
||||
ds.config.MigrationsTableEngine = DefaultMigrationsTableEngine
|
||||
if len(ch.config.MigrationsTableEngine) == 0 {
|
||||
ch.config.MigrationsTableEngine = DefaultMigrationsTableEngine
|
||||
}
|
||||
|
||||
return ds.ensureVersionTable()
|
||||
return ch.ensureVersionTable()
|
||||
}
|
||||
|
||||
func (ds *Datastore) Run(r io.Reader) error {
|
||||
if ds.config.MultiStatementEnabled {
|
||||
func (ch *ClickHouse) Run(r io.Reader) error {
|
||||
if ch.config.MultiStatementEnabled {
|
||||
var err error
|
||||
if e := multistmt.Parse(r, multiStmtDelimiter, ds.config.MultiStatementMaxSize, func(m []byte) bool {
|
||||
if e := multistmt.Parse(r, multiStmtDelimiter, ch.config.MultiStatementMaxSize, func(m []byte) bool {
|
||||
tq := strings.TrimSpace(string(m))
|
||||
if tq == "" {
|
||||
return true
|
||||
}
|
||||
if _, e := ds.conn.Exec(string(m)); e != nil {
|
||||
if _, e := ch.conn.Exec(string(m)); e != nil {
|
||||
err = database.Error{OrigErr: e, Err: "migration failed", Query: m}
|
||||
return false
|
||||
}
|
||||
@@ -174,25 +153,24 @@ func (ds *Datastore) Run(r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
migration, err := io.ReadAll(r)
|
||||
migration, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ds.conn.Exec(string(migration)); err != nil {
|
||||
if _, err := ch.conn.Exec(string(migration)); err != nil {
|
||||
return database.Error{OrigErr: err, Err: "migration failed", Query: migration}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) Version() (int, bool, error) {
|
||||
func (ch *ClickHouse) Version() (int, bool, error) {
|
||||
var (
|
||||
version int
|
||||
dirty uint8
|
||||
query = "SELECT version, dirty FROM `" + ds.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1"
|
||||
query = "SELECT version, dirty FROM `" + ch.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1"
|
||||
)
|
||||
if err := ds.conn.QueryRow(query).Scan(&version, &dirty); err != nil {
|
||||
if err := ch.conn.QueryRow(query).Scan(&version, &dirty); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
@@ -201,7 +179,7 @@ func (ds *Datastore) Version() (int, bool, error) {
|
||||
return version, dirty == 1, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) SetVersion(version int, dirty bool) error {
|
||||
func (ch *ClickHouse) SetVersion(version int, dirty bool) error {
|
||||
var (
|
||||
bool = func(v bool) uint8 {
|
||||
if v {
|
||||
@@ -209,13 +187,13 @@ func (ds *Datastore) SetVersion(version int, dirty bool) error {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
tx, err = ds.conn.Begin()
|
||||
tx, err = ch.conn.Begin()
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := "INSERT INTO " + ds.config.MigrationsTable + " (version, dirty, sequence) VALUES (?, ?, ?)"
|
||||
query := "INSERT INTO " + ch.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)}
|
||||
}
|
||||
@@ -225,24 +203,28 @@ func (ds *Datastore) 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 Datastore type.
|
||||
func (ds *Datastore) ensureVersionTable() (err error) {
|
||||
if err = ds.Lock(); err != nil {
|
||||
// convention of "caller locks" in the ClickHouse type.
|
||||
func (ch *ClickHouse) ensureVersionTable() (err error) {
|
||||
if err = ch.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := ds.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if e := ch.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
table string
|
||||
query = "SHOW TABLES FROM " + quoteIdentifier(ds.config.DatabaseName) + " LIKE '" + ds.config.MigrationsTable + "'"
|
||||
query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'"
|
||||
)
|
||||
// check if migration table exists
|
||||
if err := ds.conn.QueryRow(query).Scan(&table); err != nil {
|
||||
if err := ch.conn.QueryRow(query).Scan(&table); err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -251,42 +233,42 @@ func (ds *Datastore) ensureVersionTable() (err error) {
|
||||
}
|
||||
|
||||
// if not, create the empty migration table
|
||||
if len(ds.config.ClusterName) > 0 {
|
||||
if len(ch.config.ClusterName) > 0 {
|
||||
query = fmt.Sprintf(`
|
||||
CREATE TABLE %s ON CLUSTER %s (
|
||||
version Int64,
|
||||
dirty UInt8,
|
||||
sequence UInt64
|
||||
) Engine=%s`, ds.config.MigrationsTable, ds.config.ClusterName, ds.config.MigrationsTableEngine)
|
||||
) Engine=%s`, ch.config.MigrationsTable, ch.config.ClusterName, ch.config.MigrationsTableEngine)
|
||||
} else {
|
||||
query = fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
version Int64,
|
||||
dirty UInt8,
|
||||
sequence UInt64
|
||||
) Engine=%s`, ds.config.MigrationsTable, ds.config.MigrationsTableEngine)
|
||||
) Engine=%s`, ch.config.MigrationsTable, ch.config.MigrationsTableEngine)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(ds.config.MigrationsTableEngine, "Tree") {
|
||||
if strings.HasSuffix(ch.config.MigrationsTableEngine, "Tree") {
|
||||
query = fmt.Sprintf(`%s ORDER BY sequence`, query)
|
||||
}
|
||||
|
||||
if _, err := ds.conn.Exec(query); err != nil {
|
||||
if _, err := ch.conn.Exec(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) Drop() (err error) {
|
||||
query := "SHOW TABLES FROM " + quoteIdentifier(ds.config.DatabaseName)
|
||||
tables, err := ds.conn.Query(query)
|
||||
func (ch *ClickHouse) Drop() (err error) {
|
||||
query := "SHOW TABLES FROM " + ch.config.DatabaseName
|
||||
tables, err := ch.conn.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)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -296,9 +278,9 @@ func (ds *Datastore) Drop() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
query = "DROP TABLE IF EXISTS " + quoteIdentifier(ds.config.DatabaseName) + "." + quoteIdentifier(table)
|
||||
query = "DROP TABLE IF EXISTS " + ch.config.DatabaseName + "." + table
|
||||
|
||||
if _, err := ds.conn.Exec(query); err != nil {
|
||||
if _, err := ch.conn.Exec(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
}
|
||||
@@ -309,29 +291,18 @@ func (ds *Datastore) Drop() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) Lock() error {
|
||||
if !ds.isLocked.CompareAndSwap(false, true) {
|
||||
func (ch *ClickHouse) Lock() error {
|
||||
if !ch.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) Unlock() error {
|
||||
if !ds.isLocked.CompareAndSwap(true, false) {
|
||||
func (ch *ClickHouse) Unlock() error {
|
||||
if !ch.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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, `"`, `""`) + `"`
|
||||
}
|
||||
func (ch *ClickHouse) Close() error { return ch.conn.Close() }
|
||||
@@ -1,4 +1,4 @@
|
||||
package datastore_test
|
||||
package clickhouse_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,15 +8,13 @@ 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/datastore"
|
||||
"github.com/golang-migrate/migrate/v4/database/clickhouse"
|
||||
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
|
||||
@@ -28,19 +26,19 @@ var (
|
||||
PortRequired: true, ReadyFunc: isReady,
|
||||
}
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "clickhouse:24.8", Options: opts},
|
||||
{ImageName: "yandex/clickhouse-server:21.3", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
func datastoreConnectionString(host, port, engine string) string {
|
||||
func clickhouseConnectionString(host, port, engine string) string {
|
||||
if engine != "" {
|
||||
return fmt.Sprintf(
|
||||
"datastore://%v:%v?username=user&password=password&database=db&x-multi-statement=true&x-migrations-table-engine=%v&debug=false",
|
||||
"clickhouse://%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(
|
||||
"datastore://%v:%v?username=user&password=password&database=db&x-multi-statement=true&debug=false",
|
||||
"clickhouse://%v:%v?username=user&password=password&database=db&x-multi-statement=true&debug=false",
|
||||
host, port)
|
||||
}
|
||||
|
||||
@@ -50,7 +48,7 @@ func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
db, err := sql.Open("datastore", datastoreConnectionString(ip, port, ""))
|
||||
db, err := sql.Open("clickhouse", clickhouseConnectionString(ip, port, ""))
|
||||
|
||||
if err != nil {
|
||||
log.Println("open error", err)
|
||||
@@ -92,8 +90,8 @@ func testSimple(t *testing.T, engine string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := datastoreConnectionString(ip, port, engine)
|
||||
p := &datastore.Datastore{}
|
||||
addr := clickhouseConnectionString(ip, port, engine)
|
||||
p := &clickhouse.ClickHouse{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -115,12 +113,12 @@ func testSimpleWithInstanceDefaultConfigValues(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := datastoreConnectionString(ip, port, "")
|
||||
conn, err := sql.Open("datastore", addr)
|
||||
addr := clickhouseConnectionString(ip, port, "")
|
||||
conn, err := sql.Open("clickhouse", addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := datastore.WithInstance(conn, &datastore.Config{})
|
||||
d, err := clickhouse.WithInstance(conn, &clickhouse.Config{})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
t.Fatal(err)
|
||||
@@ -142,8 +140,8 @@ func testMigrate(t *testing.T, engine string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := datastoreConnectionString(ip, port, engine)
|
||||
p := &datastore.Datastore{}
|
||||
addr := clickhouseConnectionString(ip, port, engine)
|
||||
p := &clickhouse.ClickHouse{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -171,8 +169,8 @@ func testVersion(t *testing.T, engine string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := datastoreConnectionString(ip, port, engine)
|
||||
p := &datastore.Datastore{}
|
||||
addr := clickhouseConnectionString(ip, port, engine)
|
||||
p := &clickhouse.ClickHouse{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -206,8 +204,8 @@ func testDrop(t *testing.T, engine string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := datastoreConnectionString(ip, port, engine)
|
||||
p := &datastore.Datastore{}
|
||||
addr := clickhouseConnectionString(ip, port, engine)
|
||||
p := &clickhouse.ClickHouse{}
|
||||
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;
|
||||
@@ -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 convenience:
|
||||
When using Migrate CLI we need to pass to database URL. Let's export it to a variable for convienience:
|
||||
```
|
||||
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,18 +3,24 @@ 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() {
|
||||
@@ -161,7 +167,7 @@ func (c *CockroachDb) Lock() error {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := rows.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -211,7 +217,7 @@ func (c *CockroachDb) Unlock() error {
|
||||
}
|
||||
|
||||
func (c *CockroachDb) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -276,7 +282,7 @@ func (c *CockroachDb) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -318,7 +324,11 @@ func (c *CockroachDb) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := c.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@ import (
|
||||
const defaultPort = 26257
|
||||
|
||||
var (
|
||||
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
|
||||
opts = dktest.Options{Cmd: []string{"start", "--insecure"}, PortRequired: true, ReadyFunc: isReady}
|
||||
// Released versions: https://www.cockroachlabs.com/docs/releases/
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{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},
|
||||
{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},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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,10 +0,0 @@
|
||||
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;
|
||||
@@ -1,81 +0,0 @@
|
||||
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 Driver interface.
|
||||
// Package database provides the Database 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,19 +1,20 @@
|
||||
//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() {
|
||||
@@ -107,21 +108,21 @@ func (f *Firebird) Close() error {
|
||||
}
|
||||
|
||||
func (f *Firebird) Lock() error {
|
||||
if !f.isLocked.CompareAndSwap(false, true) {
|
||||
if !f.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firebird) Unlock() error {
|
||||
if !f.isLocked.CompareAndSwap(true, false) {
|
||||
if !f.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firebird) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -180,7 +181,7 @@ func (f *Firebird) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -223,7 +224,11 @@ func (f *Firebird) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := f.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -38,9 +38,8 @@ var (
|
||||
},
|
||||
}
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "jacobalberty/firebird:v3.0", Options: opts},
|
||||
{ImageName: "jacobalberty/firebird:v4.0", Options: opts},
|
||||
{ImageName: "jacobalberty/firebird:v5.0", Options: opts},
|
||||
{ImageName: "jacobalberty/firebird:2.5-ss", Options: opts},
|
||||
{ImageName: "jacobalberty/firebird:3.0", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
| `x-transaction-mode` | `TransactionMode` | If set to `true` wrap commands in [transaction](https://docs.mongodb.com/manual/core/transactions). Available only for replica set. Driver is using [strconv.ParseBool](https://golang.org/pkg/strconv/#ParseBool) for parsing|
|
||||
| `x-advisory-locking` | `true` | Feature flag for advisory locking, if set to false, disable advisory locking |
|
||||
| `x-advisory-lock-collection` | `migrate_advisory_lock` | The name of the collection to use for advisory locking.|
|
||||
| `x-advisory-lock-timeout` | `15` | The max time in seconds that migrate will wait to acquire a lock before failing. |
|
||||
| `x-advisory-lock-timeout-interval` | `10` | The max time in seconds between attempts to acquire the advisory lock, the lock is attempted to be acquired using an exponential backoff algorithm. |
|
||||
| `x-advisory-lock-timout` | `15` | The max time in seconds that the advisory lock will wait if the db is already locked. |
|
||||
| `x-advisory-lock-timout-interval` | `10` | The max timeout in seconds interval that the advisory lock will wait if the db is already locked. |
|
||||
| `dbname` | `DatabaseName` | The name of the database to connect to |
|
||||
| `user` | | The user to sign in as. Can be omitted |
|
||||
| `password` | | The user's password. Can be omitted |
|
||||
|
||||
+23
-35
@@ -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() {
|
||||
@@ -36,9 +36,8 @@ const LockIndexName = "lock_unique_key" // the name of the inde
|
||||
const contextWaitTimeout = 5 * time.Second // how long to wait for the request to mongo to block/wait for.
|
||||
|
||||
var (
|
||||
ErrNoDatabaseName = fmt.Errorf("no database name")
|
||||
ErrNilConfig = fmt.Errorf("no config")
|
||||
ErrLockTimeoutConfigConflict = fmt.Errorf("both x-advisory-lock-timeout-interval and x-advisory-lock-timout-interval were specified")
|
||||
ErrNoDatabaseName = fmt.Errorf("no database name")
|
||||
ErrNilConfig = fmt.Errorf("no config")
|
||||
)
|
||||
|
||||
type Mongo struct {
|
||||
@@ -114,7 +113,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
|
||||
@@ -138,22 +137,7 @@ func (m *Mongo) Open(dsn string) (database.Driver, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lockTimeoutIntervalValue := unknown.Get("x-advisory-lock-timeout-interval")
|
||||
// The initial release had a typo for this argument but for backwards compatibility sake, we will keep supporting it
|
||||
// and we will error out if both values are set.
|
||||
lockTimeoutIntervalValueFromTypo := unknown.Get("x-advisory-lock-timout-interval")
|
||||
|
||||
lockTimeout := lockTimeoutIntervalValue
|
||||
|
||||
if lockTimeoutIntervalValue != "" && lockTimeoutIntervalValueFromTypo != "" {
|
||||
return nil, ErrLockTimeoutConfigConflict
|
||||
} else if lockTimeoutIntervalValueFromTypo != "" {
|
||||
lockTimeout = lockTimeoutIntervalValueFromTypo
|
||||
}
|
||||
|
||||
maxLockCheckInterval, err := parseInt(lockTimeout, DefaultLockTimeoutInterval)
|
||||
|
||||
maxLockingIntervals, err := parseInt(unknown.Get("x-advisory-lock-timout-interval"), DefaultLockTimeoutInterval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -173,7 +157,7 @@ func (m *Mongo) Open(dsn string) (database.Driver, error) {
|
||||
CollectionName: lockCollection,
|
||||
Timeout: lockingTimout,
|
||||
Enabled: advisoryLockingFlag,
|
||||
Interval: maxLockCheckInterval,
|
||||
Interval: maxLockingIntervals,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -182,7 +166,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 +183,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 +225,7 @@ func (m *Mongo) Version() (version int, dirty bool, err error) {
|
||||
}
|
||||
|
||||
func (m *Mongo) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -268,8 +252,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,7 +309,11 @@ func (m *Mongo) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(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:5.0", Options: opts},
|
||||
{ImageName: "mongo:6.0", Options: opts},
|
||||
{ImageName: "mongo:7.0", Options: opts},
|
||||
{ImageName: "mongo:8.0", Options: opts},
|
||||
{ImageName: "mongo:3.4", Options: opts},
|
||||
{ImageName: "mongo:3.6", Options: opts},
|
||||
{ImageName: "mongo:4.0", Options: opts},
|
||||
{ImageName: "mongo:4.2", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -74,22 +74,6 @@ 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 {
|
||||
@@ -115,7 +99,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 {
|
||||
@@ -141,7 +125,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 {
|
||||
@@ -196,7 +180,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 {
|
||||
@@ -257,15 +241,6 @@ 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,7 +6,6 @@
|
||||
|------------|---------------------|-------------|
|
||||
| `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 |
|
||||
@@ -14,7 +13,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 certificate file. Must be used with `x-tls-key`. |
|
||||
| `x-tls-cert` | | The location of the client certicicate 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) |
|
||||
|
||||
@@ -29,9 +28,9 @@ import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/mysql"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/golang-migrate/migrate"
|
||||
"github.com/golang-migrate/migrate/database/mysql"
|
||||
_ "github.com/golang-migrate/migrate/source/file"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
+24
-38
@@ -1,4 +1,5 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package mysql
|
||||
|
||||
@@ -7,18 +8,17 @@ 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,14 +34,13 @@ 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
|
||||
StatementTimeout time.Duration
|
||||
MigrationsTable string
|
||||
DatabaseName string
|
||||
NoLock bool
|
||||
}
|
||||
|
||||
type Mysql struct {
|
||||
@@ -155,7 +154,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 := os.ReadFile(parsedParams.Get("x-tls-ca"))
|
||||
pem, err := ioutil.ReadFile(parsedParams.Get("x-tls-ca"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -242,25 +241,15 @@ 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,
|
||||
StatementTimeout: time.Duration(statementTimeout) * time.Millisecond,
|
||||
DatabaseName: config.DBName,
|
||||
MigrationsTable: customParams["x-migrations-table"],
|
||||
NoLock: noLock,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -333,20 +322,13 @@ func (m *Mysql) Unlock() error {
|
||||
}
|
||||
|
||||
func (m *Mysql) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.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(ctx, query); err != nil {
|
||||
if _, err := m.conn.ExecContext(context.Background(), query); err != nil {
|
||||
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
|
||||
}
|
||||
|
||||
@@ -354,15 +336,15 @@ func (m *Mysql) Run(migration io.Reader) error {
|
||||
}
|
||||
|
||||
func (m *Mysql) SetVersion(version int, dirty bool) error {
|
||||
tx, err := m.conn.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable})
|
||||
tx, err := m.conn.BeginTx(context.Background(), &sql.TxOptions{})
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Err: "transaction start failed"}
|
||||
}
|
||||
|
||||
query := "DELETE FROM `" + m.config.MigrationsTable + "` LIMIT 1"
|
||||
query := "TRUNCATE `" + m.config.MigrationsTable + "`"
|
||||
if _, err := tx.ExecContext(context.Background(), query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -374,7 +356,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -416,7 +398,7 @@ func (m *Mysql) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -469,7 +451,11 @@ func (m *Mysql) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
@@ -16,14 +17,19 @@ 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
|
||||
@@ -40,14 +46,16 @@ var (
|
||||
}
|
||||
// Supported versions: https://www.mysql.com/support/supportedplatforms/database.html
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "mysql:8.0", Options: opts},
|
||||
{ImageName: "mysql:8.4", Options: opts},
|
||||
{ImageName: "mysql:9.0", Options: opts},
|
||||
{ImageName: "mysql:5.5", Options: opts},
|
||||
{ImageName: "mysql:5.6", Options: opts},
|
||||
{ImageName: "mysql:5.7", Options: opts},
|
||||
{ImageName: "mysql:8", Options: opts},
|
||||
}
|
||||
specsAnsiQuotes = []dktesting.ContainerSpec{
|
||||
{ImageName: "mysql:8.0", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:8.4", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:9.0", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:5.5", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:5.6", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:5.7", Options: optsAnsiQuotes},
|
||||
{ImageName: "mysql:8", Options: optsAnsiQuotes},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -80,7 +88,7 @@ func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
// mysql.SetLogger(mysql.Logger(log.New(io.Discard, "", log.Ltime)))
|
||||
// mysql.SetLogger(mysql.Logger(log.New(ioutil.Discard, "", log.Ltime)))
|
||||
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -113,7 +121,7 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
// mysql.SetLogger(mysql.Logger(log.New(io.Discard, "", log.Ltime)))
|
||||
// mysql.SetLogger(mysql.Logger(log.New(ioutil.Discard, "", log.Ltime)))
|
||||
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -151,7 +159,7 @@ func TestMigrate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrateAnsiQuotes(t *testing.T) {
|
||||
// mysql.SetLogger(mysql.Logger(log.New(io.Discard, "", log.Ltime)))
|
||||
// mysql.SetLogger(mysql.Logger(log.New(ioutil.Discard, "", log.Ltime)))
|
||||
|
||||
dktesting.ParallelTest(t, specsAnsiQuotes, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
@@ -334,7 +342,7 @@ func TestExtractCustomQueryParams(t *testing.T) {
|
||||
}
|
||||
|
||||
func createTmpCert(t *testing.T) string {
|
||||
tmpCertFile, err := os.CreateTemp("", "migrate_test_cert")
|
||||
tmpCertFile, err := ioutil.TempFile("", "migrate_test_cert")
|
||||
if err != nil {
|
||||
t.Fatal("Failed to create temp cert file:", err)
|
||||
}
|
||||
@@ -389,7 +397,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::password@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
|
||||
// expectedDSN: "username::pasword@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,15 +2,16 @@ 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"
|
||||
)
|
||||
|
||||
@@ -141,7 +142,7 @@ func (n *Neo4j) Run(migration io.Reader) (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = errors.Join(err, cerr)
|
||||
err = multierror.Append(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -172,7 +173,7 @@ func (n *Neo4j) Run(migration io.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(migration)
|
||||
body, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -188,7 +189,7 @@ func (n *Neo4j) SetVersion(version int, dirty bool) (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = errors.Join(err, cerr)
|
||||
err = multierror.Append(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -213,7 +214,7 @@ func (n *Neo4j) Version() (version int, dirty bool, err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = errors.Join(err, cerr)
|
||||
err = multierror.Append(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -261,7 +262,7 @@ func (n *Neo4j) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = errors.Join(err, cerr)
|
||||
err = multierror.Append(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -278,7 +279,7 @@ func (n *Neo4j) ensureVersionConstraint() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if cerr := session.Close(); cerr != nil {
|
||||
err = errors.Join(err, cerr)
|
||||
err = multierror.Append(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -8,15 +8,13 @@ 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
|
||||
|
||||
@@ -66,7 +64,7 @@ func TestUserUnencodedReservedURLChars(t *testing.T) {
|
||||
testedChars := make([]string, 0, len(reservedChars))
|
||||
for _, tc := range testcases {
|
||||
testedChars = append(testedChars, tc.char)
|
||||
t.Run(reservedCharTestNamePrefix+tc.char, func(t *testing.T) {
|
||||
t.Run("reserved char "+tc.char, func(t *testing.T) {
|
||||
s := scheme + baseUsername + tc.char + urlSuffixAndSep
|
||||
u, err := url.Parse(s)
|
||||
if err == nil {
|
||||
@@ -100,12 +98,13 @@ 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(reservedCharTestNamePrefix+c, func(t *testing.T) {
|
||||
t.Run("reserved char "+c, func(t *testing.T) {
|
||||
encodedChar := "%" + hex.EncodeToString([]byte(c))
|
||||
s := scheme + baseUsername + encodedChar + urlSuffixAndSep
|
||||
expectedUsername := baseUsername + c
|
||||
@@ -127,7 +126,7 @@ func TestUserEncodedReservedURLChars(t *testing.T) {
|
||||
// with net/url Parse()
|
||||
func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
username := baseUsername
|
||||
schemeAndUsernameAndSep := scheme + username + ":"
|
||||
schemeAndUsernameAndSep := "database://" + username + ":"
|
||||
basePassword := "password"
|
||||
urlSuffixAndSep := "@localhost:12345/myDB?someParam=true"
|
||||
|
||||
@@ -175,7 +174,7 @@ func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
testedChars := make([]string, 0, len(reservedChars))
|
||||
for _, tc := range testcases {
|
||||
testedChars = append(testedChars, tc.char)
|
||||
t.Run(reservedCharTestNamePrefix+tc.char, func(t *testing.T) {
|
||||
t.Run("reserved char "+tc.char, func(t *testing.T) {
|
||||
s := schemeAndUsernameAndSep + basePassword + tc.char + urlSuffixAndSep
|
||||
u, err := url.Parse(s)
|
||||
if err == nil {
|
||||
@@ -214,13 +213,13 @@ func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
|
||||
func TestPasswordEncodedReservedURLChars(t *testing.T) {
|
||||
username := baseUsername
|
||||
schemeAndUsernameAndSep := scheme + username + ":"
|
||||
schemeAndUsernameAndSep := "database://" + username + ":"
|
||||
basePassword := "password"
|
||||
urlSuffixAndSep := "@localhost:12345/myDB?someParam=true"
|
||||
|
||||
for _, c := range reservedChars {
|
||||
c := string(c)
|
||||
t.Run(reservedCharTestNamePrefix+c, func(t *testing.T) {
|
||||
t.Run("reserved char "+c, func(t *testing.T) {
|
||||
encodedChar := "%" + hex.EncodeToString([]byte(c))
|
||||
s := schemeAndUsernameAndSep + basePassword + encodedChar + urlSuffixAndSep
|
||||
expectedPassword := basePassword + c
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 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 |
|
||||
@@ -11,8 +9,6 @@ This package is for [pgx/v4](https://pkg.go.dev/github.com/jackc/pgx/v4). A back
|
||||
| `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 |
|
||||
|
||||
+38
-168
@@ -1,38 +1,33 @@
|
||||
//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 (
|
||||
@@ -40,8 +35,6 @@ var (
|
||||
|
||||
DefaultMigrationsTable = "schema_migrations"
|
||||
DefaultMultiStatementMaxSize = 10 * 1 << 20 // 10 MB
|
||||
DefaultLockTable = "schema_lock"
|
||||
DefaultLockStrategy = LockStrategyAdvisory
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -55,8 +48,6 @@ type Config struct {
|
||||
MigrationsTable string
|
||||
DatabaseName string
|
||||
SchemaName string
|
||||
LockTable string
|
||||
LockStrategy string
|
||||
migrationsSchemaName string
|
||||
migrationsTableName string
|
||||
StatementTimeout time.Duration
|
||||
@@ -116,14 +107,6 @@ 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 {
|
||||
@@ -149,10 +132,6 @@ 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
|
||||
}
|
||||
@@ -171,7 +150,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/v4", migrate.FilterCustomQuery(purl).String())
|
||||
db, err := sql.Open("pgx", migrate.FilterCustomQuery(purl).String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -181,7 +160,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] != '"')) {
|
||||
@@ -212,13 +191,10 @@ 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,
|
||||
@@ -226,8 +202,6 @@ 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 {
|
||||
@@ -246,118 +220,38 @@ 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 {
|
||||
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)
|
||||
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 {
|
||||
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)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -371,7 +265,7 @@ func (p *Postgres) Run(migration io.Reader) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -412,7 +306,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.ReplaceAll(s, "\r\n", "\n")
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
// pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes
|
||||
runes := []rune(s)
|
||||
if pos > len(runes) {
|
||||
@@ -454,7 +348,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -466,7 +360,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -508,7 +402,7 @@ func (p *Postgres) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -519,12 +413,6 @@ 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)
|
||||
}
|
||||
@@ -556,7 +444,11 @@ func (p *Postgres) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -585,33 +477,11 @@ 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.ReplaceAll(name, `"`, `""`) + `"`
|
||||
return `"` + strings.Replace(name, `"`, `""`, -1) + `"`
|
||||
}
|
||||
|
||||
+12
-36
@@ -8,17 +8,19 @@ import (
|
||||
sqldriver "database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
|
||||
"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"
|
||||
@@ -34,11 +36,11 @@ var (
|
||||
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},
|
||||
{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},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -131,32 +133,6 @@ 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()
|
||||
@@ -755,10 +731,10 @@ func Test_computeLineFromPos(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
input := tc.input
|
||||
if crlf {
|
||||
input = strings.ReplaceAll(input, "\n", "\r\n")
|
||||
input = strings.Replace(input, "\n", "\r\n", -1)
|
||||
}
|
||||
if nonASCII {
|
||||
input = strings.ReplaceAll(input, "FROM", "FRÖM")
|
||||
input = strings.Replace(input, "FROM", "FRÖM", -1)
|
||||
}
|
||||
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,480 +0,0 @@
|
||||
//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, `"`, `""`) + `"`
|
||||
}
|
||||
@@ -1,761 +0,0 @@
|
||||
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,23 +1,25 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package postgres
|
||||
|
||||
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/lib/pq"
|
||||
)
|
||||
|
||||
@@ -63,19 +65,19 @@ type Postgres struct {
|
||||
config *Config
|
||||
}
|
||||
|
||||
func WithConnection(ctx context.Context, conn *sql.Conn, config *Config) (*Postgres, error) {
|
||||
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
|
||||
if config == nil {
|
||||
return nil, ErrNilConfig
|
||||
}
|
||||
|
||||
if err := conn.PingContext(ctx); err != nil {
|
||||
if err := instance.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.DatabaseName == "" {
|
||||
query := `SELECT CURRENT_DATABASE()`
|
||||
var databaseName string
|
||||
if err := conn.QueryRowContext(ctx, query).Scan(&databaseName); err != nil {
|
||||
if err := instance.QueryRow(query).Scan(&databaseName); err != nil {
|
||||
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
@@ -88,16 +90,16 @@ func WithConnection(ctx context.Context, conn *sql.Conn, config *Config) (*Postg
|
||||
|
||||
if config.SchemaName == "" {
|
||||
query := `SELECT CURRENT_SCHEMA()`
|
||||
var schemaName sql.NullString
|
||||
if err := conn.QueryRowContext(ctx, query).Scan(&schemaName); err != nil {
|
||||
var schemaName string
|
||||
if err := instance.QueryRow(query).Scan(&schemaName); err != nil {
|
||||
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if !schemaName.Valid {
|
||||
if len(schemaName) == 0 {
|
||||
return nil, ErrNoSchema
|
||||
}
|
||||
|
||||
config.SchemaName = schemaName.String
|
||||
config.SchemaName = schemaName
|
||||
}
|
||||
|
||||
if len(config.MigrationsTable) == 0 {
|
||||
@@ -117,8 +119,15 @@ func WithConnection(ctx context.Context, conn *sql.Conn, config *Config) (*Postg
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := instance.Conn(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
px := &Postgres{
|
||||
conn: conn,
|
||||
db: instance,
|
||||
config: config,
|
||||
}
|
||||
|
||||
@@ -129,26 +138,6 @@ func WithConnection(ctx context.Context, conn *sql.Conn, config *Config) (*Postg
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := instance.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := instance.Conn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
px, err := WithConnection(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
px.db = instance
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
purl, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
@@ -165,7 +154,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] != '"')) {
|
||||
@@ -196,7 +185,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,11 +207,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
|
||||
func (p *Postgres) Close() error {
|
||||
connErr := p.conn.Close()
|
||||
var dbErr error
|
||||
if p.db != nil {
|
||||
dbErr = p.db.Close()
|
||||
}
|
||||
|
||||
dbErr := p.db.Close()
|
||||
if connErr != nil || dbErr != nil {
|
||||
return fmt.Errorf("conn: %v, db: %v", connErr, dbErr)
|
||||
}
|
||||
@@ -275,7 +260,7 @@ func (p *Postgres) Run(migration io.Reader) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -319,7 +304,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.ReplaceAll(s, "\r\n", "\n")
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
// pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes
|
||||
runes := []rune(s)
|
||||
if pos > len(runes) {
|
||||
@@ -361,7 +346,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -373,7 +358,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -415,7 +400,7 @@ func (p *Postgres) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -457,7 +442,11 @@ func (p *Postgres) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -35,11 +35,11 @@ var (
|
||||
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},
|
||||
{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},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -85,32 +85,6 @@ 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 {
|
||||
@@ -132,7 +106,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 {
|
||||
@@ -158,7 +132,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 {
|
||||
@@ -191,7 +165,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 {
|
||||
@@ -224,7 +198,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 {
|
||||
@@ -253,7 +227,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 {
|
||||
@@ -275,7 +249,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 {
|
||||
@@ -345,7 +319,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 {
|
||||
@@ -413,7 +387,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 {
|
||||
@@ -483,7 +457,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 {
|
||||
@@ -560,7 +534,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 {
|
||||
@@ -628,7 +602,7 @@ func testParallelSchema(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testPostgresLock(t *testing.T) {
|
||||
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 {
|
||||
@@ -668,7 +642,7 @@ func testPostgresLock(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testWithInstanceConcurrent(t *testing.T) {
|
||||
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 {
|
||||
@@ -710,44 +684,6 @@ func testWithInstanceConcurrent(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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", pgConnectionString(ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
conn, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p, err := WithConnection(ctx, conn, &Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := p.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
dt.Test(t, p, []byte("SELECT 1"))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_computeLineFromPos(t *testing.T) {
|
||||
testcases := []struct {
|
||||
pos int
|
||||
@@ -798,10 +734,10 @@ func Test_computeLineFromPos(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
input := tc.input
|
||||
if crlf {
|
||||
input = strings.ReplaceAll(input, "\n", "\r\n")
|
||||
input = strings.Replace(input, "\n", "\r\n", -1)
|
||||
}
|
||||
if nonASCII {
|
||||
input = strings.ReplaceAll(input, "FROM", "FRÖM")
|
||||
input = strings.Replace(input, "FROM", "FRÖM", -1)
|
||||
}
|
||||
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
|
||||
|
||||
|
||||
+16
-10
@@ -2,12 +2,14 @@ package ql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
nurl "net/url"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
@@ -71,7 +73,11 @@ func (m *Ql) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -128,7 +134,7 @@ func (m *Ql) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -161,19 +167,19 @@ func (m *Ql) Drop() (err error) {
|
||||
return nil
|
||||
}
|
||||
func (m *Ql) Lock() error {
|
||||
if !m.isLocked.CompareAndSwap(false, true) {
|
||||
if !m.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *Ql) Unlock() error {
|
||||
if !m.isLocked.CompareAndSwap(true, false) {
|
||||
if !m.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *Ql) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -188,7 +194,7 @@ func (m *Ql) executeQuery(query string) error {
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -216,7 +222,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
+20
-2
@@ -3,6 +3,8 @@ package ql
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -13,7 +15,15 @@ import (
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "ql-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "ql.db"))
|
||||
p := &Ql{}
|
||||
addr := fmt.Sprintf("ql://%s", filepath.Join(dir, "ql.db"))
|
||||
@@ -35,7 +45,15 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "ql-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "ql.db"))
|
||||
|
||||
db, err := sql.Open("ql", filepath.Join(dir, "ql.db"))
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
//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"
|
||||
)
|
||||
|
||||
@@ -126,21 +128,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.CompareAndSwap(false, true) {
|
||||
if !p.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Redshift) Unlock() error {
|
||||
if !p.isLocked.CompareAndSwap(true, false) {
|
||||
if !p.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Redshift) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -174,7 +176,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.ReplaceAll(s, "\r\n", "\n")
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
// pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes
|
||||
runes := []rune(s)
|
||||
if pos > len(runes) {
|
||||
@@ -216,7 +218,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -228,7 +230,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -270,7 +272,7 @@ func (p *Redshift) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -312,7 +314,11 @@ func (p *Redshift) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -28,19 +28,10 @@ import (
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
const (
|
||||
pgPassword = "redshift"
|
||||
)
|
||||
|
||||
var (
|
||||
opts = dktest.Options{
|
||||
Env: map[string]string{"POSTGRES_PASSWORD": pgPassword},
|
||||
PortRequired: true,
|
||||
ReadyFunc: isReady,
|
||||
}
|
||||
|
||||
opts = dktest.Options{PortRequired: true, ReadyFunc: isReady}
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "migrate/postgres8:8", Options: opts},
|
||||
{ImageName: "postgres:8", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -53,7 +44,7 @@ func pgConnectionString(host, port string) string {
|
||||
}
|
||||
|
||||
func connectionString(schema, host, port string) string {
|
||||
return fmt.Sprintf("%s://postgres:%s@%s:%s/postgres?sslmode=disable", schema, pgPassword, host, port)
|
||||
return fmt.Sprintf("%s://postgres@%s:%s/postgres?sslmode=disable", schema, host, port)
|
||||
}
|
||||
|
||||
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
@@ -201,7 +192,7 @@ func TestFilterCustomQuery(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-custom=foobar", pgPassword, ip, port)
|
||||
addr := fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&x-custom=foobar", ip, port)
|
||||
p := &Redshift{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
@@ -243,7 +234,7 @@ func TestWithSchema(t *testing.T) {
|
||||
}
|
||||
|
||||
// re-connect using that schema
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&search_path=foobar", pgPassword, ip, port))
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&search_path=foobar", ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -378,10 +369,10 @@ func Test_computeLineFromPos(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
input := tc.input
|
||||
if crlf {
|
||||
input = strings.ReplaceAll(input, "\n", "\r\n")
|
||||
input = strings.Replace(input, "\n", "\r\n", -1)
|
||||
}
|
||||
if nonASCII {
|
||||
input = strings.ReplaceAll(input, "FROM", "FRÖM")
|
||||
input = strings.Replace(input, "FROM", "FRÖM", -1)
|
||||
}
|
||||
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS pets;
|
||||
@@ -1,3 +0,0 @@
|
||||
CREATE TABLE pets (
|
||||
name string
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS pets;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE pets ADD predator bool;
|
||||
@@ -1,328 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
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,15 +3,16 @@ 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"
|
||||
)
|
||||
@@ -158,21 +159,21 @@ func (p *Snowflake) Close() error {
|
||||
}
|
||||
|
||||
func (p *Snowflake) Lock() error {
|
||||
if !p.isLocked.CompareAndSwap(false, true) {
|
||||
if !p.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Snowflake) Unlock() error {
|
||||
if !p.isLocked.CompareAndSwap(true, false) {
|
||||
if !p.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Snowflake) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -206,7 +207,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.ReplaceAll(s, "\r\n", "\n")
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
// pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes
|
||||
runes := []rune(s)
|
||||
if pos > len(runes) {
|
||||
@@ -248,7 +249,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -262,7 +263,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -304,7 +305,7 @@ func (p *Snowflake) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -346,7 +347,11 @@ func (p *Snowflake) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(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-statements` is required
|
||||
so in order to be able to use migration with DDL containing comments `x-clean-stamements` is required
|
||||
|
||||
## Multiple statements
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package spanner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
nurl "net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"context"
|
||||
|
||||
"cloud.google.com/go/spanner"
|
||||
sdb "cloud.google.com/go/spanner/admin/database/apiv1"
|
||||
@@ -19,8 +20,10 @@ import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
|
||||
adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
uatomic "go.uber.org/atomic"
|
||||
"google.golang.org/api/iterator"
|
||||
adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -31,6 +34,11 @@ 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")
|
||||
@@ -58,7 +66,7 @@ type Spanner struct {
|
||||
|
||||
config *Config
|
||||
|
||||
lock atomic.Bool
|
||||
lock *uatomic.Uint32
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
@@ -90,6 +98,7 @@ 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 {
|
||||
@@ -146,7 +155,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.CompareAndSwap(false, true); swapped {
|
||||
if swapped := s.lock.CAS(unlockedVal, lockedVal); swapped {
|
||||
return nil
|
||||
}
|
||||
return ErrLockHeld
|
||||
@@ -154,7 +163,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.CompareAndSwap(true, false); swapped {
|
||||
if swapped := s.lock.CAS(lockedVal, unlockedVal); swapped {
|
||||
return nil
|
||||
}
|
||||
return ErrLockNotHeld
|
||||
@@ -162,7 +171,7 @@ func (s *Spanner) Unlock() error {
|
||||
|
||||
// Run implements database.Driver
|
||||
func (s *Spanner) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -298,7 +307,11 @@ func (s *Spanner) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := s.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@ 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"
|
||||
)
|
||||
|
||||
@@ -72,7 +73,11 @@ func (m *Sqlite) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -136,7 +141,7 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -173,21 +178,21 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
|
||||
func (m *Sqlite) Lock() error {
|
||||
if !m.isLocked.CompareAndSwap(false, true) {
|
||||
if !m.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Unlock() error {
|
||||
if !m.isLocked.CompareAndSwap(true, false) {
|
||||
if !m.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -206,7 +211,7 @@ func (m *Sqlite) executeQuery(query string) error {
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -241,7 +246,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package sqlcipher
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -15,7 +17,15 @@ import (
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite3://%s", filepath.Join(dir, "sqlite3.db"))
|
||||
@@ -27,7 +37,15 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
|
||||
db, err := sql.Open("sqlite3", filepath.Join(dir, "sqlite3.db"))
|
||||
@@ -54,7 +72,15 @@ func TestMigrate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrationTable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test-migration-table")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
|
||||
@@ -94,7 +120,15 @@ func TestMigrationTable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoTxWrap(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite3://%s?x-no-tx-wrap=true", filepath.Join(dir, "sqlite3.db"))
|
||||
@@ -108,11 +142,19 @@ func TestNoTxWrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoTxWrapInvalidValue(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite3://%s?x-no-tx-wrap=yeppers", filepath.Join(dir, "sqlite3.db"))
|
||||
_, err := p.Open(addr)
|
||||
_, err = p.Open(addr)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "x-no-tx-wrap")
|
||||
assert.Contains(t, err.Error(), "invalid syntax")
|
||||
|
||||
@@ -2,16 +2,17 @@ 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"
|
||||
)
|
||||
|
||||
@@ -72,7 +73,11 @@ func (m *Sqlite) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -136,7 +141,7 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -173,21 +178,21 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
|
||||
func (m *Sqlite) Lock() error {
|
||||
if !m.isLocked.CompareAndSwap(false, true) {
|
||||
if !m.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Unlock() error {
|
||||
if !m.isLocked.CompareAndSwap(true, false) {
|
||||
if !m.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -206,7 +211,7 @@ func (m *Sqlite) executeQuery(query string) error {
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -241,7 +246,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package sqlite
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -15,7 +17,15 @@ import (
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite://%s", filepath.Join(dir, "sqlite.db"))
|
||||
@@ -27,7 +37,15 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db"))
|
||||
|
||||
db, err := sql.Open("sqlite", filepath.Join(dir, "sqlite.db"))
|
||||
@@ -54,7 +72,15 @@ func TestMigrate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrationTable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite-driver-test-migration-table")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db"))
|
||||
|
||||
@@ -94,7 +120,15 @@ func TestMigrationTable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoTxWrap(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite://%s?x-no-tx-wrap=true", filepath.Join(dir, "sqlite.db"))
|
||||
@@ -108,11 +142,19 @@ func TestNoTxWrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoTxWrapInvalidValue(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite://%s?x-no-tx-wrap=yeppers", filepath.Join(dir, "sqlite.db"))
|
||||
_, err := p.Open(addr)
|
||||
_, err = p.Open(addr)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "x-no-tx-wrap")
|
||||
assert.Contains(t, err.Error(), "invalid syntax")
|
||||
@@ -120,7 +162,15 @@ func TestNoTxWrapInvalidValue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrateWithDirectoryNameContainsWhitespaces(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "directory name contains whitespaces")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
dbPath := filepath.Join(dir, "sqlite.db")
|
||||
t.Logf("DB path : %s\n", dbPath)
|
||||
p := &Sqlite{}
|
||||
|
||||
@@ -2,16 +2,17 @@ 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"
|
||||
)
|
||||
|
||||
@@ -72,7 +73,11 @@ func (m *Sqlite) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := m.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -136,7 +141,7 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
defer func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -173,21 +178,21 @@ func (m *Sqlite) Drop() (err error) {
|
||||
}
|
||||
|
||||
func (m *Sqlite) Lock() error {
|
||||
if !m.isLocked.CompareAndSwap(false, true) {
|
||||
if !m.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Unlock() error {
|
||||
if !m.isLocked.CompareAndSwap(true, false) {
|
||||
if !m.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Sqlite) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -206,7 +211,7 @@ func (m *Sqlite) executeQuery(query string) error {
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -241,7 +246,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 = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package sqlite3
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -15,7 +17,15 @@ import (
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite3://%s", filepath.Join(dir, "sqlite3.db"))
|
||||
@@ -27,7 +37,15 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
|
||||
db, err := sql.Open("sqlite3", filepath.Join(dir, "sqlite3.db"))
|
||||
@@ -54,7 +72,15 @@ func TestMigrate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrationTable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test-migration-table")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
|
||||
@@ -94,7 +120,15 @@ func TestMigrationTable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoTxWrap(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite3://%s?x-no-tx-wrap=true", filepath.Join(dir, "sqlite3.db"))
|
||||
@@ -108,11 +142,19 @@ func TestNoTxWrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoTxWrapInvalidValue(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
|
||||
p := &Sqlite{}
|
||||
addr := fmt.Sprintf("sqlite3://%s?x-no-tx-wrap=yeppers", filepath.Join(dir, "sqlite3.db"))
|
||||
_, err := p.Open(addr)
|
||||
_, err = p.Open(addr)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "x-no-tx-wrap")
|
||||
assert.Contains(t, err.Error(), "invalid syntax")
|
||||
@@ -120,7 +162,15 @@ func TestNoTxWrapInvalidValue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrateWithDirectoryNameContainsWhitespaces(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir, err := ioutil.TempDir("", "directory name contains whitespaces")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
dbPath := filepath.Join(dir, "sqlite3.db")
|
||||
t.Logf("DB path : %s\n", dbPath)
|
||||
p := &Sqlite{}
|
||||
|
||||
@@ -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/microsoft/go-mssqldb for full parameter list.
|
||||
See https://github.com/denisenkom/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/microsoft/go-mssqldb#deprecated for more information.
|
||||
See https://github.com/denisenkom/go-mssqldb#deprecated for more information.
|
||||
|
||||
### Official Support by migrate
|
||||
|
||||
|
||||
@@ -3,18 +3,20 @@ package sqlserver
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"go.uber.org/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"
|
||||
mssql "github.com/microsoft/go-mssqldb" // mssql support
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -29,10 +31,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[int]string{
|
||||
var lockErrorMap = map[mssql.ReturnStatus]string{
|
||||
-1: "The lock request timed out.",
|
||||
-2: "The lock request was canceled.",
|
||||
-3: "The lock request was chosen as a deadlock victim.",
|
||||
@@ -197,24 +199,18 @@ func (ss *SQLServer) Lock() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// This will block until the lock is acquired.
|
||||
// This will either obtain the lock immediately and return true,
|
||||
// or return false if the lock cannot be acquired immediately.
|
||||
// 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 := `
|
||||
DECLARE @lockResult int;
|
||||
EXEC @lockResult = sp_getapplock @Resource = @p1, @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = -1;
|
||||
SELECT @lockResult;`
|
||||
query := `EXEC sp_getapplock @Resource = @p1, @LockMode = 'Update', @LockOwner = 'Session', @LockTimeout = 0`
|
||||
|
||||
var status int
|
||||
if err = ss.conn.QueryRowContext(context.Background(), query, aid).Scan(&status); err == nil && status > -1 {
|
||||
var status mssql.ReturnStatus
|
||||
if _, err = ss.conn.ExecContext(context.Background(), query, aid, &status); err == nil && status > -1 {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)}
|
||||
} else {
|
||||
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)}
|
||||
return &database.Error{Err: fmt.Sprintf("try lock failed with error %v: %v", status, lockErrorMap[status]), Query: []byte(query)}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -239,7 +235,7 @@ func (ss *SQLServer) Unlock() error {
|
||||
|
||||
// Run the migrations for the database
|
||||
func (ss *SQLServer) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
migr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -268,10 +264,10 @@ func (ss *SQLServer) SetVersion(version int, dirty bool) error {
|
||||
return &database.Error{OrigErr: err, Err: "transaction start failed"}
|
||||
}
|
||||
|
||||
query := `TRUNCATE TABLE ` + ss.getMigrationTable()
|
||||
query := `TRUNCATE TABLE "` + ss.config.MigrationsTable + `"`
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -284,10 +280,10 @@ func (ss *SQLServer) SetVersion(version int, dirty bool) error {
|
||||
if dirty {
|
||||
dirtyBit = 1
|
||||
}
|
||||
query = `INSERT INTO ` + ss.getMigrationTable() + ` (version, dirty) VALUES (@p1, @p2)`
|
||||
query = `INSERT INTO "` + ss.config.MigrationsTable + `" (version, dirty) VALUES (@p1, @p2)`
|
||||
if _, err := tx.Exec(query, version, dirtyBit); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = errors.Join(err, errRollback)
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
@@ -302,7 +298,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.getMigrationTable()
|
||||
query := `SELECT TOP 1 version, dirty FROM "` + ss.config.MigrationsTable + `"`
|
||||
err = ss.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
@@ -359,17 +355,21 @@ func (ss *SQLServer) ensureVersionTable() (err error) {
|
||||
|
||||
defer func() {
|
||||
if e := ss.Unlock(); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
query := `IF NOT EXISTS
|
||||
(SELECT *
|
||||
FROM sysobjects
|
||||
WHERE id = object_id(N'` + ss.getMigrationTable() + `')
|
||||
WHERE id = object_id(N'[dbo].[` + ss.config.MigrationsTable + `]')
|
||||
AND OBJECTPROPERTY(id, N'IsUserTable') = 1
|
||||
)
|
||||
CREATE TABLE ` + ss.getMigrationTable() + ` ( version BIGINT PRIMARY KEY NOT NULL, dirty BIT NOT NULL );`
|
||||
CREATE TABLE ` + ss.config.MigrationsTable + ` ( 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,10 +378,6 @@ 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 {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
sqldriver "database/sql/driver"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -24,16 +23,14 @@ const defaultPort = 1433
|
||||
const saPassword = "Root1234"
|
||||
|
||||
var (
|
||||
sqlServerOpts = dktest.Options{
|
||||
Env: map[string]string{"ACCEPT_EULA": "Y", "MSSQL_SA_PASSWORD": saPassword, "MSSQL_PID": "Express"},
|
||||
opts = dktest.Options{
|
||||
Env: map[string]string{"ACCEPT_EULA": "Y", "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/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},
|
||||
{ImageName: "mcr.microsoft.com/mssql/server:2017-latest", Options: opts},
|
||||
{ImageName: "mcr.microsoft.com/mssql/server:2019-latest", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -77,35 +74,8 @@ func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func SkipIfUnsupportedArch(t *testing.T, c dktest.ContainerInfo) {
|
||||
if strings.Contains(c.ImageName, "mssql") && !strings.HasPrefix(runtime.GOARCH, "amd") {
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -128,9 +98,8 @@ 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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -157,10 +126,9 @@ 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)
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -191,16 +159,14 @@ 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)
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := msConnectionString(ip, port)
|
||||
|
||||
p := &SQLServer{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
@@ -223,9 +189,8 @@ 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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -262,9 +227,8 @@ 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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -279,9 +243,8 @@ 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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -311,9 +274,8 @@ 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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package stub
|
||||
|
||||
import (
|
||||
"go.uber.org/atomic"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
)
|
||||
@@ -49,21 +50,21 @@ func (s *Stub) Close() error {
|
||||
}
|
||||
|
||||
func (s *Stub) Lock() error {
|
||||
if !s.isLocked.CompareAndSwap(false, true) {
|
||||
if !s.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) Unlock() error {
|
||||
if !s.isLocked.CompareAndSwap(true, false) {
|
||||
if !s.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) Run(migration io.Reader) error {
|
||||
m, err := io.ReadAll(migration)
|
||||
m, err := ioutil.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,6 +116,7 @@ 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.CompareAndSwap(o, n) {
|
||||
if !lock.CAS(o, n) {
|
||||
return casErr
|
||||
}
|
||||
if err := f(); err != nil {
|
||||
|
||||
@@ -2,7 +2,7 @@ package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"go.uber.org/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
lock bool
|
||||
lock *atomic.Bool
|
||||
from bool
|
||||
to bool
|
||||
expectLock bool
|
||||
@@ -63,7 +63,7 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Test positive CAS lock",
|
||||
lock: false,
|
||||
lock: atomic.NewBool(false),
|
||||
from: false,
|
||||
to: true,
|
||||
expectLock: true,
|
||||
@@ -72,7 +72,7 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Test negative CAS lock",
|
||||
lock: true,
|
||||
lock: atomic.NewBool(true),
|
||||
from: false,
|
||||
to: true,
|
||||
expectLock: true,
|
||||
@@ -81,7 +81,7 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Test negative with callback lock",
|
||||
lock: false,
|
||||
lock: atomic.NewBool(false),
|
||||
from: false,
|
||||
to: true,
|
||||
expectLock: false,
|
||||
@@ -92,15 +92,13 @@ func TestCasRestoreOnErr(t *testing.T) {
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var lock atomic.Bool
|
||||
lock.Store(tc.lock)
|
||||
if err := CasRestoreOnErr(&lock, tc.from, tc.to, casErr, func() error {
|
||||
if err := CasRestoreOnErr(tc.lock, tc.from, tc.to, casErr, func() error {
|
||||
return tc.fErr
|
||||
}); err != tc.expectError {
|
||||
t.Error("Incorrect error value returned")
|
||||
}
|
||||
|
||||
if lock.Load() != tc.expectLock {
|
||||
if tc.lock.Load() != tc.expectLock {
|
||||
t.Error("Incorrect state of lock")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# 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) |
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -1,5 +0,0 @@
|
||||
CREATE TABLE users (
|
||||
user_id integer unique,
|
||||
name varchar(40),
|
||||
email varchar(40)
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE users ADD COLUMN city varchar(100);
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS users_email_index;
|
||||
@@ -1,3 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS books;
|
||||
@@ -1,5 +0,0 @@
|
||||
CREATE TABLE books (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
author varchar(40)
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
@@ -1,5 +0,0 @@
|
||||
CREATE TABLE movies (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
director varchar(40)
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
-- 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.
|
||||
@@ -1 +0,0 @@
|
||||
-- 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.
|
||||
@@ -1 +0,0 @@
|
||||
-- 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.
|
||||
@@ -1 +0,0 @@
|
||||
-- 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.
|
||||
@@ -1,475 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
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"))
|
||||
})
|
||||
}
|
||||
+2
-28
@@ -1,13 +1,11 @@
|
||||
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
|
||||
@@ -16,30 +14,6 @@ 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,231 +1,69 @@
|
||||
module github.com/golang-migrate/migrate/v4
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
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/aws/aws-sdk-go v1.49.6
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
cloud.google.com/go/spanner v1.24.0
|
||||
cloud.google.com/go/storage v1.10.0
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.14
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3
|
||||
github.com/Microsoft/go-winio v0.5.0 // 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.0.2
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
||||
github.com/dhui/dktest v0.4.6
|
||||
github.com/docker/docker v28.3.3+incompatible
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.10.0
|
||||
github.com/dhui/dktest v0.3.7
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect
|
||||
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/google/go-github/v39 v39.2.0
|
||||
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/gofrs/uuid v4.0.0+incompatible // indirect
|
||||
github.com/google/go-github/v35 v35.2.0
|
||||
github.com/gorilla/mux v1.7.4 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
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/ktrysmt/go-bitbucket v0.6.4
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/markbates/pkger v0.15.1
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/microsoft/go-mssqldb v1.0.0
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
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/snowflakedb/gosnowflake v1.19.1
|
||||
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.29.6
|
||||
)
|
||||
|
||||
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/BurntSushi/toml v1.4.0 // 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/apache/arrow-go/v18 v18.4.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.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/dustin/go-humanize v1.0.1 // 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/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/hanzo-ds/native v0.71.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // 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/ncruces/go-strftime v0.1.9 // 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
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // 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.2 // 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/thrift v0.22.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.35 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0 // indirect
|
||||
github.com/aws/smithy-go v1.27.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.2.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/gabriel-vasile/mimetype v1.4.7 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // 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 v1.0.0 // indirect
|
||||
github.com/google/flatbuffers v25.2.10+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/klauspost/asmfmt v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // 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-20230129092748-24d4a6f8daec // 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
|
||||
github.com/snowflakedb/gosnowflake v1.6.3
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xanzy/go-gitlab v0.15.0
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // 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-20240903120638-7835f813f4da // 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
|
||||
go.mongodb.org/mongo-driver v1.7.0
|
||||
go.uber.org/atomic v1.6.0
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/net v0.0.0-20211013171255-e13a2654a71e // indirect
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect
|
||||
golang.org/x/tools v0.1.5
|
||||
google.golang.org/api v0.51.0
|
||||
google.golang.org/genproto v0.0.0-20211013025323-ce878158c4d4
|
||||
google.golang.org/grpc v1.41.0 // indirect
|
||||
modernc.org/b v1.0.0 // indirect
|
||||
modernc.org/db v1.0.0 // indirect
|
||||
modernc.org/file v1.0.0 // indirect
|
||||
modernc.org/fileutil v1.3.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.41.0 // indirect
|
||||
modernc.org/lldb v1.0.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/ql v1.0.0
|
||||
modernc.org/sortutil v1.1.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
modernc.org/sqlite v1.10.6
|
||||
modernc.org/zappy v1.0.0 // indirect
|
||||
)
|
||||
|
||||
go 1.16
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build aws_s3
|
||||
// +build aws_s3
|
||||
|
||||
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