diff --git a/api/migrations/migrations.go b/api/migrations/migrations.go index b87ad1a..a2748b1 100644 --- a/api/migrations/migrations.go +++ b/api/migrations/migrations.go @@ -1,6 +1,6 @@ package migrations -import "github.com/contextwtf/lanyard/migrate" +import "github.com/contextwtf/migrate" var Migrations = []migrate.Migration{ { diff --git a/cmd/api/main.go b/cmd/api/main.go index f77d70f..97147a5 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -13,7 +13,7 @@ import ( "github.com/contextwtf/lanyard/api" "github.com/contextwtf/lanyard/api/migrations" "github.com/contextwtf/lanyard/api/tracing" - "github.com/contextwtf/lanyard/migrate" + "github.com/contextwtf/migrate" "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" _ "github.com/jackc/pgx/v4/stdlib" diff --git a/cmd/dumpschema/main.go b/cmd/dumpschema/main.go index 647e035..6aadd07 100644 --- a/cmd/dumpschema/main.go +++ b/cmd/dumpschema/main.go @@ -11,7 +11,7 @@ import ( "strings" "github.com/contextwtf/lanyard/api/migrations" - "github.com/contextwtf/lanyard/migrate" + "github.com/contextwtf/migrate" _ "github.com/lib/pq" ) diff --git a/go.mod b/go.mod index fd26a49..a713ef1 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/contextwtf/lanyard go 1.19 require ( - blake.io/pqx v0.0.0-20220606000416-b72a6fb40461 + github.com/contextwtf/migrate v0.0.1 github.com/ethereum/go-ethereum v1.10.21 github.com/jackc/pgx/v4 v4.16.1 - github.com/lib/pq v1.10.5 + github.com/lib/pq v1.10.9 github.com/opentracing/opentracing-go v1.2.0 github.com/rs/cors v1.8.2 - github.com/rs/zerolog v1.27.0 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 - golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df + github.com/rs/zerolog v1.29.1 + golang.org/x/sync v0.3.0 + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 gopkg.in/DataDog/dd-trace-go.v1 v1.40.1 ) @@ -42,13 +42,11 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/philhofer/fwd v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rs/xid v1.3.0 // indirect + github.com/rs/xid v1.4.0 // indirect github.com/tinylib/msgp v1.1.2 // indirect - github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect google.golang.org/protobuf v1.27.1 // indirect - kr.dev/errorfmt v0.1.1 // indirect ) diff --git a/go.sum b/go.sum index 9b15292..a2a3e91 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,4 @@ -blake.io/pqx v0.0.0-20220606000416-b72a6fb40461 h1:Ai3MnXZ8a24nLSXRAS+b333Cb1pp23UkpbCyIdJDxYg= -blake.io/pqx v0.0.0-20220606000416-b72a6fb40461/go.mod h1:hcG2tklM4QIxdfL+laWGAmtIDVgPKkWtxGG/t7umOfA= +blake.io/pqx v0.2.1 h1:Qz3yyNmPIFCyRS9HLnxtQNIL809ZC13aWvpeiXU3oS8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= @@ -106,9 +105,11 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/confluentinc/confluent-kafka-go v1.4.0/go.mod h1:u2zNLny2xq+5rWeTQjFHbDzzNuba4P1vo31r9r4uAdg= +github.com/contextwtf/migrate v0.0.1 h1:l8XmE2prDIFuaGspiOVZOU/NmiYXsEcGyh2GWA5nuRE= +github.com/contextwtf/migrate v0.0.1/go.mod h1:OuoM29wVz9zmQNma79HRFcZcyENNMSj+lCYkA/q2dvQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -438,8 +439,8 @@ github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= -github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -524,7 +525,6 @@ github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0je github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -547,16 +547,15 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= -github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= @@ -630,7 +629,6 @@ github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -699,7 +697,6 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200901203048-c4f52b2c50aa/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= -golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 h1:rxKZ2gOnYxjfmakvUUqh9Gyb6KXfrj7JWTxORTYqb0E= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -775,8 +772,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -922,8 +919,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -1052,9 +1049,7 @@ k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -kr.dev/diff v0.2.0 h1:cbU8pftbTxST8Is3TZwXW2PuaPXDgaibnJfuhG57LCM= kr.dev/errorfmt v0.1.1 h1:0YA5N2yV0xKxJ4eD5cX2S9wEnJHDHOZzerKbrZqtRrQ= -kr.dev/errorfmt v0.1.1/go.mod h1:X5EQZa3qf6c/l1DMjhflAbKGAGvlP6/ByWnaOpfbJME= mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/migrate/migrate.go b/migrate/migrate.go deleted file mode 100644 index c299c7c..0000000 --- a/migrate/migrate.go +++ /dev/null @@ -1,275 +0,0 @@ -package migrate - -import ( - "context" - "crypto/sha256" - "database/sql" - "encoding/hex" - "errors" - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/rs/zerolog/log" - "golang.org/x/xerrors" -) - -// Migration describes a PostgreSQL database migration. -// At least one of SQL and Hash must be set. -// If both are set, Hash must be the SHA-256 of SQL. -type Migration struct { - Name string - SQL string // unavailable for applied migrations - Hash string // internally computed if unset - RequiredSHA string - OutsideTx bool -} - -func (m *Migration) hash() string { - if m.SQL == "" { - return m.Hash - } - h := sha256.Sum256([]byte(withoutWhiteSpace(m.SQL))) - return hex.EncodeToString(h[:]) -} - -// withoutWhiteSpace removes all whitespace, interior and exterior so that -// migration specifications can be tweaked for readability without changing the -// semantics. -func withoutWhiteSpace(s string) string { - return strings.Join(strings.Fields(s), " ") -} - -func (m Migration) String() string { - return fmt.Sprintf("%s - %s", m.Name, m.hash()[:5]) -} - -// Error records an error -// and the migration that caused it. -// Index is the index of Mig in -// the given list of migrations, -// or the index where it would have -// been if it's not in the list. -type Error struct { - Mig Migration - Index int - Err error -} - -func (e *Error) Error() string { - return e.Mig.String() + " at " + strconv.Itoa(e.Index) + ": " + e.Err.Error() -} - -// Run runs all unapplied migrations in m. -func Run(ctx context.Context, db beginner, m []Migration) error { - for { - // keep going until there are no more to run (or an error) - ran, err := run1(ctx, db, m) - if !ran { - return err - } - } -} - -type beginner interface { - execer - BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error) -} -type execer interface { - ExecContext(context.Context, string, ...any) (sql.Result, error) - QueryContext(context.Context, string, ...any) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...any) *sql.Row -} - -// run1 runs a single unapplied migration. -// It returns whether it ran successfully -// along with any error. -// -// Note that a return value of false, nil -// means that there were no unapplied migrations to run. -func run1(ctx context.Context, db beginner, migrations []Migration) (ran bool, err error) { - // Begin a SQL transaction for all changes to the migrations - // table. We use it to acquire an exclusive lock on the - // table to ensure we're the only process migrating this - // database. - tx, err := db.BeginTx(ctx, nil) - if err != nil { - return false, xerrors.Errorf("begin tx: %w", err) - } - defer tx.Rollback() - - // Acquire a tx-level lock. 4 is an arbitrary bigint that should - // uniquely identify this lock. - _, err = tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(4)`) - if err != nil { - return false, xerrors.Errorf("advisory lock: %w", err) - } - - // Create the migrations table if not yet created. - const q = ` - CREATE SEQUENCE IF NOT EXISTS migration_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - CREATE TABLE IF NOT EXISTS migrations ( - filename text NOT NULL, - hash text NOT NULL, - applied_at timestamp with time zone DEFAULT now() NOT NULL, - index int DEFAULT nextval('migration_seq') NOT NULL, - PRIMARY KEY(filename) - ); - ` - _, err = tx.ExecContext(ctx, q) - if err != nil { - return false, xerrors.Errorf("creating migration table: %w", err) - } - - // Query migrations for the set of unapplied transactions. - unapplied, err := FilterApplied(tx, migrations) - if err != nil { - return false, err - } - - if len(unapplied) == 0 { - return false, nil // all up to date! - } - - m := unapplied[0] - - // Some migrations contain SQL that PostgreSQL does not support - // within a transaction, so we flag those and run them outside - // of the migration transaction. - // - // This means it is possible that the migration can be applied - // successfully, but then we fail to insert a row in the - // migrations table. We'll need to keep an eye on any error - // messages from migratedb, to avoid attempting to apply - // the same migration again. - if m.OutsideTx { - _, err = db.ExecContext(ctx, m.SQL) - } else { - _, err = tx.ExecContext(ctx, m.SQL) - } - if err != nil { - return false, xerrors.Errorf("migration %s: %w", m.Name, err) - } - - err = insertAppliedMigration(ctx, tx, m) - if err != nil { - return false, err - } - err = tx.Commit() - if err == nil { - log.Ctx(ctx).Info().Str("migration", m.Name).Msg("success") - } else { - log.Ctx(ctx).Error().Err(err).Str("migration", m.Name).Msg("failed") - } - return err == nil, err -} - -// GetApplied returns the list of currently-applied migrations. -func GetApplied(ctx context.Context, db execer) ([]Migration, error) { - const q1 = ` - SELECT count(*) FROM pg_tables - WHERE schemaname=current_schema() AND tablename='migrations' - ` - var n int - err := db.QueryRowContext(ctx, q1).Scan(&n) - if err != nil { - return nil, xerrors.Errorf("checking for migrations table: %w", err) - } - if n == 0 { - return nil, nil - } - - const q2 = ` - SELECT filename, hash - FROM migrations - ORDER BY index - ` - var a []Migration - rows, err := db.QueryContext(ctx, q2) - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - m := Migration{} - err := rows.Scan(&m.Name, &m.Hash) - if err != nil { - return nil, err - } - a = append(a, m) - } - err = rows.Err() - if err != nil { - return nil, err - } - return a, nil -} - -// FilterApplied returns the slice of ms containing -// all migrations in ms that haven't yet been applied. -func FilterApplied(db execer, ms []Migration) ([]Migration, error) { - applied, err := GetApplied(context.TODO(), db) - if err != nil { - return nil, err - } - for i, app := range applied { - if i >= len(ms) { - return nil, &Error{Mig: app, Index: i, Err: errors.New("applied but not requested")} - } - m := ms[i] - if app.hash() != m.hash() { - return nil, &Error{Mig: app, Index: i, Err: errors.New("hash mismatch")} - } - } - return ms[len(applied):], nil -} - -func insertAppliedMigration(ctx context.Context, db *sql.Tx, m Migration) error { - const q = ` - INSERT INTO migrations (filename, hash, applied_at) - VALUES($1, $2, NOW()) - ` - _, err := db.ExecContext(ctx, q, m.Name, m.hash()) - if err != nil { - return xerrors.Errorf("recording applied migration: %w", err) - } - return nil -} - -var validNameRegex = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\.\d\.[a-z][a-z0-9_-]+\.sql$`) - -// Validity returns an error if the provided list of migrations -// isn't valid. The required properties are: -// names are well formed -// names are in order -// keys (YYYY-MM-DD.N) are not duplicated -func Validity(migrations []Migration) error { - list := make([]Migration, len(migrations)) - copy(list, migrations) - - for i, m := range list { - if !validNameRegex.MatchString(m.Name) { - return fmt.Errorf("bad name: %s", m.Name) - } - if i > 0 && list[i-1].Name >= m.Name { - return errors.New("out of order: " + m.Name) - } - } - - // Fail if we have more than one of any index - // on the same day. YYYY-MM-DD.N - a := make([]string, len(list)) - for i, m := range list { - a[i] = m.Name[:12] - if i > 0 && a[i-1] == a[i] { - return fmt.Errorf("duplicate indexes %s %s", list[i-1].Name, m.Name) - } - } - return nil -} diff --git a/migrate/migrate_test.go b/migrate/migrate_test.go deleted file mode 100644 index c5f63b3..0000000 --- a/migrate/migrate_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package migrate - -import ( - "context" - "errors" - "reflect" - "testing" - - "blake.io/pqx/pqxtest" - _ "github.com/lib/pq" - "golang.org/x/sync/errgroup" -) - -func TestMain(m *testing.M) { - pqxtest.TestMain(m) -} - -func TestMigrateConcurrent(t *testing.T) { - db := pqxtest.CreateDB(t, ``) - - migrations := []Migration{ - { - Name: "2000-01-01.0.migrate.select1.sql", - SQL: "CREATE TABLE foo (id text NOT NULL PRIMARY KEY);", - }, - } - - var group errgroup.Group - for i := 0; i < 5; i++ { - group.Go(func() error { - return Run(context.Background(), db, migrations) - }) - } - err := group.Wait() - if err != nil { - t.Error(err) - } - - // Check that there are no remaining, unapplied migrations. - unapplied, err := FilterApplied(db, migrations) - if err != nil { - t.Fatal(err) - } - if len(unapplied) > 0 { - t.Errorf("len(FilterApplied(migrations)) = %d, want 0", len(unapplied)) - } -} - -func TestFilterApplied(t *testing.T) { - const migrationTable = ` - CREATE SEQUENCE IF NOT EXISTS migration_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - CREATE TABLE IF NOT EXISTS migrations ( - filename text NOT NULL, - hash text NOT NULL, - applied_at timestamp with time zone DEFAULT now() NOT NULL, - index int DEFAULT nextval('migration_seq') NOT NULL, - PRIMARY KEY(filename) - ); - ` - const oneMigration = ` - INSERT INTO migrations (filename, hash, applied_at) - VALUES ('x', 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', '2016-02-09T23:21:55 US/Pacific'); - ` - - cases := []struct { - initSQL string - migs []Migration - want []Migration - }{ - { - initSQL: `SELECT 1;`, - migs: nil, - want: nil, - }, - { - initSQL: migrationTable, - migs: nil, - want: nil, - }, - { - initSQL: `SELECT 1;`, - migs: []Migration{{Name: "x", SQL: "a"}}, - want: []Migration{{Name: "x", SQL: "a"}}, - }, - { - initSQL: migrationTable + oneMigration, - migs: []Migration{{Name: "x", SQL: "a"}}, - want: []Migration{}, - }, - { - initSQL: `SELECT 1;`, - migs: []Migration{ - {Name: "x", SQL: "a"}, - {Name: "y", SQL: "b"}, - }, - want: []Migration{ - {Name: "x", SQL: "a"}, - {Name: "y", SQL: "b"}, - }, - }, - { - initSQL: migrationTable + oneMigration, - migs: []Migration{ - {Name: "x", SQL: "a"}, - {Name: "y", SQL: "b"}, - }, - want: []Migration{{Name: "y", SQL: "b"}}, - }, - { - initSQL: migrationTable + oneMigration, - migs: []Migration{{Name: "x1", SQL: "a"}}, - want: []Migration{}, - }, - } - - for _, test := range cases { - db := pqxtest.CreateDB(t, ``) - _, err := db.ExecContext(context.TODO(), test.initSQL) - if err != nil { - t.Error(err) - continue - } - got, err := FilterApplied(db, test.migs) - if err != nil { - t.Error(err) - continue - } - if !reflect.DeepEqual(got, test.want) { - t.Errorf("FilterApplied() = %#v, want %#v", got, test.want) - } - } -} - -func TestFilterAppliedError(t *testing.T) { - const migrationTable = ` - CREATE SEQUENCE IF NOT EXISTS migration_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - CREATE TABLE IF NOT EXISTS migrations ( - filename text NOT NULL, - hash text NOT NULL, - applied_at timestamp with time zone DEFAULT now() NOT NULL, - index int DEFAULT nextval('migration_seq') NOT NULL, - PRIMARY KEY(filename) - ); - ` - const oneMigration = ` - INSERT INTO migrations (filename, hash, applied_at) - VALUES ('x', 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', '2016-02-09T23:21:55 US/Pacific'); - ` - - cases := []struct { - initSQL string - migs []Migration - want *Error - }{ - { - initSQL: migrationTable + oneMigration, - migs: nil, - want: &Error{ - Mig: Migration{ - Name: "x", - Hash: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", - }, - Index: 0, - Err: errors.New("applied but not requested"), - }, - }, - { - initSQL: migrationTable + oneMigration, - migs: []Migration{{Name: "x", SQL: "a1"}}, - want: &Error{ - Mig: Migration{ - Name: "x", - Hash: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", - }, - Index: 0, - Err: errors.New("hash mismatch"), - }, - }, - } - - for _, test := range cases { - db := pqxtest.CreateDB(t, ``) - _, err := db.ExecContext(context.TODO(), test.initSQL) - if err != nil { - t.Error(err) - continue - } - _, got := FilterApplied(db, test.migs) - if !reflect.DeepEqual(got, test.want) { - t.Errorf("FilterApplied(db, %v) err = %v, want %v", test.migs, got, test.want) - t.Logf("got: %#v", got) - t.Logf("want: %#v", test.want) - } - } -}