* upgrade to use testfixtures v3 * simplify logic * make vendor * update per @lunny * Update templates/repo/empty.tmpl * Update templates/repo/empty.tmpl Co-authored-by: Lauris BH <lauris@nix.lv>tags/v1.13.0-rc1
"github.com/go-git/go-git/v5/plumbing" | "github.com/go-git/go-git/v5/plumbing" | ||||
context2 "github.com/gorilla/context" | context2 "github.com/gorilla/context" | ||||
"github.com/unknwon/com" | "github.com/unknwon/com" | ||||
"gopkg.in/testfixtures.v2" | |||||
"xorm.io/xorm" | "xorm.io/xorm" | ||||
) | ) | ||||
setting.Database.LogSQL = true | setting.Database.LogSQL = true | ||||
//x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared") | //x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared") | ||||
var helper testfixtures.Helper = &testfixtures.SQLite{} | |||||
models.NewEngine(context.Background(), func(_ *xorm.Engine) error { | models.NewEngine(context.Background(), func(_ *xorm.Engine) error { | ||||
return nil | return nil | ||||
}) | }) | ||||
models.HasEngine = true | models.HasEngine = true | ||||
//x.ShowSQL(true) | //x.ShowSQL(true) | ||||
err = models.InitFixtures( | err = models.InitFixtures( | ||||
helper, | |||||
path.Join(curDir, "models/fixtures/"), | path.Join(curDir, "models/fixtures/"), | ||||
) | ) | ||||
if err != nil { | if err != nil { |
github.com/go-redis/redis v6.15.2+incompatible | github.com/go-redis/redis v6.15.2+incompatible | ||||
github.com/go-sql-driver/mysql v1.4.1 | github.com/go-sql-driver/mysql v1.4.1 | ||||
github.com/go-swagger/go-swagger v0.21.0 | github.com/go-swagger/go-swagger v0.21.0 | ||||
github.com/go-testfixtures/testfixtures/v3 v3.2.0 | |||||
github.com/gobwas/glob v0.2.3 | github.com/gobwas/glob v0.2.3 | ||||
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 | github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 | ||||
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 | github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 | ||||
github.com/issue9/identicon v1.0.1 | github.com/issue9/identicon v1.0.1 | ||||
github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d | github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d | ||||
github.com/jmhodges/levigo v1.0.0 // indirect | github.com/jmhodges/levigo v1.0.0 // indirect | ||||
github.com/joho/godotenv v1.3.0 // indirect | |||||
github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 | github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 | ||||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 | github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 | ||||
github.com/klauspost/compress v1.10.2 | github.com/klauspost/compress v1.10.2 | ||||
github.com/mailru/easyjson v0.7.0 // indirect | github.com/mailru/easyjson v0.7.0 // indirect | ||||
github.com/markbates/goth v1.61.2 | github.com/markbates/goth v1.61.2 | ||||
github.com/mattn/go-isatty v0.0.11 | github.com/mattn/go-isatty v0.0.11 | ||||
github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d // indirect | |||||
github.com/mattn/go-sqlite3 v1.11.0 | |||||
github.com/mattn/go-sqlite3 v2.0.2+incompatible | |||||
github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 | github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 | ||||
github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81 | github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81 | ||||
github.com/mgechev/revive v1.0.2 | github.com/mgechev/revive v1.0.2 | ||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df | ||||
gopkg.in/ini.v1 v1.52.0 | gopkg.in/ini.v1 v1.52.0 | ||||
gopkg.in/ldap.v3 v3.0.2 | gopkg.in/ldap.v3 v3.0.2 | ||||
gopkg.in/testfixtures.v2 v2.5.0 | |||||
gopkg.in/yaml.v2 v2.2.8 | |||||
gopkg.in/yaml.v2 v2.3.0 | |||||
mvdan.cc/xurls/v2 v2.1.0 | mvdan.cc/xurls/v2 v2.1.0 | ||||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 | strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 | ||||
xorm.io/builder v0.3.7 | xorm.io/builder v0.3.7 |
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= | github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= | ||||
github.com/denisenkom/go-mssqldb v0.0.0-20190924004331-208c0a498538 h1:bpWCJ5MddHsv4Xtl3azkK89mZzd/vvut32mvAnKbyUA= | github.com/denisenkom/go-mssqldb v0.0.0-20190924004331-208c0a498538 h1:bpWCJ5MddHsv4Xtl3azkK89mZzd/vvut32mvAnKbyUA= | ||||
github.com/denisenkom/go-mssqldb v0.0.0-20190924004331-208c0a498538/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | github.com/denisenkom/go-mssqldb v0.0.0-20190924004331-208c0a498538/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | ||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | |||||
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg= | github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg= | ||||
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | ||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | ||||
github.com/go-swagger/go-swagger v0.21.0/go.mod h1:tDb8PdDVFcaE8EPXkMOsuxpL3UEPiwu1UDZar9Z/1RY= | github.com/go-swagger/go-swagger v0.21.0/go.mod h1:tDb8PdDVFcaE8EPXkMOsuxpL3UEPiwu1UDZar9Z/1RY= | ||||
github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0= | github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0= | ||||
github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= | github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= | ||||
github.com/go-testfixtures/testfixtures/v3 v3.2.0 h1:FGAW3z5UzmrZGjR/dZp1u3Tbld0SDmirLO4RrR5++7Q= | |||||
github.com/go-testfixtures/testfixtures/v3 v3.2.0/go.mod h1:RZctY24ixituGC73XlAV1gkCwYMVwiSwPm26MNlQIhE= | |||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= | github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= | ||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= | github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= | ||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= | ||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= | ||||
github.com/lib/pq v1.2.0/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.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= | github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= | ||||
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | ||||
github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:uNwtsDp7ci48vBTTxDuwcoTXz4lwtDTe7TjCQ0noaWY= | github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:uNwtsDp7ci48vBTTxDuwcoTXz4lwtDTe7TjCQ0noaWY= | ||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | ||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= | ||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= | ||||
github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d h1:m+dSK37rFf2fqppZhg15yI2IwC9BtucBiRwSDm9VL8g= | |||||
github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8= | |||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= | ||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= | ||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= | github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= | ||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U= | |||||
github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | |||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= | ||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | ||||
github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk= | github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk= | ||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= | ||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= | ||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= | ||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | |||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | |||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= | ||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= | ||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= | ||||
gopkg.in/ldap.v3 v3.0.2/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw= | gopkg.in/ldap.v3 v3.0.2/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw= | ||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= | ||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= | ||||
gopkg.in/testfixtures.v2 v2.5.0 h1:N08B7l2GzFQenyYbzqthDnKAA+cmb17iAZhhFxr7JHw= | |||||
gopkg.in/testfixtures.v2 v2.5.0/go.mod h1:vyAq+MYCgNpR29qitQdLZhdbLFf4mR/2MFJRFoQZZ2M= | |||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= | ||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= | ||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= | ||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= | |||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= |
"github.com/PuerkitoBio/goquery" | "github.com/PuerkitoBio/goquery" | ||||
"github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
"github.com/unknwon/com" | "github.com/unknwon/com" | ||||
"gopkg.in/testfixtures.v2" | |||||
) | ) | ||||
var mac *macaron.Macaron | var mac *macaron.Macaron | ||||
} | } | ||||
} | } | ||||
var helper testfixtures.Helper | |||||
if setting.Database.UseMySQL { | |||||
helper = &testfixtures.MySQL{} | |||||
} else if setting.Database.UsePostgreSQL { | |||||
helper = &testfixtures.PostgreSQL{} | |||||
} else if setting.Database.UseSQLite3 { | |||||
helper = &testfixtures.SQLite{} | |||||
} else if setting.Database.UseMSSQL { | |||||
helper = &testfixtures.SQLServer{} | |||||
} else { | |||||
fmt.Println("Unsupported RDBMS for integration tests") | |||||
os.Exit(1) | |||||
} | |||||
err := models.InitFixtures( | err := models.InitFixtures( | ||||
helper, | |||||
path.Join(filepath.Dir(setting.AppPath), "models/fixtures/"), | path.Join(filepath.Dir(setting.AppPath), "models/fixtures/"), | ||||
) | ) | ||||
if err != nil { | if err != nil { |
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"os" | |||||
"time" | "time" | ||||
"gopkg.in/testfixtures.v2" | |||||
"github.com/go-testfixtures/testfixtures/v3" | |||||
"xorm.io/xorm/schemas" | "xorm.io/xorm/schemas" | ||||
) | ) | ||||
var fixtures *testfixtures.Context | |||||
var fixtures *testfixtures.Loader | |||||
// InitFixtures initialize test fixtures for a test database | // InitFixtures initialize test fixtures for a test database | ||||
func InitFixtures(helper testfixtures.Helper, dir string) (err error) { | |||||
testfixtures.SkipDatabaseNameCheck(true) | |||||
fixtures, err = testfixtures.NewFolder(x.DB().DB, helper, dir) | |||||
func InitFixtures(dir string) (err error) { | |||||
testfiles := testfixtures.Directory(dir) | |||||
dialect := "unknown" | |||||
switch x.Dialect().URI().DBType { | |||||
case schemas.POSTGRES: | |||||
dialect = "postgres" | |||||
case schemas.MYSQL: | |||||
dialect = "mysql" | |||||
case schemas.MSSQL: | |||||
dialect = "mssql" | |||||
case schemas.SQLITE: | |||||
dialect = "sqlite3" | |||||
default: | |||||
fmt.Println("Unsupported RDBMS for integration tests") | |||||
os.Exit(1) | |||||
} | |||||
loaderOptions := []func(loader *testfixtures.Loader) error{ | |||||
testfixtures.Database(x.DB().DB), | |||||
testfixtures.Dialect(dialect), | |||||
testfixtures.DangerousSkipTestDatabaseCheck(), | |||||
testfiles, | |||||
} | |||||
if x.Dialect().URI().DBType == schemas.POSTGRES { | |||||
loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) | |||||
} | |||||
fixtures, err = testfixtures.New(loaderOptions...) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return err | return err | ||||
} | } | ||||
"github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
"github.com/unknwon/com" | "github.com/unknwon/com" | ||||
"gopkg.in/testfixtures.v2" | |||||
"xorm.io/xorm" | "xorm.io/xorm" | ||||
"xorm.io/xorm/names" | "xorm.io/xorm/names" | ||||
) | ) | ||||
x.ShowSQL(true) | x.ShowSQL(true) | ||||
} | } | ||||
return InitFixtures(&testfixtures.SQLite{}, fixturesDir) | |||||
return InitFixtures(fixturesDir) | |||||
} | } | ||||
func removeAllWithRetry(dir string) error { | func removeAllWithRetry(dir string) error { |
*.test | *.test | ||||
*.prof | *.prof | ||||
# SQLite databases | |||||
*.sqlite3 | *.sqlite3 | ||||
.env | .env | ||||
/testfixtures | |||||
/dist |
build: | |||||
binary: testfixtures | |||||
main: ./cmd/testfixtures | |||||
goos: | |||||
- windows | |||||
- darwin | |||||
- linux | |||||
goarch: | |||||
- 386 | |||||
- amd64 | |||||
ignore: | |||||
- goos: darwin | |||||
goarch: 386 | |||||
flags: | |||||
- -tags=sqlite | |||||
archives: | |||||
- name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}" | |||||
format_overrides: | |||||
- goos: windows | |||||
format: zip | |||||
release: | |||||
draft: true | |||||
snapshot: | |||||
name_template: "{{.Tag}}" | |||||
checksum: | |||||
name_template: "testfixtures_checksums.txt" | |||||
nfpms: | |||||
- vendor: testfixtures | |||||
homepage: https://github.com/go-testfixtures/testfixtures | |||||
maintainer: Andrey Nering <andrey.nering@gmail.com> | |||||
description: Ruby on Rails like test fixtures for Go. | |||||
license: MIT | |||||
formats: | |||||
- deb | |||||
- rpm | |||||
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" |
MYSQL_CONN_STRING="root:@/testfixtures_test?multiStatements=true" | MYSQL_CONN_STRING="root:@/testfixtures_test?multiStatements=true" | ||||
SQLITE_CONN_STRING="testdb.sqlite3" | SQLITE_CONN_STRING="testdb.sqlite3" | ||||
SQLSERVER_CONN_STRING="server=localhost\SQLExpress;database=testfixtures_test;user id=sa;password=sqlserver;encrypt=disable" | SQLSERVER_CONN_STRING="server=localhost\SQLExpress;database=testfixtures_test;user id=sa;password=sqlserver;encrypt=disable" | ||||
ORACLE_CONN_STRING="testfixtures/testfixtures@localhost/XE" |
# Changelog | |||||
## v3.2.0 - 2020-05-10 | |||||
- Add support for loading multiple files and directories | |||||
([#65](https://github.com/go-testfixtures/testfixtures/pull/65)). | |||||
## v3.1.2 - 2020-04-26 | |||||
- Dump: Fix column order in generated YAML files | |||||
([#62](https://github.com/go-testfixtures/testfixtures/pull/62)). | |||||
## v3.1.1 - 2020-01-11 | |||||
- testfixtures now work with both `mssql` and `sqlserver` drivers. | |||||
Note that [the `mssql` one is deprecated](https://github.com/denisenkom/go-mssqldb#deprecated), | |||||
though. So try to migrate to `sqlserver` once possible. | |||||
## v3.1.0 - 2020-01-09 | |||||
- Using `sqlserver` driver instead of the deprecated `mssql` | |||||
([#58](https://github.com/go-testfixtures/testfixtures/pull/58)). | |||||
## v3.0.0 - 2019-12-26 | |||||
### Breaking changes | |||||
- The import path changed from `gopkg.in/testfixtures.v2` to | |||||
`github.com/go-testfixtures/testfixtures/v3`. | |||||
- This package no longer support Oracle databases. This decision was | |||||
taken because too few people actually used this package with Oracle and it | |||||
was the most difficult to test (we didn't run on CI due the lack of an | |||||
official Docker image, etc). | |||||
- The public API was totally rewritten to be more flexible and ideomatic. | |||||
It now uses functional options. It differs from v2, but should be easy | |||||
enough to upgrade. | |||||
- Some deprecated APIs from v2 were removed as well. | |||||
- This now requires Go >= 1.13. | |||||
### New features | |||||
- We now have a CLI so you can easily use testfixtures to load a sample | |||||
database from fixtures if you want. | |||||
- Templating via [text/template](https://golang.org/pkg/text/template/) | |||||
is now available. This allows some fancier use cases like generating data | |||||
or specific columns dynamically. | |||||
- It's now possible to choose which time zone to use when parsing timestamps | |||||
from fixtures. The default is the same as before, whatever is set on | |||||
`time.Local`. | |||||
- Errors now use the new `%w` verb only available on Go >= 1.13. | |||||
### MISC | |||||
- Travis and AppVeyor are gone. We're using GitHub Actions exclusively now. | |||||
The whole suite is ran inside Docker (with help of Docker Compose), so it's | |||||
easy to run tests locally as well. | |||||
Check the new README for some examples! | |||||
## v2.6.0 - 2019-10-24 | |||||
- Add support for TimescaleDB | |||||
([#53](https://github.com/go-testfixtures/testfixtures/pull/53)). | |||||
## v2.5.3 - 2018-12-15 | |||||
- Fixes related to use of foreign key pragmas on MySQL (#43). | |||||
## v2.5.2 - 2018-11-25 | |||||
- This library now supports [Go Modules](https://github.com/golang/go/wiki/Modules); | |||||
- Also allow `.yaml` (as an alternative to `.yml`) as the file extension (#42). | |||||
## v2.5.1 - 2018-11-04 | |||||
- Allowing disabling reset of PostgreSQL sequences (#38). | |||||
## v2.5.0 - 2018-09-07 | |||||
- Add public function DetectTestDatabase (#35, #36). | |||||
## v2.4.5 - 2018-07-07 | |||||
- Fix for MySQL/MariaDB: ignoring views on operations that should be run only on tables (#33). | |||||
## v2.4.4 - 2018-07-02 | |||||
- Fix for multiple schemas on Microsoft SQL Server (#29 and #30); | |||||
- Configuring AppVeyor CI to also test for Microsoft SQL Server. | |||||
--- | |||||
Sorry, we don't have changelog for older releases 😢. |
FROM golang:1.14-alpine | |||||
RUN apk update | |||||
RUN apk add alpine-sdk | |||||
WORKDIR /testfixtures | |||||
COPY . . | |||||
RUN go mod download |
# testfixtures | |||||
[![GoDoc](https://godoc.org/github.com/go-testfixtures/testfixtures?status.svg)][doc] | |||||
> ***Warning***: this package will wipe the database data before loading the | |||||
fixtures! It is supposed to be used on a test database. Please, double check | |||||
if you are running it against the correct database. | |||||
> **TIP**: There are options not described in this README page. It's | |||||
> recommended that you also check [the documentation][doc]. | |||||
Writing tests is hard, even more when you have to deal with an SQL database. | |||||
This package aims to make writing functional tests for web apps written in | |||||
Go easier. | |||||
Basically this package mimics the ["Ruby on Rails' way"][railstests] of writing tests | |||||
for database applications, where sample data is kept in fixtures files. Before | |||||
the execution of every test, the test database is cleaned and the fixture data | |||||
is loaded into the database. | |||||
The idea is running tests against a real database, instead of relying in mocks, | |||||
which is boring to setup and may lead to production bugs not being caught in | |||||
the tests. | |||||
## Installation | |||||
First, import it like this: | |||||
```go | |||||
import ( | |||||
"github.com/go-testfixtures/testfixtures/v3" | |||||
) | |||||
``` | |||||
## Usage | |||||
Create a folder for the fixture files. Each file should contain data for a | |||||
single table and have the name `<table_name>.yml`: | |||||
``` | |||||
myapp/ | |||||
myapp.go | |||||
myapp_test.go | |||||
... | |||||
fixtures/ | |||||
posts.yml | |||||
comments.yml | |||||
tags.yml | |||||
posts_tags.yml | |||||
... | |||||
``` | |||||
The file would look like this (it can have as many record you want): | |||||
```yml | |||||
# comments.yml | |||||
- id: 1 | |||||
post_id: 1 | |||||
content: A comment... | |||||
author_name: John Doe | |||||
author_email: john@doe.com | |||||
created_at: 2020-12-31 23:59:59 | |||||
updated_at: 2020-12-31 23:59:59 | |||||
- id: 2 | |||||
post_id: 2 | |||||
content: Another comment... | |||||
author_name: John Doe | |||||
author_email: john@doe.com | |||||
created_at: 2020-12-31 23:59:59 | |||||
updated_at: 2020-12-31 23:59:59 | |||||
# ... | |||||
``` | |||||
An YAML object or array will be converted to JSON. It will be stored on a native | |||||
JSON type like JSONB on PostgreSQL or as a TEXT or VARCHAR column on other | |||||
databases. | |||||
```yml | |||||
- id: 1 | |||||
post_attributes: | |||||
author: John Due | |||||
author_email: john@due.com | |||||
title: "..." | |||||
tags: | |||||
- programming | |||||
- go | |||||
- testing | |||||
post: "..." | |||||
``` | |||||
If you need to write raw SQL, probably to call a function, prefix the value | |||||
of the column with `RAW=`: | |||||
```yml | |||||
- id: 1 | |||||
uuid_column: RAW=uuid_generate_v4() | |||||
postgis_type_column: RAW=ST_GeomFromText('params...') | |||||
created_at: RAW=NOW() | |||||
updated_at: RAW=NOW() | |||||
``` | |||||
Your tests would look like this: | |||||
```go | |||||
package myapp | |||||
import ( | |||||
"database/sql" | |||||
_ "github.com/lib/pq" | |||||
"github.com/go-testfixtures/testfixtures/v3" | |||||
) | |||||
var ( | |||||
db *sql.DB | |||||
fixtures *testfixtures.Loader | |||||
) | |||||
func TestMain(m *testing.M) { | |||||
var err error | |||||
// Open connection to the test database. | |||||
// Do NOT import fixtures in a production database! | |||||
// Existing data would be deleted. | |||||
db, err = sql.Open("postgres", "dbname=myapp_test") | |||||
if err != nil { | |||||
... | |||||
} | |||||
fixtures, err := testfixtures.New( | |||||
testfixtures.Database(db), // You database connection | |||||
testfixtures.Dialect("postgres"), // Available: "postgresql", "timescaledb", "mysql", "mariadb", "sqlite" and "sqlserver" | |||||
testfixtures.Directory("testdata/fixtures"), // the directory containing the YAML files | |||||
) | |||||
if err != nil { | |||||
... | |||||
} | |||||
os.Exit(m.Run()) | |||||
} | |||||
func prepareTestDatabase() { | |||||
if err := fixtures.Load(); err != nil { | |||||
... | |||||
} | |||||
} | |||||
func TestX(t *testing.T) { | |||||
prepareTestDatabase() | |||||
// Your test here ... | |||||
} | |||||
func TestY(t *testing.T) { | |||||
prepareTestDatabase() | |||||
// Your test here ... | |||||
} | |||||
func TestZ(t *testing.T) { | |||||
prepareTestDatabase() | |||||
// Your test here ... | |||||
} | |||||
``` | |||||
Alternatively, you can use the `Files` option, to specify which | |||||
files you want to load into the database: | |||||
```go | |||||
fixtures, err := testfixtures.New( | |||||
testfixtures.Database(db), | |||||
testfixtures.Dialect("postgres"), | |||||
testfixtures.Files( | |||||
"fixtures/orders.yml", | |||||
"fixtures/customers.yml", | |||||
), | |||||
) | |||||
if err != nil { | |||||
... | |||||
} | |||||
fixtures, err := testfixtures.NewFiles(db, &testfixtures.PostgreSQL{}, | |||||
"fixtures/orders.yml", | |||||
"fixtures/customers.yml", | |||||
// add as many files you want | |||||
) | |||||
if err != nil { | |||||
... | |||||
} | |||||
``` | |||||
With `Paths` option, you can specify the paths that fixtures will load | |||||
from. Path can be directory or file. If directory, we will search YAML files | |||||
in it. | |||||
```go | |||||
fixtures, err := testfixtures.New( | |||||
testfixtures.Database(db), | |||||
testfixtures.Dialect("postgres"), | |||||
testfixtures.Paths( | |||||
"fixtures/orders.yml", | |||||
"fixtures/customers.yml", | |||||
"common_fixtures/users" | |||||
), | |||||
) | |||||
if err != nil { | |||||
... | |||||
} | |||||
``` | |||||
## Security check | |||||
In order to prevent you from accidentally wiping the wrong database, this | |||||
package will refuse to load fixtures if the database name (or database | |||||
filename for SQLite) doesn't contains "test". If you want to disable this | |||||
check, use: | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.DangerousSkipTestDatabaseCheck(), | |||||
) | |||||
``` | |||||
## Sequences | |||||
For PostgreSQL, this package also resets all sequences to a high | |||||
number to prevent duplicated primary keys while running the tests. | |||||
The default is 10000, but you can change that with: | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.ResetSequencesTo(10000), | |||||
) | |||||
``` | |||||
Or, if you want to skip the reset of sequences entirely: | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.SkipResetSequences(), | |||||
) | |||||
``` | |||||
## Compatible databases | |||||
### PostgreSQL / TimescaleDB | |||||
This package has two approaches to disable foreign keys while importing fixtures | |||||
for PostgreSQL databases: | |||||
#### With `DISABLE TRIGGER` | |||||
This is the default approach. For that use: | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.Dialect("postgres"), // or "timescaledb" | |||||
) | |||||
``` | |||||
With the above snippet this package will use `DISABLE TRIGGER` to temporarily | |||||
disabling foreign key constraints while loading fixtures. This work with any | |||||
version of PostgreSQL, but it is **required** to be connected in the database | |||||
as a SUPERUSER. You can make a PostgreSQL user a SUPERUSER with: | |||||
```sql | |||||
ALTER USER your_user SUPERUSER; | |||||
``` | |||||
#### With `ALTER CONSTRAINT` | |||||
This approach don't require to be connected as a SUPERUSER, but only work with | |||||
PostgreSQL versions >= 9.4. Try this if you are getting foreign key violation | |||||
errors with the previous approach. It is as simple as using: | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.Dialect("postgres"), | |||||
testfixtures.UseAlterConstraint(), | |||||
) | |||||
``` | |||||
Tested using the [github.com/lib/pq](https://github.com/lib/pq) driver. | |||||
### MySQL / MariaDB | |||||
Just make sure the connection string have | |||||
[the multistatement parameter](https://github.com/go-sql-driver/mysql#multistatements) | |||||
set to true, and use: | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.Dialect("mysql"), // or "mariadb" | |||||
) | |||||
``` | |||||
Tested using the [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) driver. | |||||
### SQLite | |||||
SQLite is also supported. It is recommended to create foreign keys as | |||||
`DEFERRABLE` (the default) to prevent problems. See more | |||||
[on the SQLite documentation](https://www.sqlite.org/foreignkeys.html#fk_deferred). | |||||
(Foreign key constraints are no-op by default on SQLite, but enabling it is | |||||
recommended). | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.Dialect("sqlite"), | |||||
) | |||||
``` | |||||
Tested using the [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) driver. | |||||
### Microsoft SQL Server | |||||
SQL Server support requires SQL Server >= 2008. Inserting on `IDENTITY` columns | |||||
are handled as well. Just make sure you are logged in with a user with | |||||
`ALTER TABLE` permission. | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.Dialect("sqlserver"), | |||||
) | |||||
``` | |||||
Tested using the `mssql` and `sqlserver` drivers from the | |||||
[github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) lib. | |||||
## Templating | |||||
Testfixtures supports templating, but it's disabled by default. Most people | |||||
won't need it, but it may be useful to dynamically generate data. | |||||
Enable it by doing: | |||||
```go | |||||
testfixtures.New( | |||||
... | |||||
testfixtures.Template(), | |||||
// the above options are optional | |||||
TemplateFuncs(...), | |||||
TemplateDelims("{{", "}}"), | |||||
TemplateOptions("missingkey=zero"), | |||||
TemplateData(...), | |||||
) | |||||
``` | |||||
The YAML file could look like this: | |||||
```yaml | |||||
# It's possible generate values... | |||||
- id: {{sha256 "my-awesome-post}} | |||||
title: My Awesome Post | |||||
text: {{randomText}} | |||||
# ... or records | |||||
{{range $post := $.Posts}} | |||||
- id: {{$post.Id}} | |||||
title: {{$post.Title}} | |||||
text: {{$post.Text}} | |||||
{{end}} | |||||
``` | |||||
## Generating fixtures for a existing database | |||||
The following code will generate a YAML file for each table of the database | |||||
into a given folder. It may be useful to boostrap a test scenario from a sample | |||||
database of your app. | |||||
```go | |||||
dumper, err := testfixtures.NewDumper( | |||||
testfixtures.DumpDatabase(db), | |||||
testfixtures.DumpDialect("postgres"), // or your database of choice | |||||
testfixtures.DumpDirectory("tmp/fixtures"), | |||||
textfixtures.DumpTables( // optional, will dump all table if not given | |||||
"posts", | |||||
"comments", | |||||
"tags", | |||||
) | |||||
) | |||||
if err != nil { | |||||
... | |||||
} | |||||
if err := dumper.Dump(); err != nil { | |||||
... | |||||
} | |||||
``` | |||||
> This was intended to run in small sample databases. It will likely break | |||||
if run in a production/big database. | |||||
## Gotchas | |||||
### Parallel testing | |||||
This library doesn't yet support running tests in parallel! Running tests | |||||
in parallel can result in random data being present in the database, which | |||||
will likely cause tests to randomly/intermittently fail. | |||||
This is specially tricky since it's not immediately clear that `go test ./...` | |||||
run tests for each package in parallel. If more than one package use this | |||||
library, you can face this issue. Please, use `go test -p 1 ./...` or run tests | |||||
for each package in separated commands to fix this issue. | |||||
If you're looking into being able to run tests in parallel you can try using | |||||
testfixtures together with the [txdb][gotxdb] package, which allows wrapping | |||||
each test run in a transaction. | |||||
## CLI | |||||
We also have a CLI to load fixtures in a given database. | |||||
Grab it from the [releases page](https://github.com/go-testfixtures/testfixtures/releases) | |||||
and use it like: | |||||
```bash | |||||
testfixtures -d postgres -c "postgres://user:password@localhost/database" -D testdata/fixtures | |||||
``` | |||||
The connection string changes for each database driver. | |||||
Use `--help` for all flags. | |||||
## Contributing | |||||
We recommend you to [install Task](https://taskfile.dev/#/installation) and | |||||
Docker before contributing to this package, since some stuff is automated | |||||
using these tools. | |||||
It's recommended to use Docker Compose to run tests, since it runs tests for | |||||
all supported databases once. To do that you just need to run: | |||||
```bash | |||||
task docker | |||||
``` | |||||
But if you want to run tests locally, copy the `.sample.env` file as `.env` | |||||
and edit it according to your database setup. You'll need to create a database | |||||
(likely names `testfixtures_test`) before continuing. Then run the command | |||||
for the database you want to run tests against: | |||||
```bash | |||||
task test:pg # PostgreSQL | |||||
task test:mysql # MySQL | |||||
task test:sqlite # SQLite | |||||
task test:sqlserver # Microsoft SQL Server | |||||
``` | |||||
GitHub Actions (CI) runs the same Docker setup available locally. | |||||
## Alternatives | |||||
If you don't think using fixtures is a good idea, you can try one of these | |||||
packages instead: | |||||
- [factory-go][factorygo]: Factory for Go. Inspired by Python's Factory Boy | |||||
and Ruby's Factory Girl | |||||
- [go-txdb (Single transaction SQL driver for Go)][gotxdb]: Use a single | |||||
database transaction for each functional test, so you can rollback to | |||||
previous state between tests to have the same database state in all tests | |||||
- [go-sqlmock][gosqlmock]: A mock for the sql.DB interface. This allow you to | |||||
unit test database code without having to connect to a real database | |||||
- [dbcleaner][dbcleaner] - Clean database for testing, inspired by | |||||
database_cleaner for Ruby | |||||
[doc]: https://pkg.go.dev/github.com/go-testfixtures/testfixtures/v3?tab=doc | |||||
[railstests]: http://guides.rubyonrails.org/testing.html#the-test-database | |||||
[gotxdb]: https://github.com/DATA-DOG/go-txdb | |||||
[gosqlmock]: https://github.com/DATA-DOG/go-sqlmock | |||||
[factorygo]: https://github.com/bluele/factory-go | |||||
[dbcleaner]: https://github.com/khaiql/dbcleaner |
# https://taskfile.org | |||||
version: '2' | |||||
tasks: | |||||
build: | |||||
cmds: | |||||
- go build -v -tags sqlite -o ./testfixtures{{exeExt}} ./cmd/testfixtures | |||||
test-cli: | |||||
cmds: | |||||
- ./testfixtures -d sqlite -c testdb.sqlite3 -D testdata/fixtures | |||||
test:pg: | |||||
desc: Test PostgreSQL | |||||
cmds: | |||||
- task: test-db | |||||
vars: {DATABASE: postgresql} | |||||
test:mysql: | |||||
desc: Test MySQL | |||||
cmds: | |||||
- task: test:db | |||||
vars: {DATABASE: mysql} | |||||
test:sqlite: | |||||
desc: Test SQLite | |||||
cmds: | |||||
- task: test-db | |||||
vars: {DATABASE: sqlite} | |||||
test:sqlserver: | |||||
desc: Test SQLServer | |||||
cmds: | |||||
- task: test-db | |||||
vars: {DATABASE: sqlserver} | |||||
test-db: | |||||
cmds: | |||||
- go test -v -tags {{.DATABASE}} | |||||
goreleaser:test: | |||||
desc: Tests release process without publishing | |||||
cmds: | |||||
- goreleaser --snapshot --rm-dist | |||||
docker: | |||||
cmds: | |||||
- task: docker:build | |||||
- task: docker:test | |||||
docker:build: | |||||
cmds: | |||||
- docker build -t testfixtures . | |||||
docker:test: | |||||
cmds: | |||||
- docker-compose down -v | |||||
- docker-compose run testfixtures go test -v -tags 'postgresql sqlite mysql sqlserver' |
version: '3' | |||||
services: | |||||
testfixtures: | |||||
image: testfixtures | |||||
depends_on: | |||||
- postgresql | |||||
- mysql | |||||
- sqlserver | |||||
environment: | |||||
PGPASSWORD: postgres | |||||
PG_CONN_STRING: host=postgresql user=postgres dbname=testfixtures_test port=5432 sslmode=disable | |||||
MYSQL_CONN_STRING: root:mysql@tcp(mysql)/testfixtures_test?multiStatements=true | |||||
SQLITE_CONN_STRING: testfixtures_test.sqlite3 | |||||
SQLSERVER_CONN_STRING: server=sqlserver;database=master;user id=sa;password=SQL@1server;encrypt=disable | |||||
postgresql: | |||||
image: postgres:12.1-alpine | |||||
environment: | |||||
POSTGRES_DB: testfixtures_test | |||||
POSTGRES_USER: postgres | |||||
POSTGRES_PASSWORD: postgres | |||||
mysql: | |||||
image: mysql:8.0 | |||||
environment: | |||||
MYSQL_DATABASE: testfixtures_test | |||||
MYSQL_ROOT_PASSWORD: mysql | |||||
sqlserver: | |||||
image: mcr.microsoft.com/mssql/server:2019-latest | |||||
environment: | |||||
ACCEPT_EULA: 'Y' | |||||
SA_PASSWORD: SQL@1server |
package testfixtures | |||||
import ( | |||||
"database/sql" | |||||
"fmt" | |||||
"os" | |||||
"path/filepath" | |||||
"unicode/utf8" | |||||
"gopkg.in/yaml.v2" | |||||
) | |||||
// Dumper is resposible for dumping fixtures from the database into a | |||||
// directory. | |||||
type Dumper struct { | |||||
db *sql.DB | |||||
helper helper | |||||
dir string | |||||
tables []string | |||||
} | |||||
// NewDumper creates a new dumper with the given options. | |||||
// | |||||
// The "DumpDatabase", "DumpDialect" and "DumpDirectory" options are required. | |||||
func NewDumper(options ...func(*Dumper) error) (*Dumper, error) { | |||||
d := &Dumper{} | |||||
for _, option := range options { | |||||
if err := option(d); err != nil { | |||||
return nil, err | |||||
} | |||||
} | |||||
return d, nil | |||||
} | |||||
// DumpDatabase sets the database to be dumped. | |||||
func DumpDatabase(db *sql.DB) func(*Dumper) error { | |||||
return func(d *Dumper) error { | |||||
d.db = db | |||||
return nil | |||||
} | |||||
} | |||||
// DumpDialect informs Loader about which database dialect you're using. | |||||
// | |||||
// Possible options are "postgresql", "timescaledb", "mysql", "mariadb", | |||||
// "sqlite" and "sqlserver". | |||||
func DumpDialect(dialect string) func(*Dumper) error { | |||||
return func(d *Dumper) error { | |||||
h, err := helperForDialect(dialect) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
d.helper = h | |||||
return nil | |||||
} | |||||
} | |||||
// DumpDirectory sets the directory where the fixtures files will be created. | |||||
func DumpDirectory(dir string) func(*Dumper) error { | |||||
return func(d *Dumper) error { | |||||
d.dir = dir | |||||
return nil | |||||
} | |||||
} | |||||
// DumpTables allows you to choose which tables you want to dump. | |||||
// | |||||
// If not informed, Dumper will dump all tables by default. | |||||
func DumpTables(tables ...string) func(*Dumper) error { | |||||
return func(d *Dumper) error { | |||||
d.tables = tables | |||||
return nil | |||||
} | |||||
} | |||||
// Dump dumps the databases as YAML fixtures. | |||||
func (d *Dumper) Dump() error { | |||||
tables := d.tables | |||||
if len(tables) == 0 { | |||||
var err error | |||||
tables, err = d.helper.tableNames(d.db) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
for _, table := range tables { | |||||
if err := d.dumpTable(table); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func (d *Dumper) dumpTable(table string) error { | |||||
query := fmt.Sprintf("SELECT * FROM %s", d.helper.quoteKeyword(table)) | |||||
stmt, err := d.db.Prepare(query) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer stmt.Close() | |||||
rows, err := stmt.Query() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer rows.Close() | |||||
columns, err := rows.Columns() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
fixtures := make([]yaml.MapSlice, 0, 10) | |||||
for rows.Next() { | |||||
entries := make([]interface{}, len(columns)) | |||||
entryPtrs := make([]interface{}, len(entries)) | |||||
for i := range entries { | |||||
entryPtrs[i] = &entries[i] | |||||
} | |||||
if err := rows.Scan(entryPtrs...); err != nil { | |||||
return err | |||||
} | |||||
entryMap := make([]yaml.MapItem, len(entries)) | |||||
for i, column := range columns { | |||||
entryMap[i] = yaml.MapItem{ | |||||
Key: column, | |||||
Value: convertValue(entries[i]), | |||||
} | |||||
} | |||||
fixtures = append(fixtures, entryMap) | |||||
} | |||||
if err = rows.Err(); err != nil { | |||||
return err | |||||
} | |||||
filePath := filepath.Join(d.dir, table+".yml") | |||||
f, err := os.Create(filePath) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer f.Close() | |||||
data, err := yaml.Marshal(fixtures) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
_, err = f.Write(data) | |||||
return err | |||||
} | |||||
func convertValue(value interface{}) interface{} { | |||||
switch v := value.(type) { | |||||
case []byte: | |||||
if utf8.Valid(v) { | |||||
return string(v) | |||||
} | |||||
} | |||||
return value | |||||
} |
module github.com/go-testfixtures/testfixtures/v3 | |||||
require ( | |||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 | |||||
github.com/go-sql-driver/mysql v1.4.1 | |||||
github.com/joho/godotenv v1.3.0 | |||||
github.com/lib/pq v1.3.0 | |||||
github.com/mattn/go-sqlite3 v2.0.2+incompatible | |||||
github.com/spf13/pflag v1.0.5 | |||||
google.golang.org/appengine v1.3.0 // indirect | |||||
gopkg.in/yaml.v2 v2.2.7 | |||||
) | |||||
go 1.13 |
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA= | |||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | |||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= | |||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | |||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= | |||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= | |||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | |||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= | |||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= | |||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= | |||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | |||||
github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U= | |||||
github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | |||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | |||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | |||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= | |||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | |||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |||||
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= | |||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | |||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | |||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= | |||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
const ( | const ( | ||||
paramTypeDollar = iota + 1 | paramTypeDollar = iota + 1 | ||||
paramTypeQuestion | paramTypeQuestion | ||||
paramTypeColon | |||||
paramTypeAtSign | |||||
) | ) | ||||
type loadFunction func(tx *sql.Tx) error | type loadFunction func(tx *sql.Tx) error | ||||
// Helper is the generic interface for the database helper | |||||
type Helper interface { | |||||
type helper interface { | |||||
init(*sql.DB) error | init(*sql.DB) error | ||||
disableReferentialIntegrity(*sql.DB, loadFunction) error | disableReferentialIntegrity(*sql.DB, loadFunction) error | ||||
paramType() int | paramType() int | ||||
} | } | ||||
var ( | var ( | ||||
_ Helper = &MySQL{} | |||||
_ Helper = &PostgreSQL{} | |||||
_ Helper = &SQLite{} | |||||
_ Helper = &Oracle{} | |||||
_ Helper = &SQLServer{} | |||||
_ helper = &mySQL{} | |||||
_ helper = &postgreSQL{} | |||||
_ helper = &sqlite{} | |||||
_ helper = &sqlserver{} | |||||
) | ) | ||||
type baseHelper struct{} | type baseHelper struct{} |
"fmt" | "fmt" | ||||
) | ) | ||||
// MySQL is the MySQL helper for this package | |||||
type MySQL struct { | |||||
type mySQL struct { | |||||
baseHelper | baseHelper | ||||
tables []string | tables []string | ||||
tablesChecksum map[string]int64 | tablesChecksum map[string]int64 | ||||
} | } | ||||
func (h *MySQL) init(db *sql.DB) error { | |||||
func (h *mySQL) init(db *sql.DB) error { | |||||
var err error | var err error | ||||
h.tables, err = h.tableNames(db) | h.tables, err = h.tableNames(db) | ||||
if err != nil { | if err != nil { | ||||
return nil | return nil | ||||
} | } | ||||
func (*MySQL) paramType() int { | |||||
func (*mySQL) paramType() int { | |||||
return paramTypeQuestion | return paramTypeQuestion | ||||
} | } | ||||
func (*MySQL) quoteKeyword(str string) string { | |||||
func (*mySQL) quoteKeyword(str string) string { | |||||
return fmt.Sprintf("`%s`", str) | return fmt.Sprintf("`%s`", str) | ||||
} | } | ||||
func (*MySQL) databaseName(q queryable) (string, error) { | |||||
func (*mySQL) databaseName(q queryable) (string, error) { | |||||
var dbName string | var dbName string | ||||
err := q.QueryRow("SELECT DATABASE()").Scan(&dbName) | err := q.QueryRow("SELECT DATABASE()").Scan(&dbName) | ||||
return dbName, err | return dbName, err | ||||
} | } | ||||
func (h *MySQL) tableNames(q queryable) ([]string, error) { | |||||
func (h *mySQL) tableNames(q queryable) ([]string, error) { | |||||
query := ` | query := ` | ||||
SELECT table_name | SELECT table_name | ||||
FROM information_schema.tables | FROM information_schema.tables | ||||
} | } | ||||
func (h *MySQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
// re-enable after load | |||||
defer func() { | |||||
if _, err2 := db.Exec("SET FOREIGN_KEY_CHECKS = 1"); err2 != nil && err == nil { | |||||
err = err2 | |||||
} | |||||
}() | |||||
func (h *mySQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
tx, err := db.Begin() | tx, err := db.Begin() | ||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
return err | return err | ||||
} | } | ||||
if err = loadFn(tx); err != nil { | |||||
err = loadFn(tx) | |||||
_, err2 := tx.Exec("SET FOREIGN_KEY_CHECKS = 1") | |||||
if err != nil { | |||||
return err | return err | ||||
} | } | ||||
if err2 != nil { | |||||
return err2 | |||||
} | |||||
return tx.Commit() | return tx.Commit() | ||||
} | } | ||||
func (h *MySQL) isTableModified(q queryable, tableName string) (bool, error) { | |||||
func (h *mySQL) isTableModified(q queryable, tableName string) (bool, error) { | |||||
checksum, err := h.getChecksum(q, tableName) | checksum, err := h.getChecksum(q, tableName) | ||||
if err != nil { | if err != nil { | ||||
return true, err | return true, err | ||||
return oldChecksum == 0 || checksum != oldChecksum, nil | return oldChecksum == 0 || checksum != oldChecksum, nil | ||||
} | } | ||||
func (h *MySQL) afterLoad(q queryable) error { | |||||
func (h *mySQL) afterLoad(q queryable) error { | |||||
if h.tablesChecksum != nil { | if h.tablesChecksum != nil { | ||||
return nil | return nil | ||||
} | } | ||||
return nil | return nil | ||||
} | } | ||||
func (h *MySQL) getChecksum(q queryable, tableName string) (int64, error) { | |||||
func (h *mySQL) getChecksum(q queryable, tableName string) (int64, error) { | |||||
sql := fmt.Sprintf("CHECKSUM TABLE %s", h.quoteKeyword(tableName)) | sql := fmt.Sprintf("CHECKSUM TABLE %s", h.quoteKeyword(tableName)) | ||||
var ( | var ( | ||||
table string | table string |
"strings" | "strings" | ||||
) | ) | ||||
// PostgreSQL is the PG helper for this package | |||||
type PostgreSQL struct { | |||||
type postgreSQL struct { | |||||
baseHelper | baseHelper | ||||
// UseAlterConstraint If true, the contraint disabling will do | |||||
// using ALTER CONTRAINT sintax, only allowed in PG >= 9.4. | |||||
// If false, the constraint disabling will use DISABLE TRIGGER ALL, | |||||
// which requires SUPERUSER privileges. | |||||
UseAlterConstraint bool | |||||
useAlterConstraint bool | |||||
skipResetSequences bool | |||||
resetSequencesTo int64 | |||||
tables []string | tables []string | ||||
sequences []string | sequences []string | ||||
constraintName string | constraintName string | ||||
} | } | ||||
func (h *PostgreSQL) init(db *sql.DB) error { | |||||
func (h *postgreSQL) init(db *sql.DB) error { | |||||
var err error | var err error | ||||
h.tables, err = h.tableNames(db) | h.tables, err = h.tableNames(db) | ||||
return nil | return nil | ||||
} | } | ||||
func (*PostgreSQL) paramType() int { | |||||
func (*postgreSQL) paramType() int { | |||||
return paramTypeDollar | return paramTypeDollar | ||||
} | } | ||||
func (*PostgreSQL) databaseName(q queryable) (string, error) { | |||||
func (*postgreSQL) databaseName(q queryable) (string, error) { | |||||
var dbName string | var dbName string | ||||
err := q.QueryRow("SELECT current_database()").Scan(&dbName) | err := q.QueryRow("SELECT current_database()").Scan(&dbName) | ||||
return dbName, err | return dbName, err | ||||
} | } | ||||
func (h *PostgreSQL) tableNames(q queryable) ([]string, error) { | |||||
func (h *postgreSQL) tableNames(q queryable) ([]string, error) { | |||||
var tables []string | var tables []string | ||||
sql := ` | sql := ` | ||||
INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace | INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace | ||||
WHERE pg_class.relkind = 'r' | WHERE pg_class.relkind = 'r' | ||||
AND pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema') | AND pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema') | ||||
AND pg_namespace.nspname NOT LIKE 'pg_toast%'; | |||||
AND pg_namespace.nspname NOT LIKE 'pg_toast%' | |||||
AND pg_namespace.nspname NOT LIKE '\_timescaledb%'; | |||||
` | ` | ||||
rows, err := q.Query(sql) | rows, err := q.Query(sql) | ||||
if err != nil { | if err != nil { | ||||
return tables, nil | return tables, nil | ||||
} | } | ||||
func (h *PostgreSQL) getSequences(q queryable) ([]string, error) { | |||||
func (h *postgreSQL) getSequences(q queryable) ([]string, error) { | |||||
const sql = ` | const sql = ` | ||||
SELECT pg_namespace.nspname || '.' || pg_class.relname AS sequence_name | SELECT pg_namespace.nspname || '.' || pg_class.relname AS sequence_name | ||||
FROM pg_class | FROM pg_class | ||||
INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace | INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace | ||||
WHERE pg_class.relkind = 'S' | WHERE pg_class.relkind = 'S' | ||||
AND pg_namespace.nspname NOT LIKE '\_timescaledb%' | |||||
` | ` | ||||
rows, err := q.Query(sql) | rows, err := q.Query(sql) | ||||
return sequences, nil | return sequences, nil | ||||
} | } | ||||
func (*PostgreSQL) getNonDeferrableConstraints(q queryable) ([]pgConstraint, error) { | |||||
func (*postgreSQL) getNonDeferrableConstraints(q queryable) ([]pgConstraint, error) { | |||||
var constraints []pgConstraint | var constraints []pgConstraint | ||||
sql := ` | sql := ` | ||||
FROM information_schema.table_constraints | FROM information_schema.table_constraints | ||||
WHERE constraint_type = 'FOREIGN KEY' | WHERE constraint_type = 'FOREIGN KEY' | ||||
AND is_deferrable = 'NO' | AND is_deferrable = 'NO' | ||||
AND table_schema NOT LIKE '\_timescaledb%' | |||||
` | ` | ||||
rows, err := q.Query(sql) | rows, err := q.Query(sql) | ||||
if err != nil { | if err != nil { | ||||
return constraints, nil | return constraints, nil | ||||
} | } | ||||
func (h *PostgreSQL) disableTriggers(db *sql.DB, loadFn loadFunction) (err error) { | |||||
func (h *postgreSQL) disableTriggers(db *sql.DB, loadFn loadFunction) (err error) { | |||||
defer func() { | defer func() { | ||||
// re-enable triggers after load | // re-enable triggers after load | ||||
var sql string | var sql string | ||||
return tx.Commit() | return tx.Commit() | ||||
} | } | ||||
func (h *PostgreSQL) makeConstraintsDeferrable(db *sql.DB, loadFn loadFunction) (err error) { | |||||
func (h *postgreSQL) makeConstraintsDeferrable(db *sql.DB, loadFn loadFunction) (err error) { | |||||
defer func() { | defer func() { | ||||
// ensure constraint being not deferrable again after load | // ensure constraint being not deferrable again after load | ||||
var sql string | var sql string | ||||
return tx.Commit() | return tx.Commit() | ||||
} | } | ||||
func (h *PostgreSQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
func (h *postgreSQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
// ensure sequences being reset after load | // ensure sequences being reset after load | ||||
defer func() { | |||||
if err2 := h.resetSequences(db); err2 != nil && err == nil { | |||||
err = err2 | |||||
} | |||||
}() | |||||
if !h.skipResetSequences { | |||||
defer func() { | |||||
if err2 := h.resetSequences(db); err2 != nil && err == nil { | |||||
err = err2 | |||||
} | |||||
}() | |||||
} | |||||
if h.UseAlterConstraint { | |||||
if h.useAlterConstraint { | |||||
return h.makeConstraintsDeferrable(db, loadFn) | return h.makeConstraintsDeferrable(db, loadFn) | ||||
} | } | ||||
return h.disableTriggers(db, loadFn) | return h.disableTriggers(db, loadFn) | ||||
} | } | ||||
func (h *PostgreSQL) resetSequences(db *sql.DB) error { | |||||
func (h *postgreSQL) resetSequences(db *sql.DB) error { | |||||
resetSequencesTo := h.resetSequencesTo | |||||
if resetSequencesTo == 0 { | |||||
resetSequencesTo = 10000 | |||||
} | |||||
for _, sequence := range h.sequences { | for _, sequence := range h.sequences { | ||||
_, err := db.Exec(fmt.Sprintf("SELECT SETVAL('%s', %d)", sequence, resetSequencesTo)) | _, err := db.Exec(fmt.Sprintf("SELECT SETVAL('%s', %d)", sequence, resetSequencesTo)) | ||||
if err != nil { | if err != nil { | ||||
return nil | return nil | ||||
} | } | ||||
func (h *PostgreSQL) isTableModified(q queryable, tableName string) (bool, error) { | |||||
func (h *postgreSQL) isTableModified(q queryable, tableName string) (bool, error) { | |||||
checksum, err := h.getChecksum(q, tableName) | checksum, err := h.getChecksum(q, tableName) | ||||
if err != nil { | if err != nil { | ||||
return false, err | return false, err | ||||
return oldChecksum == "" || checksum != oldChecksum, nil | return oldChecksum == "" || checksum != oldChecksum, nil | ||||
} | } | ||||
func (h *PostgreSQL) afterLoad(q queryable) error { | |||||
func (h *postgreSQL) afterLoad(q queryable) error { | |||||
if h.tablesChecksum != nil { | if h.tablesChecksum != nil { | ||||
return nil | return nil | ||||
} | } | ||||
return nil | return nil | ||||
} | } | ||||
func (h *PostgreSQL) getChecksum(q queryable, tableName string) (string, error) { | |||||
func (h *postgreSQL) getChecksum(q queryable, tableName string) (string, error) { | |||||
sqlStr := fmt.Sprintf(` | sqlStr := fmt.Sprintf(` | ||||
SELECT md5(CAST((array_agg(t.*)) AS TEXT)) | SELECT md5(CAST((array_agg(t.*)) AS TEXT)) | ||||
FROM %s AS t | FROM %s AS t | ||||
return checksum.String, nil | return checksum.String, nil | ||||
} | } | ||||
func (*PostgreSQL) quoteKeyword(s string) string { | |||||
func (*postgreSQL) quoteKeyword(s string) string { | |||||
parts := strings.Split(s, ".") | parts := strings.Split(s, ".") | ||||
for i, p := range parts { | for i, p := range parts { | ||||
parts[i] = fmt.Sprintf(`"%s"`, p) | parts[i] = fmt.Sprintf(`"%s"`, p) |
"path/filepath" | "path/filepath" | ||||
) | ) | ||||
// SQLite is the SQLite Helper for this package | |||||
type SQLite struct { | |||||
type sqlite struct { | |||||
baseHelper | baseHelper | ||||
} | } | ||||
func (*SQLite) paramType() int { | |||||
func (*sqlite) paramType() int { | |||||
return paramTypeQuestion | return paramTypeQuestion | ||||
} | } | ||||
func (*SQLite) databaseName(q queryable) (string, error) { | |||||
func (*sqlite) databaseName(q queryable) (string, error) { | |||||
var seq int | var seq int | ||||
var main, dbName string | var main, dbName string | ||||
err := q.QueryRow("PRAGMA database_list").Scan(&seq, &main, &dbName) | err := q.QueryRow("PRAGMA database_list").Scan(&seq, &main, &dbName) | ||||
return dbName, nil | return dbName, nil | ||||
} | } | ||||
func (*SQLite) tableNames(q queryable) ([]string, error) { | |||||
func (*sqlite) tableNames(q queryable) ([]string, error) { | |||||
query := ` | query := ` | ||||
SELECT name | SELECT name | ||||
FROM sqlite_master | FROM sqlite_master | ||||
return tables, nil | return tables, nil | ||||
} | } | ||||
func (*SQLite) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
func (*sqlite) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
defer func() { | defer func() { | ||||
if _, err2 := db.Exec("PRAGMA defer_foreign_keys = OFF"); err2 != nil && err == nil { | if _, err2 := db.Exec("PRAGMA defer_foreign_keys = OFF"); err2 != nil && err == nil { | ||||
err = err2 | err = err2 |
"strings" | "strings" | ||||
) | ) | ||||
// SQLServer is the helper for SQL Server for this package. | |||||
// SQL Server >= 2008 is required. | |||||
type SQLServer struct { | |||||
type sqlserver struct { | |||||
baseHelper | baseHelper | ||||
tables []string | |||||
paramTypeCache int | |||||
tables []string | |||||
} | } | ||||
func (h *SQLServer) init(db *sql.DB) error { | |||||
func (h *sqlserver) init(db *sql.DB) error { | |||||
var err error | var err error | ||||
// NOTE(@andreynering): The SQL Server lib (github.com/denisenkom/go-mssqldb) | |||||
// supports both the "?" style (when using the deprecated "mssql" driver) | |||||
// and the "@p1" style (when using the new "sqlserver" driver). | |||||
// | |||||
// Since we don't have a way to know which driver it's been used, | |||||
// this is a small hack to detect the allowed param style. | |||||
var v int | |||||
if err := db.QueryRow("SELECT ?", 1).Scan(&v); err == nil && v == 1 { | |||||
h.paramTypeCache = paramTypeQuestion | |||||
} else { | |||||
h.paramTypeCache = paramTypeAtSign | |||||
} | |||||
h.tables, err = h.tableNames(db) | h.tables, err = h.tableNames(db) | ||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
return nil | return nil | ||||
} | } | ||||
func (*SQLServer) paramType() int { | |||||
return paramTypeQuestion | |||||
func (h *sqlserver) paramType() int { | |||||
return h.paramTypeCache | |||||
} | } | ||||
func (*SQLServer) quoteKeyword(s string) string { | |||||
func (*sqlserver) quoteKeyword(s string) string { | |||||
parts := strings.Split(s, ".") | parts := strings.Split(s, ".") | ||||
for i, p := range parts { | for i, p := range parts { | ||||
parts[i] = fmt.Sprintf(`[%s]`, p) | parts[i] = fmt.Sprintf(`[%s]`, p) | ||||
return strings.Join(parts, ".") | return strings.Join(parts, ".") | ||||
} | } | ||||
func (*SQLServer) databaseName(q queryable) (string, error) { | |||||
func (*sqlserver) databaseName(q queryable) (string, error) { | |||||
var dbName string | var dbName string | ||||
err := q.QueryRow("SELECT DB_NAME()").Scan(&dbName) | err := q.QueryRow("SELECT DB_NAME()").Scan(&dbName) | ||||
return dbName, err | return dbName, err | ||||
} | } | ||||
func (*SQLServer) tableNames(q queryable) ([]string, error) { | |||||
rows, err := q.Query("SELECT table_schema + '.' + table_name FROM information_schema.tables") | |||||
func (*sqlserver) tableNames(q queryable) ([]string, error) { | |||||
rows, err := q.Query("SELECT table_schema + '.' + table_name FROM information_schema.tables WHERE table_name <> 'spt_values'") | |||||
if err != nil { | if err != nil { | ||||
return nil, err | return nil, err | ||||
} | } | ||||
return tables, nil | return tables, nil | ||||
} | } | ||||
func (h *SQLServer) tableHasIdentityColumn(q queryable, tableName string) bool { | |||||
sql := ` | |||||
func (h *sqlserver) tableHasIdentityColumn(q queryable, tableName string) (bool, error) { | |||||
sql := fmt.Sprintf(` | |||||
SELECT COUNT(*) | SELECT COUNT(*) | ||||
FROM SYS.IDENTITY_COLUMNS | FROM SYS.IDENTITY_COLUMNS | ||||
WHERE OBJECT_ID = OBJECT_ID(?) | |||||
` | |||||
WHERE OBJECT_ID = OBJECT_ID('%s') | |||||
`, tableName) | |||||
var count int | var count int | ||||
q.QueryRow(sql, h.quoteKeyword(tableName)).Scan(&count) | |||||
return count > 0 | |||||
if err := q.QueryRow(sql).Scan(&count); err != nil { | |||||
return false, err | |||||
} | |||||
return count > 0, nil | |||||
} | } | ||||
func (h *SQLServer) whileInsertOnTable(tx *sql.Tx, tableName string, fn func() error) (err error) { | |||||
if h.tableHasIdentityColumn(tx, tableName) { | |||||
func (h *sqlserver) whileInsertOnTable(tx *sql.Tx, tableName string, fn func() error) (err error) { | |||||
hasIdentityColumn, err := h.tableHasIdentityColumn(tx, tableName) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if hasIdentityColumn { | |||||
defer func() { | defer func() { | ||||
_, err2 := tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", h.quoteKeyword(tableName))) | _, err2 := tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", h.quoteKeyword(tableName))) | ||||
if err2 != nil && err == nil { | if err2 != nil && err == nil { | ||||
err = err2 | |||||
err = fmt.Errorf("testfixtures: could not disable identity insert: %w", err2) | |||||
} | } | ||||
}() | }() | ||||
_, err := tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", h.quoteKeyword(tableName))) | _, err := tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", h.quoteKeyword(tableName))) | ||||
if err != nil { | if err != nil { | ||||
return err | |||||
return fmt.Errorf("testfixtures: could not enable identity insert: %w", err) | |||||
} | } | ||||
} | } | ||||
return fn() | return fn() | ||||
} | } | ||||
func (h *SQLServer) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
func (h *sqlserver) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
// ensure the triggers are re-enable after all | // ensure the triggers are re-enable after all | ||||
defer func() { | defer func() { | ||||
var sql string | var sql string | ||||
// SQL Server because commands like a `CREATE SCHEMA...` and a `CREATE TABLE...` | // SQL Server because commands like a `CREATE SCHEMA...` and a `CREATE TABLE...` | ||||
// could not be executed in the same batch. | // could not be executed in the same batch. | ||||
// See https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2008-r2/ms175502(v=sql.105)#rules-for-using-batches | // See https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2008-r2/ms175502(v=sql.105)#rules-for-using-batches | ||||
func (*SQLServer) splitter() []byte { | |||||
func (*sqlserver) splitter() []byte { | |||||
return []byte("GO\n") | return []byte("GO\n") | ||||
} | } |
package testfixtures // import "github.com/go-testfixtures/testfixtures/v3" | |||||
import ( | |||||
"bytes" | |||||
"database/sql" | |||||
"fmt" | |||||
"io/ioutil" | |||||
"os" | |||||
"path" | |||||
"path/filepath" | |||||
"regexp" | |||||
"strings" | |||||
"text/template" | |||||
"time" | |||||
"gopkg.in/yaml.v2" | |||||
) | |||||
// Loader is the responsible to loading fixtures. | |||||
type Loader struct { | |||||
db *sql.DB | |||||
helper helper | |||||
fixturesFiles []*fixtureFile | |||||
skipTestDatabaseCheck bool | |||||
location *time.Location | |||||
template bool | |||||
templateFuncs template.FuncMap | |||||
templateLeftDelim string | |||||
templateRightDelim string | |||||
templateOptions []string | |||||
templateData interface{} | |||||
} | |||||
type fixtureFile struct { | |||||
path string | |||||
fileName string | |||||
content []byte | |||||
insertSQLs []insertSQL | |||||
} | |||||
type insertSQL struct { | |||||
sql string | |||||
params []interface{} | |||||
} | |||||
var ( | |||||
testDatabaseRegexp = regexp.MustCompile("(?i)test") | |||||
errDatabaseIsRequired = fmt.Errorf("testfixtures: database is required") | |||||
errDialectIsRequired = fmt.Errorf("testfixtures: dialect is required") | |||||
) | |||||
// New instantiates a new Loader instance. The "Database" and "Driver" | |||||
// options are required. | |||||
func New(options ...func(*Loader) error) (*Loader, error) { | |||||
l := &Loader{ | |||||
templateLeftDelim: "{{", | |||||
templateRightDelim: "}}", | |||||
templateOptions: []string{"missingkey=zero"}, | |||||
} | |||||
for _, option := range options { | |||||
if err := option(l); err != nil { | |||||
return nil, err | |||||
} | |||||
} | |||||
if l.db == nil { | |||||
return nil, errDatabaseIsRequired | |||||
} | |||||
if l.helper == nil { | |||||
return nil, errDialectIsRequired | |||||
} | |||||
if err := l.helper.init(l.db); err != nil { | |||||
return nil, err | |||||
} | |||||
if err := l.buildInsertSQLs(); err != nil { | |||||
return nil, err | |||||
} | |||||
return l, nil | |||||
} | |||||
// Database sets an existing sql.DB instant to Loader. | |||||
func Database(db *sql.DB) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
l.db = db | |||||
return nil | |||||
} | |||||
} | |||||
// Dialect informs Loader about which database dialect you're using. | |||||
// | |||||
// Possible options are "postgresql", "timescaledb", "mysql", "mariadb", | |||||
// "sqlite" and "sqlserver". | |||||
func Dialect(dialect string) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
h, err := helperForDialect(dialect) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
l.helper = h | |||||
return nil | |||||
} | |||||
} | |||||
func helperForDialect(dialect string) (helper, error) { | |||||
switch dialect { | |||||
case "postgres", "postgresql", "timescaledb": | |||||
return &postgreSQL{}, nil | |||||
case "mysql", "mariadb": | |||||
return &mySQL{}, nil | |||||
case "sqlite", "sqlite3": | |||||
return &sqlite{}, nil | |||||
case "mssql", "sqlserver": | |||||
return &sqlserver{}, nil | |||||
default: | |||||
return nil, fmt.Errorf(`testfixtures: unrecognized dialect "%s"`, dialect) | |||||
} | |||||
} | |||||
// UseAlterConstraint If true, the contraint disabling will do | |||||
// using ALTER CONTRAINT sintax, only allowed in PG >= 9.4. | |||||
// If false, the constraint disabling will use DISABLE TRIGGER ALL, | |||||
// which requires SUPERUSER privileges. | |||||
// | |||||
// Only valid for PostgreSQL. Returns an error otherwise. | |||||
func UseAlterConstraint() func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
pgHelper, ok := l.helper.(*postgreSQL) | |||||
if !ok { | |||||
return fmt.Errorf("testfixtures: UseAlterConstraint is only valid for PostgreSQL databases") | |||||
} | |||||
pgHelper.useAlterConstraint = true | |||||
return nil | |||||
} | |||||
} | |||||
// SkipResetSequences prevents Loader from reseting sequences after loading | |||||
// fixtures. | |||||
// | |||||
// Only valid for PostgreSQL. Returns an error otherwise. | |||||
func SkipResetSequences() func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
pgHelper, ok := l.helper.(*postgreSQL) | |||||
if !ok { | |||||
return fmt.Errorf("testfixtures: SkipResetSequences is only valid for PostgreSQL databases") | |||||
} | |||||
pgHelper.skipResetSequences = true | |||||
return nil | |||||
} | |||||
} | |||||
// ResetSequencesTo sets the value the sequences will be reset to. | |||||
// | |||||
// Defaults to 10000. | |||||
// | |||||
// Only valid for PostgreSQL. Returns an error otherwise. | |||||
func ResetSequencesTo(value int64) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
pgHelper, ok := l.helper.(*postgreSQL) | |||||
if !ok { | |||||
return fmt.Errorf("testfixtures: ResetSequencesTo is only valid for PostgreSQL databases") | |||||
} | |||||
pgHelper.resetSequencesTo = value | |||||
return nil | |||||
} | |||||
} | |||||
// DangerousSkipTestDatabaseCheck will make Loader not check if the database | |||||
// name contains "test". Use with caution! | |||||
func DangerousSkipTestDatabaseCheck() func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
l.skipTestDatabaseCheck = true | |||||
return nil | |||||
} | |||||
} | |||||
// Directory informs Loader to load YAML files from a given directory. | |||||
func Directory(dir string) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
fixtures, err := l.fixturesFromDir(dir) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
l.fixturesFiles = append(l.fixturesFiles, fixtures...) | |||||
return nil | |||||
} | |||||
} | |||||
// Files informs Loader to load a given set of YAML files. | |||||
func Files(files ...string) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
fixtures, err := l.fixturesFromFiles(files...) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
l.fixturesFiles = append(l.fixturesFiles, fixtures...) | |||||
return nil | |||||
} | |||||
} | |||||
// Paths inform Loader to load a given set of YAML files and directories. | |||||
func Paths(paths ...string) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
fixtures, err := l.fixturesFromPaths(paths...) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
l.fixturesFiles = append(l.fixturesFiles, fixtures...) | |||||
return nil | |||||
} | |||||
} | |||||
// Location makes Loader use the given location by default when parsing | |||||
// dates. If not given, by default it uses the value of time.Local. | |||||
func Location(location *time.Location) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
l.location = location | |||||
return nil | |||||
} | |||||
} | |||||
// Template makes loader process each YAML file as an template using the | |||||
// text/template package. | |||||
// | |||||
// For more information on how templates work in Go please read: | |||||
// https://golang.org/pkg/text/template/ | |||||
// | |||||
// If not given the YAML files are parsed as is. | |||||
func Template() func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
l.template = true | |||||
return nil | |||||
} | |||||
} | |||||
// TemplateFuncs allow choosing which functions will be available | |||||
// when processing templates. | |||||
// | |||||
// For more information see: https://golang.org/pkg/text/template/#Template.Funcs | |||||
func TemplateFuncs(funcs template.FuncMap) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
if !l.template { | |||||
return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateFuns() option`) | |||||
} | |||||
l.templateFuncs = funcs | |||||
return nil | |||||
} | |||||
} | |||||
// TemplateDelims allow choosing which delimiters will be used for templating. | |||||
// This defaults to "{{" and "}}". | |||||
// | |||||
// For more information see https://golang.org/pkg/text/template/#Template.Delims | |||||
func TemplateDelims(left, right string) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
if !l.template { | |||||
return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateDelims() option`) | |||||
} | |||||
l.templateLeftDelim = left | |||||
l.templateRightDelim = right | |||||
return nil | |||||
} | |||||
} | |||||
// TemplateOptions allows you to specific which text/template options will | |||||
// be enabled when processing templates. | |||||
// | |||||
// This defaults to "missingkey=zero". Check the available options here: | |||||
// https://golang.org/pkg/text/template/#Template.Option | |||||
func TemplateOptions(options ...string) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
if !l.template { | |||||
return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateOptions() option`) | |||||
} | |||||
l.templateOptions = options | |||||
return nil | |||||
} | |||||
} | |||||
// TemplateData allows you to specify which data will be available | |||||
// when processing templates. Data is accesible by prefixing it with a "." | |||||
// like {{.MyKey}}. | |||||
func TemplateData(data interface{}) func(*Loader) error { | |||||
return func(l *Loader) error { | |||||
if !l.template { | |||||
return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateData() option`) | |||||
} | |||||
l.templateData = data | |||||
return nil | |||||
} | |||||
} | |||||
// EnsureTestDatabase returns an error if the database name does not contains | |||||
// "test". | |||||
func (l *Loader) EnsureTestDatabase() error { | |||||
dbName, err := l.helper.databaseName(l.db) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if !testDatabaseRegexp.MatchString(dbName) { | |||||
return fmt.Errorf(`testfixtures: database "%s" does not appear to be a test database`, dbName) | |||||
} | |||||
return nil | |||||
} | |||||
// Load wipes and after load all fixtures in the database. | |||||
// if err := fixtures.Load(); err != nil { | |||||
// ... | |||||
// } | |||||
func (l *Loader) Load() error { | |||||
if !l.skipTestDatabaseCheck { | |||||
if err := l.EnsureTestDatabase(); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
err := l.helper.disableReferentialIntegrity(l.db, func(tx *sql.Tx) error { | |||||
for _, file := range l.fixturesFiles { | |||||
modified, err := l.helper.isTableModified(tx, file.fileNameWithoutExtension()) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if !modified { | |||||
continue | |||||
} | |||||
if err := file.delete(tx, l.helper); err != nil { | |||||
return err | |||||
} | |||||
err = l.helper.whileInsertOnTable(tx, file.fileNameWithoutExtension(), func() error { | |||||
for j, i := range file.insertSQLs { | |||||
if _, err := tx.Exec(i.sql, i.params...); err != nil { | |||||
return &InsertError{ | |||||
Err: err, | |||||
File: file.fileName, | |||||
Index: j, | |||||
SQL: i.sql, | |||||
Params: i.params, | |||||
} | |||||
} | |||||
} | |||||
return nil | |||||
}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return l.helper.afterLoad(l.db) | |||||
} | |||||
// InsertError will be returned if any error happens on database while | |||||
// inserting the record. | |||||
type InsertError struct { | |||||
Err error | |||||
File string | |||||
Index int | |||||
SQL string | |||||
Params []interface{} | |||||
} | |||||
func (e *InsertError) Error() string { | |||||
return fmt.Sprintf( | |||||
"testfixtures: error inserting record: %v, on file: %s, index: %d, sql: %s, params: %v", | |||||
e.Err, | |||||
e.File, | |||||
e.Index, | |||||
e.SQL, | |||||
e.Params, | |||||
) | |||||
} | |||||
func (l *Loader) buildInsertSQLs() error { | |||||
for _, f := range l.fixturesFiles { | |||||
var records interface{} | |||||
if err := yaml.Unmarshal(f.content, &records); err != nil { | |||||
return fmt.Errorf("testfixtures: could not unmarshal YAML: %w", err) | |||||
} | |||||
switch records := records.(type) { | |||||
case []interface{}: | |||||
f.insertSQLs = make([]insertSQL, 0, len(records)) | |||||
for _, record := range records { | |||||
recordMap, ok := record.(map[interface{}]interface{}) | |||||
if !ok { | |||||
return fmt.Errorf("testfixtures: could not cast record: not a map[interface{}]interface{}") | |||||
} | |||||
sql, values, err := l.buildInsertSQL(f, recordMap) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) | |||||
} | |||||
case map[interface{}]interface{}: | |||||
f.insertSQLs = make([]insertSQL, 0, len(records)) | |||||
for _, record := range records { | |||||
recordMap, ok := record.(map[interface{}]interface{}) | |||||
if !ok { | |||||
return fmt.Errorf("testfixtures: could not cast record: not a map[interface{}]interface{}") | |||||
} | |||||
sql, values, err := l.buildInsertSQL(f, recordMap) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) | |||||
} | |||||
default: | |||||
return fmt.Errorf("testfixtures: fixture is not a slice or map") | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func (f *fixtureFile) fileNameWithoutExtension() string { | |||||
return strings.Replace(f.fileName, filepath.Ext(f.fileName), "", 1) | |||||
} | |||||
func (f *fixtureFile) delete(tx *sql.Tx, h helper) error { | |||||
if _, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", h.quoteKeyword(f.fileNameWithoutExtension()))); err != nil { | |||||
return fmt.Errorf(`testfixtures: could not clean table "%s": %w`, f.fileNameWithoutExtension(), err) | |||||
} | |||||
return nil | |||||
} | |||||
func (l *Loader) buildInsertSQL(f *fixtureFile, record map[interface{}]interface{}) (sqlStr string, values []interface{}, err error) { | |||||
var ( | |||||
sqlColumns = make([]string, 0, len(record)) | |||||
sqlValues = make([]string, 0, len(record)) | |||||
i = 1 | |||||
) | |||||
for key, value := range record { | |||||
keyStr, ok := key.(string) | |||||
if !ok { | |||||
err = fmt.Errorf("testfixtures: record map key is not a string") | |||||
return | |||||
} | |||||
sqlColumns = append(sqlColumns, l.helper.quoteKeyword(keyStr)) | |||||
// if string, try convert to SQL or time | |||||
// if map or array, convert to json | |||||
switch v := value.(type) { | |||||
case string: | |||||
if strings.HasPrefix(v, "RAW=") { | |||||
sqlValues = append(sqlValues, strings.TrimPrefix(v, "RAW=")) | |||||
continue | |||||
} | |||||
if t, err := l.tryStrToDate(v); err == nil { | |||||
value = t | |||||
} | |||||
case []interface{}, map[interface{}]interface{}: | |||||
value = recursiveToJSON(v) | |||||
} | |||||
switch l.helper.paramType() { | |||||
case paramTypeDollar: | |||||
sqlValues = append(sqlValues, fmt.Sprintf("$%d", i)) | |||||
case paramTypeQuestion: | |||||
sqlValues = append(sqlValues, "?") | |||||
case paramTypeAtSign: | |||||
sqlValues = append(sqlValues, fmt.Sprintf("@p%d", i)) | |||||
} | |||||
values = append(values, value) | |||||
i++ | |||||
} | |||||
sqlStr = fmt.Sprintf( | |||||
"INSERT INTO %s (%s) VALUES (%s)", | |||||
l.helper.quoteKeyword(f.fileNameWithoutExtension()), | |||||
strings.Join(sqlColumns, ", "), | |||||
strings.Join(sqlValues, ", "), | |||||
) | |||||
return | |||||
} | |||||
func (l *Loader) fixturesFromDir(dir string) ([]*fixtureFile, error) { | |||||
fileinfos, err := ioutil.ReadDir(dir) | |||||
if err != nil { | |||||
return nil, fmt.Errorf(`testfixtures: could not stat directory "%s": %w`, dir, err) | |||||
} | |||||
files := make([]*fixtureFile, 0, len(fileinfos)) | |||||
for _, fileinfo := range fileinfos { | |||||
fileExt := filepath.Ext(fileinfo.Name()) | |||||
if !fileinfo.IsDir() && (fileExt == ".yml" || fileExt == ".yaml") { | |||||
fixture := &fixtureFile{ | |||||
path: path.Join(dir, fileinfo.Name()), | |||||
fileName: fileinfo.Name(), | |||||
} | |||||
fixture.content, err = ioutil.ReadFile(fixture.path) | |||||
if err != nil { | |||||
return nil, fmt.Errorf(`testfixtures: could not read file "%s": %w`, fixture.path, err) | |||||
} | |||||
if err := l.processFileTemplate(fixture); err != nil { | |||||
return nil, err | |||||
} | |||||
files = append(files, fixture) | |||||
} | |||||
} | |||||
return files, nil | |||||
} | |||||
func (l *Loader) fixturesFromFiles(fileNames ...string) ([]*fixtureFile, error) { | |||||
var ( | |||||
fixtureFiles = make([]*fixtureFile, 0, len(fileNames)) | |||||
err error | |||||
) | |||||
for _, f := range fileNames { | |||||
fixture := &fixtureFile{ | |||||
path: f, | |||||
fileName: filepath.Base(f), | |||||
} | |||||
fixture.content, err = ioutil.ReadFile(fixture.path) | |||||
if err != nil { | |||||
return nil, fmt.Errorf(`testfixtures: could not read file "%s": %w`, fixture.path, err) | |||||
} | |||||
if err := l.processFileTemplate(fixture); err != nil { | |||||
return nil, err | |||||
} | |||||
fixtureFiles = append(fixtureFiles, fixture) | |||||
} | |||||
return fixtureFiles, nil | |||||
} | |||||
func (l *Loader) fixturesFromPaths(paths ...string) ([]*fixtureFile, error) { | |||||
fixtureExtractor := func(p string, isDir bool) ([]*fixtureFile, error) { | |||||
if isDir { | |||||
return l.fixturesFromDir(p) | |||||
} | |||||
return l.fixturesFromFiles(p) | |||||
} | |||||
var fixtureFiles []*fixtureFile | |||||
for _, p := range paths { | |||||
f, err := os.Stat(p) | |||||
if err != nil { | |||||
return nil, fmt.Errorf(`testfixtures: could not stat path "%s": %w`, p, err) | |||||
} | |||||
fixtures, err := fixtureExtractor(p, f.IsDir()) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
fixtureFiles = append(fixtureFiles, fixtures...) | |||||
} | |||||
return fixtureFiles, nil | |||||
} | |||||
func (l *Loader) processFileTemplate(f *fixtureFile) error { | |||||
if !l.template { | |||||
return nil | |||||
} | |||||
t := template.New(""). | |||||
Funcs(l.templateFuncs). | |||||
Delims(l.templateLeftDelim, l.templateRightDelim). | |||||
Option(l.templateOptions...) | |||||
t, err := t.Parse(string(f.content)) | |||||
if err != nil { | |||||
return fmt.Errorf(`textfixtures: error on parsing template in %s: %w`, f.fileName, err) | |||||
} | |||||
var buffer bytes.Buffer | |||||
if err := t.Execute(&buffer, l.templateData); err != nil { | |||||
return fmt.Errorf(`textfixtures: error on executing template in %s: %w`, f.fileName, err) | |||||
} | |||||
f.content = buffer.Bytes() | |||||
return nil | |||||
} |
package testfixtures | |||||
import ( | |||||
"fmt" | |||||
"time" | |||||
) | |||||
var timeFormats = [...]string{ | |||||
"2006-01-02", | |||||
"2006-01-02 15:04", | |||||
"2006-01-02 15:04:05", | |||||
"20060102", | |||||
"20060102 15:04", | |||||
"20060102 15:04:05", | |||||
"02/01/2006", | |||||
"02/01/2006 15:04", | |||||
"02/01/2006 15:04:05", | |||||
"2006-01-02T15:04-07:00", | |||||
"2006-01-02T15:04:05-07:00", | |||||
"2006-01-02T15:04:05Z07:00", | |||||
"2006-01-02 15:04:05Z07:00", | |||||
"2006-01-02T15:04:05Z0700", | |||||
"2006-01-02 15:04:05Z0700", | |||||
"2006-01-02T15:04:05Z07", | |||||
"2006-01-02 15:04:05Z07", | |||||
"2006-01-02 15:04:05 MST", | |||||
} | |||||
func (l *Loader) tryStrToDate(s string) (time.Time, error) { | |||||
loc := l.location | |||||
if loc == nil { | |||||
loc = time.Local | |||||
} | |||||
for _, f := range timeFormats { | |||||
t, err := time.ParseInLocation(f, s, loc) | |||||
if err != nil { | |||||
continue | |||||
} | |||||
return t, nil | |||||
} | |||||
return time.Time{}, fmt.Errorf(`testfixtures: could not convert string "%s" to time`, s) | |||||
} |
- 1.9.x | - 1.9.x | ||||
- 1.10.x | - 1.10.x | ||||
- 1.11.x | - 1.11.x | ||||
- 1.12.x | |||||
- 1.13.x | |||||
- master | - master | ||||
before_install: | before_install: | ||||
- $HOME/gopath/bin/goveralls -repotoken 3qJVUE0iQwqnCbmNcDsjYu1nh4J4KIFXx | - $HOME/gopath/bin/goveralls -repotoken 3qJVUE0iQwqnCbmNcDsjYu1nh4J4KIFXx | ||||
- go test -race -v . -tags "" | - go test -race -v . -tags "" | ||||
- go test -race -v . -tags "libsqlite3" | - go test -race -v . -tags "libsqlite3" | ||||
- go test -race -v . -tags "sqlite_allow_uri_authority sqlite_app_armor sqlite_foreign_keys sqlite_fts5 sqlite_icu sqlite_introspect sqlite_json sqlite_secure_delete sqlite_see sqlite_stat4 sqlite_trace sqlite_userauth sqlite_vacuum_incr sqlite_vtable sqlite_unlock_notify" | |||||
- go test -race -v . -tags "sqlite_vacuum_full" | |||||
- go test -race -v . -tags "sqlite_allow_uri_authority sqlite_app_armor sqlite_foreign_keys sqlite_fts5 sqlite_icu sqlite_introspect sqlite_json sqlite_preupdate_hook sqlite_secure_delete sqlite_see sqlite_stat4 sqlite_trace sqlite_userauth sqlite_vacuum_incr sqlite_vtable sqlite_unlock_notify" | |||||
- go test -race -v . -tags "sqlite_vacuum_full" |
[![GoDoc Reference](https://godoc.org/github.com/mattn/go-sqlite3?status.svg)](http://godoc.org/github.com/mattn/go-sqlite3) | [![GoDoc Reference](https://godoc.org/github.com/mattn/go-sqlite3?status.svg)](http://godoc.org/github.com/mattn/go-sqlite3) | ||||
[![Build Status](https://travis-ci.org/mattn/go-sqlite3.svg?branch=master)](https://travis-ci.org/mattn/go-sqlite3) | [![Build Status](https://travis-ci.org/mattn/go-sqlite3.svg?branch=master)](https://travis-ci.org/mattn/go-sqlite3) | ||||
[![Financial Contributors on Open Collective](https://opencollective.com/mattn-go-sqlite3/all/badge.svg?label=financial+contributors)](https://opencollective.com/mattn-go-sqlite3) | |||||
[![Coverage Status](https://coveralls.io/repos/mattn/go-sqlite3/badge.svg?branch=master)](https://coveralls.io/r/mattn/go-sqlite3?branch=master) | [![Coverage Status](https://coveralls.io/repos/mattn/go-sqlite3/badge.svg?branch=master)](https://coveralls.io/r/mattn/go-sqlite3?branch=master) | ||||
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-sqlite3)](https://goreportcard.com/report/github.com/mattn/go-sqlite3) | [![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-sqlite3)](https://goreportcard.com/report/github.com/mattn/go-sqlite3) | ||||
### Overview | ### Overview | ||||
- [go-sqlite3](#go-sqlite3) | |||||
- [Description](#description) | |||||
- [Overview](#overview) | |||||
- [Installation](#installation) | - [Installation](#installation) | ||||
- [API Reference](#api-reference) | - [API Reference](#api-reference) | ||||
- [Connection String](#connection-string) | - [Connection String](#connection-string) | ||||
- [DSN Examples](#dsn-examples) | |||||
- [Features](#features) | - [Features](#features) | ||||
- [Usage](#usage) | |||||
- [Feature / Extension List](#feature--extension-list) | |||||
- [Compilation](#compilation) | - [Compilation](#compilation) | ||||
- [Android](#android) | - [Android](#android) | ||||
- [ARM](#arm) | |||||
- [Cross Compile](#cross-compile) | |||||
- [Google Cloud Platform](#google-cloud-platform) | |||||
- [ARM](#arm) | |||||
- [Cross Compile](#cross-compile) | |||||
- [Google Cloud Platform](#google-cloud-platform) | |||||
- [Linux](#linux) | - [Linux](#linux) | ||||
- [Alpine](#alpine) | - [Alpine](#alpine) | ||||
- [Fedora](#fedora) | - [Fedora](#fedora) | ||||
- [Errors](#errors) | - [Errors](#errors) | ||||
- [User Authentication](#user-authentication) | - [User Authentication](#user-authentication) | ||||
- [Compile](#compile) | - [Compile](#compile) | ||||
- [Usage](#usage) | |||||
- [Usage](#usage-1) | |||||
- [Create protected database](#create-protected-database) | |||||
- [Password Encoding](#password-encoding) | |||||
- [Available Encoders](#available-encoders) | |||||
- [Restrictions](#restrictions) | |||||
- [Support](#support) | |||||
- [User Management](#user-management) | |||||
- [SQL](#sql) | |||||
- [Examples](#examples) | |||||
- [*SQLiteConn](#sqliteconn) | |||||
- [Attached database](#attached-database) | |||||
- [Extensions](#extensions) | - [Extensions](#extensions) | ||||
- [Spatialite](#spatialite) | - [Spatialite](#spatialite) | ||||
- [FAQ](#faq) | - [FAQ](#faq) | ||||
- [License](#license) | - [License](#license) | ||||
- [Author](#author) | |||||
# Installation | # Installation | ||||
| International Components for Unicode | sqlite_icu | This option causes the International Components for Unicode or "ICU" extension to SQLite to be added to the build | | | International Components for Unicode | sqlite_icu | This option causes the International Components for Unicode or "ICU" extension to SQLite to be added to the build | | ||||
| Introspect PRAGMAS | sqlite_introspect | This option adds some extra PRAGMA statements. <ul><li>PRAGMA function_list</li><li>PRAGMA module_list</li><li>PRAGMA pragma_list</li></ul> | | | Introspect PRAGMAS | sqlite_introspect | This option adds some extra PRAGMA statements. <ul><li>PRAGMA function_list</li><li>PRAGMA module_list</li><li>PRAGMA pragma_list</li></ul> | | ||||
| JSON SQL Functions | sqlite_json | When this option is defined in the amalgamation, the JSON SQL functions are added to the build automatically | | | JSON SQL Functions | sqlite_json | When this option is defined in the amalgamation, the JSON SQL functions are added to the build automatically | | ||||
| Pre Update Hook | sqlite_preupdate_hook | Registers a callback function that is invoked prior to each INSERT, UPDATE, and DELETE operation on a database table. | | |||||
| Secure Delete | sqlite_secure_delete | This compile-time option changes the default setting of the secure_delete pragma.<br><br>When this option is not used, secure_delete defaults to off. When this option is present, secure_delete defaults to on.<br><br>The secure_delete setting causes deleted content to be overwritten with zeros. There is a small performance penalty since additional I/O must occur.<br><br>On the other hand, secure_delete can prevent fragments of sensitive information from lingering in unused parts of the database file after it has been deleted. See the documentation on the secure_delete pragma for additional information | | | Secure Delete | sqlite_secure_delete | This compile-time option changes the default setting of the secure_delete pragma.<br><br>When this option is not used, secure_delete defaults to off. When this option is present, secure_delete defaults to on.<br><br>The secure_delete setting causes deleted content to be overwritten with zeros. There is a small performance penalty since additional I/O must occur.<br><br>On the other hand, secure_delete can prevent fragments of sensitive information from lingering in unused parts of the database file after it has been deleted. See the documentation on the secure_delete pragma for additional information | | ||||
| Secure Delete (FAST) | sqlite_secure_delete_fast | For more information see [PRAGMA secure_delete](https://www.sqlite.org/pragma.html#pragma_secure_delete) | | | Secure Delete (FAST) | sqlite_secure_delete_fast | For more information see [PRAGMA secure_delete](https://www.sqlite.org/pragma.html#pragma_secure_delete) | | ||||
| Tracing / Debug | sqlite_trace | Activate trace functions | | | Tracing / Debug | sqlite_trace | Activate trace functions | | ||||
More information see [#209](https://github.com/mattn/go-sqlite3/issues/209) | More information see [#209](https://github.com/mattn/go-sqlite3/issues/209) | ||||
## Contributors | |||||
### Code Contributors | |||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. | |||||
<a href="https://github.com/mattn/go-sqlite3/graphs/contributors"><img src="https://opencollective.com/mattn-go-sqlite3/contributors.svg?width=890&button=false" /></a> | |||||
### Financial Contributors | |||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/mattn-go-sqlite3/contribute)] | |||||
#### Individuals | |||||
<a href="https://opencollective.com/mattn-go-sqlite3"><img src="https://opencollective.com/mattn-go-sqlite3/individuals.svg?width=890"></a> | |||||
#### Organizations | |||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/mattn-go-sqlite3/contribute)] | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/0/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/0/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/1/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/1/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/2/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/2/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/3/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/3/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/4/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/4/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/5/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/5/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/6/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/6/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/7/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/7/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/8/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/8/avatar.svg"></a> | |||||
<a href="https://opencollective.com/mattn-go-sqlite3/organization/9/website"><img src="https://opencollective.com/mattn-go-sqlite3/organization/9/avatar.svg"></a> | |||||
# License | # License | ||||
MIT: http://mattn.mit-license.org/2018 | MIT: http://mattn.mit-license.org/2018 |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
} | } | ||||
// Backup make backup from src to dest. | // Backup make backup from src to dest. | ||||
func (c *SQLiteConn) Backup(dest string, conn *SQLiteConn, src string) (*SQLiteBackup, error) { | |||||
func (destConn *SQLiteConn) Backup(dest string, srcConn *SQLiteConn, src string) (*SQLiteBackup, error) { | |||||
destptr := C.CString(dest) | destptr := C.CString(dest) | ||||
defer C.free(unsafe.Pointer(destptr)) | defer C.free(unsafe.Pointer(destptr)) | ||||
srcptr := C.CString(src) | srcptr := C.CString(src) | ||||
defer C.free(unsafe.Pointer(srcptr)) | defer C.free(unsafe.Pointer(srcptr)) | ||||
if b := C.sqlite3_backup_init(c.db, destptr, conn.db, srcptr); b != nil { | |||||
if b := C.sqlite3_backup_init(destConn.db, destptr, srcConn.db, srcptr); b != nil { | |||||
bb := &SQLiteBackup{b: b} | bb := &SQLiteBackup{b: b} | ||||
runtime.SetFinalizer(bb, (*SQLiteBackup).Finish) | runtime.SetFinalizer(bb, (*SQLiteBackup).Finish) | ||||
return bb, nil | return bb, nil | ||||
} | } | ||||
return nil, c.lastError() | |||||
return nil, destConn.lastError() | |||||
} | } | ||||
// Step to backs up for one step. Calls the underlying `sqlite3_backup_step` | // Step to backs up for one step. Calls the underlying `sqlite3_backup_step` |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
return callback(op, C.GoString(arg1), C.GoString(arg2), C.GoString(arg3)) | return callback(op, C.GoString(arg1), C.GoString(arg2), C.GoString(arg3)) | ||||
} | } | ||||
// Use handles to avoid passing Go pointers to C. | |||||
//export preUpdateHookTrampoline | |||||
func preUpdateHookTrampoline(handle uintptr, dbHandle uintptr, op int, db *C.char, table *C.char, oldrowid int64, newrowid int64) { | |||||
hval := lookupHandleVal(handle) | |||||
data := SQLitePreUpdateData{ | |||||
Conn: hval.db, | |||||
Op: op, | |||||
DatabaseName: C.GoString(db), | |||||
TableName: C.GoString(table), | |||||
OldRowID: oldrowid, | |||||
NewRowID: newrowid, | |||||
} | |||||
callback := hval.val.(func(SQLitePreUpdateData)) | |||||
callback(data) | |||||
} | |||||
// Use handles to avoid passing Go pointers to C. | |||||
type handleVal struct { | type handleVal struct { | ||||
db *SQLiteConn | db *SQLiteConn | ||||
val interface{} | val interface{} | ||||
return i | return i | ||||
} | } | ||||
func lookupHandle(handle uintptr) interface{} { | |||||
func lookupHandleVal(handle uintptr) handleVal { | |||||
handleLock.Lock() | handleLock.Lock() | ||||
defer handleLock.Unlock() | defer handleLock.Unlock() | ||||
r, ok := handleVals[handle] | r, ok := handleVals[handle] | ||||
panic("invalid handle") | panic("invalid handle") | ||||
} | } | ||||
} | } | ||||
return r.val | |||||
return r | |||||
} | |||||
func lookupHandle(handle uintptr) interface{} { | |||||
return lookupHandleVal(handle).val | |||||
} | } | ||||
func deleteHandles(db *SQLiteConn) { | func deleteHandles(db *SQLiteConn) { |
// Extracted from Go database/sql source code | |||||
// Copyright 2011 The Go Authors. All rights reserved. | |||||
// Use of this source code is governed by a BSD-style | |||||
// license that can be found in the LICENSE file. | |||||
// Type conversions for Scan. | |||||
package sqlite3 | |||||
import ( | |||||
"database/sql" | |||||
"database/sql/driver" | |||||
"errors" | |||||
"fmt" | |||||
"reflect" | |||||
"strconv" | |||||
"time" | |||||
) | |||||
var errNilPtr = errors.New("destination pointer is nil") // embedded in descriptive error | |||||
// convertAssign copies to dest the value in src, converting it if possible. | |||||
// An error is returned if the copy would result in loss of information. | |||||
// dest should be a pointer type. | |||||
func convertAssign(dest, src interface{}) error { | |||||
// Common cases, without reflect. | |||||
switch s := src.(type) { | |||||
case string: | |||||
switch d := dest.(type) { | |||||
case *string: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = s | |||||
return nil | |||||
case *[]byte: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = []byte(s) | |||||
return nil | |||||
case *sql.RawBytes: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = append((*d)[:0], s...) | |||||
return nil | |||||
} | |||||
case []byte: | |||||
switch d := dest.(type) { | |||||
case *string: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = string(s) | |||||
return nil | |||||
case *interface{}: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = cloneBytes(s) | |||||
return nil | |||||
case *[]byte: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = cloneBytes(s) | |||||
return nil | |||||
case *sql.RawBytes: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = s | |||||
return nil | |||||
} | |||||
case time.Time: | |||||
switch d := dest.(type) { | |||||
case *time.Time: | |||||
*d = s | |||||
return nil | |||||
case *string: | |||||
*d = s.Format(time.RFC3339Nano) | |||||
return nil | |||||
case *[]byte: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = []byte(s.Format(time.RFC3339Nano)) | |||||
return nil | |||||
case *sql.RawBytes: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = s.AppendFormat((*d)[:0], time.RFC3339Nano) | |||||
return nil | |||||
} | |||||
case nil: | |||||
switch d := dest.(type) { | |||||
case *interface{}: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = nil | |||||
return nil | |||||
case *[]byte: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = nil | |||||
return nil | |||||
case *sql.RawBytes: | |||||
if d == nil { | |||||
return errNilPtr | |||||
} | |||||
*d = nil | |||||
return nil | |||||
} | |||||
} | |||||
var sv reflect.Value | |||||
switch d := dest.(type) { | |||||
case *string: | |||||
sv = reflect.ValueOf(src) | |||||
switch sv.Kind() { | |||||
case reflect.Bool, | |||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, | |||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, | |||||
reflect.Float32, reflect.Float64: | |||||
*d = asString(src) | |||||
return nil | |||||
} | |||||
case *[]byte: | |||||
sv = reflect.ValueOf(src) | |||||
if b, ok := asBytes(nil, sv); ok { | |||||
*d = b | |||||
return nil | |||||
} | |||||
case *sql.RawBytes: | |||||
sv = reflect.ValueOf(src) | |||||
if b, ok := asBytes([]byte(*d)[:0], sv); ok { | |||||
*d = sql.RawBytes(b) | |||||
return nil | |||||
} | |||||
case *bool: | |||||
bv, err := driver.Bool.ConvertValue(src) | |||||
if err == nil { | |||||
*d = bv.(bool) | |||||
} | |||||
return err | |||||
case *interface{}: | |||||
*d = src | |||||
return nil | |||||
} | |||||
if scanner, ok := dest.(sql.Scanner); ok { | |||||
return scanner.Scan(src) | |||||
} | |||||
dpv := reflect.ValueOf(dest) | |||||
if dpv.Kind() != reflect.Ptr { | |||||
return errors.New("destination not a pointer") | |||||
} | |||||
if dpv.IsNil() { | |||||
return errNilPtr | |||||
} | |||||
if !sv.IsValid() { | |||||
sv = reflect.ValueOf(src) | |||||
} | |||||
dv := reflect.Indirect(dpv) | |||||
if sv.IsValid() && sv.Type().AssignableTo(dv.Type()) { | |||||
switch b := src.(type) { | |||||
case []byte: | |||||
dv.Set(reflect.ValueOf(cloneBytes(b))) | |||||
default: | |||||
dv.Set(sv) | |||||
} | |||||
return nil | |||||
} | |||||
if dv.Kind() == sv.Kind() && sv.Type().ConvertibleTo(dv.Type()) { | |||||
dv.Set(sv.Convert(dv.Type())) | |||||
return nil | |||||
} | |||||
// The following conversions use a string value as an intermediate representation | |||||
// to convert between various numeric types. | |||||
// | |||||
// This also allows scanning into user defined types such as "type Int int64". | |||||
// For symmetry, also check for string destination types. | |||||
switch dv.Kind() { | |||||
case reflect.Ptr: | |||||
if src == nil { | |||||
dv.Set(reflect.Zero(dv.Type())) | |||||
return nil | |||||
} | |||||
dv.Set(reflect.New(dv.Type().Elem())) | |||||
return convertAssign(dv.Interface(), src) | |||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |||||
s := asString(src) | |||||
i64, err := strconv.ParseInt(s, 10, dv.Type().Bits()) | |||||
if err != nil { | |||||
err = strconvErr(err) | |||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) | |||||
} | |||||
dv.SetInt(i64) | |||||
return nil | |||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |||||
s := asString(src) | |||||
u64, err := strconv.ParseUint(s, 10, dv.Type().Bits()) | |||||
if err != nil { | |||||
err = strconvErr(err) | |||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) | |||||
} | |||||
dv.SetUint(u64) | |||||
return nil | |||||
case reflect.Float32, reflect.Float64: | |||||
s := asString(src) | |||||
f64, err := strconv.ParseFloat(s, dv.Type().Bits()) | |||||
if err != nil { | |||||
err = strconvErr(err) | |||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) | |||||
} | |||||
dv.SetFloat(f64) | |||||
return nil | |||||
case reflect.String: | |||||
switch v := src.(type) { | |||||
case string: | |||||
dv.SetString(v) | |||||
return nil | |||||
case []byte: | |||||
dv.SetString(string(v)) | |||||
return nil | |||||
} | |||||
} | |||||
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest) | |||||
} | |||||
func strconvErr(err error) error { | |||||
if ne, ok := err.(*strconv.NumError); ok { | |||||
return ne.Err | |||||
} | |||||
return err | |||||
} | |||||
func cloneBytes(b []byte) []byte { | |||||
if b == nil { | |||||
return nil | |||||
} | |||||
c := make([]byte, len(b)) | |||||
copy(c, b) | |||||
return c | |||||
} | |||||
func asString(src interface{}) string { | |||||
switch v := src.(type) { | |||||
case string: | |||||
return v | |||||
case []byte: | |||||
return string(v) | |||||
} | |||||
rv := reflect.ValueOf(src) | |||||
switch rv.Kind() { | |||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |||||
return strconv.FormatInt(rv.Int(), 10) | |||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |||||
return strconv.FormatUint(rv.Uint(), 10) | |||||
case reflect.Float64: | |||||
return strconv.FormatFloat(rv.Float(), 'g', -1, 64) | |||||
case reflect.Float32: | |||||
return strconv.FormatFloat(rv.Float(), 'g', -1, 32) | |||||
case reflect.Bool: | |||||
return strconv.FormatBool(rv.Bool()) | |||||
} | |||||
return fmt.Sprintf("%v", src) | |||||
} | |||||
func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) { | |||||
switch rv.Kind() { | |||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |||||
return strconv.AppendInt(buf, rv.Int(), 10), true | |||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |||||
return strconv.AppendUint(buf, rv.Uint(), 10), true | |||||
case reflect.Float32: | |||||
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 32), true | |||||
case reflect.Float64: | |||||
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 64), true | |||||
case reflect.Bool: | |||||
return strconv.AppendBool(buf, rv.Bool()), true | |||||
case reflect.String: | |||||
s := rv.String() | |||||
return append(buf, s...), true | |||||
} | |||||
return | |||||
} |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
package sqlite3 | package sqlite3 | ||||
/* | |||||
#ifndef USE_LIBSQLITE3 | |||||
#include <sqlite3-binding.h> | |||||
#else | |||||
#include <sqlite3.h> | |||||
#endif | |||||
*/ | |||||
import "C" | import "C" | ||||
import "syscall" | |||||
// ErrNo inherit errno. | // ErrNo inherit errno. | ||||
type ErrNo int | type ErrNo int | ||||
type Error struct { | type Error struct { | ||||
Code ErrNo /* The error code returned by SQLite */ | Code ErrNo /* The error code returned by SQLite */ | ||||
ExtendedCode ErrNoExtended /* The extended error code returned by SQLite */ | ExtendedCode ErrNoExtended /* The extended error code returned by SQLite */ | ||||
SystemErrno syscall.Errno /* The system errno returned by the OS through SQLite, if applicable */ | |||||
err string /* The error string returned by sqlite3_errmsg(), | err string /* The error string returned by sqlite3_errmsg(), | ||||
this usually contains more specific details. */ | this usually contains more specific details. */ | ||||
} | } | ||||
} | } | ||||
func (err Error) Error() string { | func (err Error) Error() string { | ||||
var str string | |||||
if err.err != "" { | if err.err != "" { | ||||
return err.err | |||||
str = err.err | |||||
} else { | |||||
str = C.GoString(C.sqlite3_errstr(C.int(err.Code))) | |||||
} | } | ||||
return errorString(err) | |||||
if err.SystemErrno != 0 { | |||||
str += ": " + err.SystemErrno.Error() | |||||
} | |||||
return str | |||||
} | } | ||||
// result codes from http://www.sqlite.org/c3ref/c_abort_rollback.html | // result codes from http://www.sqlite.org/c3ref/c_abort_rollback.html |
** [sqlite3_libversion_number()], [sqlite3_sourceid()], | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], | ||||
** [sqlite_version()] and [sqlite_source_id()]. | ** [sqlite_version()] and [sqlite_source_id()]. | ||||
*/ | */ | ||||
#define SQLITE_VERSION "3.29.0" | |||||
#define SQLITE_VERSION_NUMBER 3029000 | |||||
#define SQLITE_SOURCE_ID "2019-07-10 17:32:03 fc82b73eaac8b36950e527f12c4b5dc1e147e6f4ad2217ae43ad82882a88bfa6" | |||||
#define SQLITE_VERSION "3.30.1" | |||||
#define SQLITE_VERSION_NUMBER 3030001 | |||||
#define SQLITE_SOURCE_ID "2019-10-10 20:19:45 18db032d058f1436ce3dea84081f4ee5a0f2259ad97301d43c426bc7f3df1b0b" | |||||
/* | /* | ||||
** CAPI3REF: Run-Time Library Version Numbers | ** CAPI3REF: Run-Time Library Version Numbers | ||||
** following this call. The second parameter may be a NULL pointer, in | ** following this call. The second parameter may be a NULL pointer, in | ||||
** which case the trigger setting is not reported back. </dd> | ** which case the trigger setting is not reported back. </dd> | ||||
** | ** | ||||
** [[SQLITE_DBCONFIG_ENABLE_VIEW]] | |||||
** <dt>SQLITE_DBCONFIG_ENABLE_VIEW</dt> | |||||
** <dd> ^This option is used to enable or disable [CREATE VIEW | views]. | |||||
** There should be two additional arguments. | |||||
** The first argument is an integer which is 0 to disable views, | |||||
** positive to enable views or negative to leave the setting unchanged. | |||||
** The second parameter is a pointer to an integer into which | |||||
** is written 0 or 1 to indicate whether views are disabled or enabled | |||||
** following this call. The second parameter may be a NULL pointer, in | |||||
** which case the view setting is not reported back. </dd> | |||||
** | |||||
** [[SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER]] | ** [[SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER]] | ||||
** <dt>SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER</dt> | ** <dt>SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER</dt> | ||||
** <dd> ^This option is used to enable or disable the | ** <dd> ^This option is used to enable or disable the | ||||
#define SQLITE_DBCONFIG_LEGACY_ALTER_TABLE 1012 /* int int* */ | #define SQLITE_DBCONFIG_LEGACY_ALTER_TABLE 1012 /* int int* */ | ||||
#define SQLITE_DBCONFIG_DQS_DML 1013 /* int int* */ | #define SQLITE_DBCONFIG_DQS_DML 1013 /* int int* */ | ||||
#define SQLITE_DBCONFIG_DQS_DDL 1014 /* int int* */ | #define SQLITE_DBCONFIG_DQS_DDL 1014 /* int int* */ | ||||
#define SQLITE_DBCONFIG_MAX 1014 /* Largest DBCONFIG */ | |||||
#define SQLITE_DBCONFIG_ENABLE_VIEW 1015 /* int int* */ | |||||
#define SQLITE_DBCONFIG_MAX 1015 /* Largest DBCONFIG */ | |||||
/* | /* | ||||
** CAPI3REF: Enable Or Disable Extended Result Codes | ** CAPI3REF: Enable Or Disable Extended Result Codes | ||||
** ^The specific value of WHERE-clause [parameter] might influence the | ** ^The specific value of WHERE-clause [parameter] might influence the | ||||
** choice of query plan if the parameter is the left-hand side of a [LIKE] | ** choice of query plan if the parameter is the left-hand side of a [LIKE] | ||||
** or [GLOB] operator or if the parameter is compared to an indexed column | ** or [GLOB] operator or if the parameter is compared to an indexed column | ||||
** and the [SQLITE_ENABLE_STAT3] compile-time option is enabled. | |||||
** and the [SQLITE_ENABLE_STAT4] compile-time option is enabled. | |||||
** </li> | ** </li> | ||||
** </ol> | ** </ol> | ||||
** | ** | ||||
** perform additional optimizations on deterministic functions, so use | ** perform additional optimizations on deterministic functions, so use | ||||
** of the [SQLITE_DETERMINISTIC] flag is recommended where possible. | ** of the [SQLITE_DETERMINISTIC] flag is recommended where possible. | ||||
** | ** | ||||
** ^The fourth parameter may also optionally include the [SQLITE_DIRECTONLY] | |||||
** flag, which if present prevents the function from being invoked from | |||||
** within VIEWs or TRIGGERs. For security reasons, the [SQLITE_DIRECTONLY] | |||||
** flag is recommended for any application-defined SQL function that has | |||||
** side-effects. | |||||
** | |||||
** ^(The fifth parameter is an arbitrary pointer. The implementation of the | ** ^(The fifth parameter is an arbitrary pointer. The implementation of the | ||||
** function can gain access to this pointer using [sqlite3_user_data()].)^ | ** function can gain access to this pointer using [sqlite3_user_data()].)^ | ||||
** | ** | ||||
** [SQLITE_UTF8 | preferred text encoding] as the fourth argument | ** [SQLITE_UTF8 | preferred text encoding] as the fourth argument | ||||
** to [sqlite3_create_function()], [sqlite3_create_function16()], or | ** to [sqlite3_create_function()], [sqlite3_create_function16()], or | ||||
** [sqlite3_create_function_v2()]. | ** [sqlite3_create_function_v2()]. | ||||
** | |||||
** The SQLITE_DETERMINISTIC flag means that the new function will always | |||||
** maps the same inputs into the same output. The abs() function is | |||||
** deterministic, for example, but randomblob() is not. | |||||
** | |||||
** The SQLITE_DIRECTONLY flag means that the function may only be invoked | |||||
** from top-level SQL, and cannot be used in VIEWs or TRIGGERs. This is | |||||
** a security feature which is recommended for all | |||||
** [application-defined SQL functions] that have side-effects. This flag | |||||
** prevents an attacker from adding triggers and views to a schema then | |||||
** tricking a high-privilege application into causing unintended side-effects | |||||
** while performing ordinary queries. | |||||
** | |||||
** The SQLITE_SUBTYPE flag indicates to SQLite that a function may call | |||||
** [sqlite3_value_subtype()] to inspect the sub-types of its arguments. | |||||
** Specifying this flag makes no difference for scalar or aggregate user | |||||
** functions. However, if it is not specified for a user-defined window | |||||
** function, then any sub-types belonging to arguments passed to the window | |||||
** function may be discarded before the window function is called (i.e. | |||||
** sqlite3_value_subtype() will always return 0). | |||||
*/ | */ | ||||
#define SQLITE_DETERMINISTIC 0x800 | |||||
#define SQLITE_DETERMINISTIC 0x000000800 | |||||
#define SQLITE_DIRECTONLY 0x000080000 | |||||
#define SQLITE_SUBTYPE 0x000100000 | |||||
/* | /* | ||||
** CAPI3REF: Deprecated Functions | ** CAPI3REF: Deprecated Functions | ||||
** ^The sqlite3_create_module() | ** ^The sqlite3_create_module() | ||||
** interface is equivalent to sqlite3_create_module_v2() with a NULL | ** interface is equivalent to sqlite3_create_module_v2() with a NULL | ||||
** destructor. | ** destructor. | ||||
** | |||||
** ^If the third parameter (the pointer to the sqlite3_module object) is | |||||
** NULL then no new module is create and any existing modules with the | |||||
** same name are dropped. | |||||
** | |||||
** See also: [sqlite3_drop_modules()] | |||||
*/ | */ | ||||
SQLITE_API int sqlite3_create_module( | SQLITE_API int sqlite3_create_module( | ||||
sqlite3 *db, /* SQLite connection to register module with */ | sqlite3 *db, /* SQLite connection to register module with */ | ||||
void(*xDestroy)(void*) /* Module destructor function */ | void(*xDestroy)(void*) /* Module destructor function */ | ||||
); | ); | ||||
/* | |||||
** CAPI3REF: Remove Unnecessary Virtual Table Implementations | |||||
** METHOD: sqlite3 | |||||
** | |||||
** ^The sqlite3_drop_modules(D,L) interface removes all virtual | |||||
** table modules from database connection D except those named on list L. | |||||
** The L parameter must be either NULL or a pointer to an array of pointers | |||||
** to strings where the array is terminated by a single NULL pointer. | |||||
** ^If the L parameter is NULL, then all virtual table modules are removed. | |||||
** | |||||
** See also: [sqlite3_create_module()] | |||||
*/ | |||||
SQLITE_API int sqlite3_drop_modules( | |||||
sqlite3 *db, /* Remove modules from this connection */ | |||||
const char **azKeep /* Except, do not remove the ones named here */ | |||||
); | |||||
/* | /* | ||||
** CAPI3REF: Virtual Table Instance Object | ** CAPI3REF: Virtual Table Instance Object | ||||
** KEYWORDS: sqlite3_vtab | ** KEYWORDS: sqlite3_vtab | ||||
#define SQLITE_TESTCTRL_FIRST 5 | #define SQLITE_TESTCTRL_FIRST 5 | ||||
#define SQLITE_TESTCTRL_PRNG_SAVE 5 | #define SQLITE_TESTCTRL_PRNG_SAVE 5 | ||||
#define SQLITE_TESTCTRL_PRNG_RESTORE 6 | #define SQLITE_TESTCTRL_PRNG_RESTORE 6 | ||||
#define SQLITE_TESTCTRL_PRNG_RESET 7 | |||||
#define SQLITE_TESTCTRL_PRNG_RESET 7 /* NOT USED */ | |||||
#define SQLITE_TESTCTRL_BITVEC_TEST 8 | #define SQLITE_TESTCTRL_BITVEC_TEST 8 | ||||
#define SQLITE_TESTCTRL_FAULT_INSTALL 9 | #define SQLITE_TESTCTRL_FAULT_INSTALL 9 | ||||
#define SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS 10 | #define SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS 10 | ||||
#define SQLITE_TESTCTRL_IMPOSTER 25 | #define SQLITE_TESTCTRL_IMPOSTER 25 | ||||
#define SQLITE_TESTCTRL_PARSER_COVERAGE 26 | #define SQLITE_TESTCTRL_PARSER_COVERAGE 26 | ||||
#define SQLITE_TESTCTRL_RESULT_INTREAL 27 | #define SQLITE_TESTCTRL_RESULT_INTREAL 27 | ||||
#define SQLITE_TESTCTRL_LAST 27 /* Largest TESTCTRL */ | |||||
#define SQLITE_TESTCTRL_PRNG_SEED 28 | |||||
#define SQLITE_TESTCTRL_EXTRA_SCHEMA_CHECKS 29 | |||||
#define SQLITE_TESTCTRL_LAST 29 /* Largest TESTCTRL */ | |||||
/* | /* | ||||
** CAPI3REF: SQL Keyword Checking | ** CAPI3REF: SQL Keyword Checking |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
return sqlite3_limit(db, limitId, newLimit); | return sqlite3_limit(db, limitId, newLimit); | ||||
#endif | #endif | ||||
} | } | ||||
#if SQLITE_VERSION_NUMBER < 3012000 | |||||
static int sqlite3_system_errno(sqlite3 *db) { | |||||
return 0; | |||||
} | |||||
#endif | |||||
*/ | */ | ||||
import "C" | import "C" | ||||
import ( | import ( | ||||
"strconv" | "strconv" | ||||
"strings" | "strings" | ||||
"sync" | "sync" | ||||
"syscall" | |||||
"time" | "time" | ||||
"unsafe" | "unsafe" | ||||
) | ) | ||||
decltype []string | decltype []string | ||||
cls bool | cls bool | ||||
closed bool | closed bool | ||||
done chan struct{} | |||||
ctx context.Context // no better alternative to pass context into Next() method | |||||
} | } | ||||
type functionInfo struct { | type functionInfo struct { | ||||
return lastError(c.db) | return lastError(c.db) | ||||
} | } | ||||
// Note: may be called with db == nil | |||||
func lastError(db *C.sqlite3) error { | func lastError(db *C.sqlite3) error { | ||||
rv := C.sqlite3_errcode(db) | |||||
rv := C.sqlite3_errcode(db) // returns SQLITE_NOMEM if db == nil | |||||
if rv == C.SQLITE_OK { | if rv == C.SQLITE_OK { | ||||
return nil | return nil | ||||
} | } | ||||
extrv := C.sqlite3_extended_errcode(db) // returns SQLITE_NOMEM if db == nil | |||||
errStr := C.GoString(C.sqlite3_errmsg(db)) // returns "out of memory" if db == nil | |||||
// https://www.sqlite.org/c3ref/system_errno.html | |||||
// sqlite3_system_errno is only meaningful if the error code was SQLITE_CANTOPEN, | |||||
// or it was SQLITE_IOERR and the extended code was not SQLITE_IOERR_NOMEM | |||||
var systemErrno syscall.Errno | |||||
if rv == C.SQLITE_CANTOPEN || (rv == C.SQLITE_IOERR && extrv != C.SQLITE_IOERR_NOMEM) { | |||||
systemErrno = syscall.Errno(C.sqlite3_system_errno(db)) | |||||
} | |||||
return Error{ | return Error{ | ||||
Code: ErrNo(rv), | Code: ErrNo(rv), | ||||
ExtendedCode: ErrNoExtended(C.sqlite3_extended_errcode(db)), | |||||
err: C.GoString(C.sqlite3_errmsg(db)), | |||||
ExtendedCode: ErrNoExtended(extrv), | |||||
SystemErrno: systemErrno, | |||||
err: errStr, | |||||
} | } | ||||
} | } | ||||
return &SQLiteTx{c}, nil | return &SQLiteTx{c}, nil | ||||
} | } | ||||
func errorString(err Error) string { | |||||
return C.GoString(C.sqlite3_errstr(C.int(err.Code))) | |||||
} | |||||
// Open database and return a new connection. | // Open database and return a new connection. | ||||
// | // | ||||
// A pragma can take either zero or one argument. | // A pragma can take either zero or one argument. | ||||
// - rwc | // - rwc | ||||
// - memory | // - memory | ||||
// | // | ||||
// shared | |||||
// cache | |||||
// SQLite Shared-Cache Mode | // SQLite Shared-Cache Mode | ||||
// https://www.sqlite.org/sharedcache.html | // https://www.sqlite.org/sharedcache.html | ||||
// Values: | // Values: | ||||
deferForeignKeys := -1 | deferForeignKeys := -1 | ||||
foreignKeys := -1 | foreignKeys := -1 | ||||
ignoreCheckConstraints := -1 | ignoreCheckConstraints := -1 | ||||
journalMode := "DELETE" | |||||
var journalMode string | |||||
lockingMode := "NORMAL" | lockingMode := "NORMAL" | ||||
queryOnly := -1 | queryOnly := -1 | ||||
recursiveTriggers := -1 | recursiveTriggers := -1 | ||||
if _, ok := params["_locking"]; ok { | if _, ok := params["_locking"]; ok { | ||||
pkey = "_locking" | pkey = "_locking" | ||||
} | } | ||||
if val := params.Get("_locking"); val != "" { | |||||
if val := params.Get(pkey); val != "" { | |||||
switch strings.ToUpper(val) { | switch strings.ToUpper(val) { | ||||
case "NORMAL", "EXCLUSIVE": | case "NORMAL", "EXCLUSIVE": | ||||
lockingMode = strings.ToUpper(val) | lockingMode = strings.ToUpper(val) | ||||
mutex|C.SQLITE_OPEN_READWRITE|C.SQLITE_OPEN_CREATE, | mutex|C.SQLITE_OPEN_READWRITE|C.SQLITE_OPEN_CREATE, | ||||
nil) | nil) | ||||
if rv != 0 { | if rv != 0 { | ||||
// Save off the error _before_ closing the database. | |||||
// This is safe even if db is nil. | |||||
err := lastError(db) | |||||
if db != nil { | if db != nil { | ||||
C.sqlite3_close_v2(db) | C.sqlite3_close_v2(db) | ||||
} | } | ||||
return nil, Error{Code: ErrNo(rv)} | |||||
return nil, err | |||||
} | } | ||||
if db == nil { | if db == nil { | ||||
return nil, errors.New("sqlite succeeded without returning a database") | return nil, errors.New("sqlite succeeded without returning a database") | ||||
// Before going any further, we need to check that the user | // Before going any further, we need to check that the user | ||||
// has provided an username and password within the DSN. | // has provided an username and password within the DSN. | ||||
// We are not allowed to continue. | // We are not allowed to continue. | ||||
if len(authUser) < 0 { | |||||
if len(authUser) == 0 { | |||||
return nil, fmt.Errorf("Missing '_auth_user' while user authentication was requested with '_auth'") | return nil, fmt.Errorf("Missing '_auth_user' while user authentication was requested with '_auth'") | ||||
} | } | ||||
if len(authPass) < 0 { | |||||
if len(authPass) == 0 { | |||||
return nil, fmt.Errorf("Missing '_auth_pass' while user authentication was requested with '_auth'") | return nil, fmt.Errorf("Missing '_auth_pass' while user authentication was requested with '_auth'") | ||||
} | } | ||||
} | } | ||||
// Journal Mode | // Journal Mode | ||||
// Because default Journal Mode is DELETE this PRAGMA can always be executed. | |||||
if err := exec(fmt.Sprintf("PRAGMA journal_mode = %s;", journalMode)); err != nil { | |||||
C.sqlite3_close_v2(db) | |||||
return nil, err | |||||
if journalMode != "" { | |||||
if err := exec(fmt.Sprintf("PRAGMA journal_mode = %s;", journalMode)); err != nil { | |||||
C.sqlite3_close_v2(db) | |||||
return nil, err | |||||
} | |||||
} | } | ||||
// Locking Mode | // Locking Mode | ||||
decltype: nil, | decltype: nil, | ||||
cls: s.cls, | cls: s.cls, | ||||
closed: false, | closed: false, | ||||
done: make(chan struct{}), | |||||
} | |||||
if ctxdone := ctx.Done(); ctxdone != nil { | |||||
go func(db *C.sqlite3) { | |||||
select { | |||||
case <-ctxdone: | |||||
select { | |||||
case <-rows.done: | |||||
default: | |||||
C.sqlite3_interrupt(db) | |||||
rows.Close() | |||||
} | |||||
case <-rows.done: | |||||
} | |||||
}(s.c.db) | |||||
ctx: ctx, | |||||
} | } | ||||
return rows, nil | return rows, nil | ||||
} | } | ||||
// LastInsertId teturn last inserted ID. | |||||
// LastInsertId return last inserted ID. | |||||
func (r *SQLiteResult) LastInsertId() (int64, error) { | func (r *SQLiteResult) LastInsertId() (int64, error) { | ||||
return r.id, nil | return r.id, nil | ||||
} | } | ||||
return s.exec(context.Background(), list) | return s.exec(context.Background(), list) | ||||
} | } | ||||
// exec executes a query that doesn't return rows. Attempts to honor context timeout. | |||||
func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result, error) { | func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result, error) { | ||||
if ctx.Done() == nil { | |||||
return s.execSync(args) | |||||
} | |||||
type result struct { | |||||
r driver.Result | |||||
err error | |||||
} | |||||
resultCh := make(chan result) | |||||
go func() { | |||||
r, err := s.execSync(args) | |||||
resultCh <- result{r, err} | |||||
}() | |||||
select { | |||||
case rv := <-resultCh: | |||||
return rv.r, rv.err | |||||
case <-ctx.Done(): | |||||
select { | |||||
case <-resultCh: // no need to interrupt | |||||
default: | |||||
// this is still racy and can be no-op if executed between sqlite3_* calls in execSync. | |||||
C.sqlite3_interrupt(s.c.db) | |||||
<-resultCh // ensure goroutine completed | |||||
} | |||||
return nil, ctx.Err() | |||||
} | |||||
} | |||||
func (s *SQLiteStmt) execSync(args []namedValue) (driver.Result, error) { | |||||
if err := s.bind(args); err != nil { | if err := s.bind(args); err != nil { | ||||
C.sqlite3_reset(s.s) | C.sqlite3_reset(s.s) | ||||
C.sqlite3_clear_bindings(s.s) | C.sqlite3_clear_bindings(s.s) | ||||
return nil, err | return nil, err | ||||
} | } | ||||
if ctxdone := ctx.Done(); ctxdone != nil { | |||||
done := make(chan struct{}) | |||||
defer close(done) | |||||
go func(db *C.sqlite3) { | |||||
select { | |||||
case <-done: | |||||
case <-ctxdone: | |||||
select { | |||||
case <-done: | |||||
default: | |||||
C.sqlite3_interrupt(db) | |||||
} | |||||
} | |||||
}(s.c.db) | |||||
} | |||||
var rowid, changes C.longlong | var rowid, changes C.longlong | ||||
rv := C._sqlite3_step_row_internal(s.s, &rowid, &changes) | rv := C._sqlite3_step_row_internal(s.s, &rowid, &changes) | ||||
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE { | if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE { | ||||
return nil | return nil | ||||
} | } | ||||
rc.closed = true | rc.closed = true | ||||
if rc.done != nil { | |||||
close(rc.done) | |||||
} | |||||
if rc.cls { | if rc.cls { | ||||
rc.s.mu.Unlock() | rc.s.mu.Unlock() | ||||
return rc.s.Close() | return rc.s.Close() | ||||
return rc.declTypes() | return rc.declTypes() | ||||
} | } | ||||
// Next move cursor to next. | |||||
// Next move cursor to next. Attempts to honor context timeout from QueryContext call. | |||||
func (rc *SQLiteRows) Next(dest []driver.Value) error { | func (rc *SQLiteRows) Next(dest []driver.Value) error { | ||||
rc.s.mu.Lock() | rc.s.mu.Lock() | ||||
defer rc.s.mu.Unlock() | defer rc.s.mu.Unlock() | ||||
if rc.s.closed { | if rc.s.closed { | ||||
return io.EOF | return io.EOF | ||||
} | } | ||||
if rc.ctx.Done() == nil { | |||||
return rc.nextSyncLocked(dest) | |||||
} | |||||
resultCh := make(chan error) | |||||
go func() { | |||||
resultCh <- rc.nextSyncLocked(dest) | |||||
}() | |||||
select { | |||||
case err := <-resultCh: | |||||
return err | |||||
case <-rc.ctx.Done(): | |||||
select { | |||||
case <-resultCh: // no need to interrupt | |||||
default: | |||||
// this is still racy and can be no-op if executed between sqlite3_* calls in nextSyncLocked. | |||||
C.sqlite3_interrupt(rc.s.c.db) | |||||
<-resultCh // ensure goroutine completed | |||||
} | |||||
return rc.ctx.Err() | |||||
} | |||||
} | |||||
// nextSyncLocked moves cursor to next; must be called with locked mutex. | |||||
func (rc *SQLiteRows) nextSyncLocked(dest []driver.Value) error { | |||||
rv := C._sqlite3_step_internal(rc.s.s) | rv := C._sqlite3_step_internal(rc.s.s) | ||||
if rv == C.SQLITE_DONE { | if rv == C.SQLITE_DONE { | ||||
return io.EOF | return io.EOF |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
#cgo CFLAGS: -DUSE_LIBSQLITE3 | #cgo CFLAGS: -DUSE_LIBSQLITE3 | ||||
#cgo linux LDFLAGS: -lsqlite3 | #cgo linux LDFLAGS: -lsqlite3 | ||||
#cgo darwin LDFLAGS: -L/usr/local/opt/sqlite/lib -lsqlite3 | #cgo darwin LDFLAGS: -L/usr/local/opt/sqlite/lib -lsqlite3 | ||||
#cgo darwin CFLAGS: -I/usr/local/opt/sqlite/include | |||||
#cgo openbsd LDFLAGS: -lsqlite3 | #cgo openbsd LDFLAGS: -lsqlite3 | ||||
#cgo solaris LDFLAGS: -lsqlite3 | #cgo solaris LDFLAGS: -lsqlite3 | ||||
*/ | */ |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | |||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>. | |||||
// Copyright (C) 2018 segment.com <friends@segment.com> | |||||
// | |||||
// Use of this source code is governed by an MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
// +build cgo | |||||
package sqlite3 | |||||
// SQLitePreUpdateData represents all of the data available during a | |||||
// pre-update hook call. | |||||
type SQLitePreUpdateData struct { | |||||
Conn *SQLiteConn | |||||
Op int | |||||
DatabaseName string | |||||
TableName string | |||||
OldRowID int64 | |||||
NewRowID int64 | |||||
} |
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>. | |||||
// Copyright (C) 2018 segment.com <friends@segment.com> | |||||
// | |||||
// Use of this source code is governed by an MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
// +build sqlite_preupdate_hook | |||||
package sqlite3 | |||||
/* | |||||
#cgo CFLAGS: -DSQLITE_ENABLE_PREUPDATE_HOOK | |||||
#cgo LDFLAGS: -lm | |||||
#ifndef USE_LIBSQLITE3 | |||||
#include <sqlite3-binding.h> | |||||
#else | |||||
#include <sqlite3.h> | |||||
#endif | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
void preUpdateHookTrampoline(void*, sqlite3 *, int, char *, char *, sqlite3_int64, sqlite3_int64); | |||||
*/ | |||||
import "C" | |||||
import ( | |||||
"errors" | |||||
"unsafe" | |||||
) | |||||
// RegisterPreUpdateHook sets the pre-update hook for a connection. | |||||
// | |||||
// The callback is passed a SQLitePreUpdateData struct with the data for | |||||
// the update, as well as methods for fetching copies of impacted data. | |||||
// | |||||
// If there is an existing update hook for this connection, it will be | |||||
// removed. If callback is nil the existing hook (if any) will be removed | |||||
// without creating a new one. | |||||
func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) { | |||||
if callback == nil { | |||||
C.sqlite3_preupdate_hook(c.db, nil, nil) | |||||
} else { | |||||
C.sqlite3_preupdate_hook(c.db, (*[0]byte)(unsafe.Pointer(C.preUpdateHookTrampoline)), unsafe.Pointer(newHandle(c, callback))) | |||||
} | |||||
} | |||||
// Depth returns the source path of the write, see sqlite3_preupdate_depth() | |||||
func (d *SQLitePreUpdateData) Depth() int { | |||||
return int(C.sqlite3_preupdate_depth(d.Conn.db)) | |||||
} | |||||
// Count returns the number of columns in the row | |||||
func (d *SQLitePreUpdateData) Count() int { | |||||
return int(C.sqlite3_preupdate_count(d.Conn.db)) | |||||
} | |||||
func (d *SQLitePreUpdateData) row(dest []interface{}, new bool) error { | |||||
for i := 0; i < d.Count() && i < len(dest); i++ { | |||||
var val *C.sqlite3_value | |||||
var src interface{} | |||||
// Initially I tried making this just a function pointer argument, but | |||||
// it's absurdly complicated to pass C function pointers. | |||||
if new { | |||||
C.sqlite3_preupdate_new(d.Conn.db, C.int(i), &val) | |||||
} else { | |||||
C.sqlite3_preupdate_old(d.Conn.db, C.int(i), &val) | |||||
} | |||||
switch C.sqlite3_value_type(val) { | |||||
case C.SQLITE_INTEGER: | |||||
src = int64(C.sqlite3_value_int64(val)) | |||||
case C.SQLITE_FLOAT: | |||||
src = float64(C.sqlite3_value_double(val)) | |||||
case C.SQLITE_BLOB: | |||||
len := C.sqlite3_value_bytes(val) | |||||
blobptr := C.sqlite3_value_blob(val) | |||||
src = C.GoBytes(blobptr, len) | |||||
case C.SQLITE_TEXT: | |||||
len := C.sqlite3_value_bytes(val) | |||||
cstrptr := unsafe.Pointer(C.sqlite3_value_text(val)) | |||||
src = C.GoBytes(cstrptr, len) | |||||
case C.SQLITE_NULL: | |||||
src = nil | |||||
} | |||||
err := convertAssign(&dest[i], src) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
// Old populates dest with the row data to be replaced. This works similar to | |||||
// database/sql's Rows.Scan() | |||||
func (d *SQLitePreUpdateData) Old(dest ...interface{}) error { | |||||
if d.Op == SQLITE_INSERT { | |||||
return errors.New("There is no old row for INSERT operations") | |||||
} | |||||
return d.row(dest, false) | |||||
} | |||||
// New populates dest with the replacement row data. This works similar to | |||||
// database/sql's Rows.Scan() | |||||
func (d *SQLitePreUpdateData) New(dest ...interface{}) error { | |||||
if d.Op == SQLITE_DELETE { | |||||
return errors.New("There is no new row for DELETE operations") | |||||
} | |||||
return d.row(dest, true) | |||||
} |
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>. | |||||
// Copyright (C) 2018 segment.com <friends@segment.com> | |||||
// | |||||
// Use of this source code is governed by an MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
// +build !sqlite_preupdate_hook,cgo | |||||
package sqlite3 | |||||
// RegisterPreUpdateHook sets the pre-update hook for a connection. | |||||
// | |||||
// The callback is passed a SQLitePreUpdateData struct with the data for | |||||
// the update, as well as methods for fetching copies of impacted data. | |||||
// | |||||
// If there is an existing update hook for this connection, it will be | |||||
// removed. If callback is nil the existing hook (if any) will be removed | |||||
// without creating a new one. | |||||
func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) { | |||||
// NOOP | |||||
} |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style |
// Copyright (C) 2018 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | // Copyright (C) 2018 G.J.R. Timmer <gjr.timmer@gmail.com>. | ||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2018 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
// Copyright (C) 2016 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
} | } | ||||
expSQLiteCStr := C.sqlite3_expanded_sql((*C.sqlite3_stmt)(pStmt)) | expSQLiteCStr := C.sqlite3_expanded_sql((*C.sqlite3_stmt)(pStmt)) | ||||
defer C.sqlite3_free(unsafe.Pointer(expSQLiteCStr)) | |||||
if expSQLiteCStr == nil { | if expSQLiteCStr == nil { | ||||
fillDBError(&info.DBError, db) | fillDBError(&info.DBError, db) | ||||
return | return |
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | |||||
// Use of this source code is governed by an MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package sqlite3 | package sqlite3 | ||||
/* | /* |
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | // | ||||
// Use of this source code is governed by an MIT-style | // Use of this source code is governed by an MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. |
/* Version 3.28.0 and later */ | /* Version 3.28.0 and later */ | ||||
int (*stmt_isexplain)(sqlite3_stmt*); | int (*stmt_isexplain)(sqlite3_stmt*); | ||||
int (*value_frombind)(sqlite3_value*); | int (*value_frombind)(sqlite3_value*); | ||||
/* Version 3.30.0 and later */ | |||||
int (*drop_modules)(sqlite3*,const char**); | |||||
}; | }; | ||||
/* | /* | ||||
/* Version 3.28.0 and later */ | /* Version 3.28.0 and later */ | ||||
#define sqlite3_stmt_isexplain sqlite3_api->isexplain | #define sqlite3_stmt_isexplain sqlite3_api->isexplain | ||||
#define sqlite3_value_frombind sqlite3_api->frombind | #define sqlite3_value_frombind sqlite3_api->frombind | ||||
/* Version 3.30.0 and later */ | |||||
#define sqlite3_drop_modules sqlite3_api->drop_modules | |||||
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */ | #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */ | ||||
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) | #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) |
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. | |||||
// | |||||
// Use of this source code is governed by an MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
// +build !cgo | // +build !cgo | ||||
package sqlite3 | package sqlite3 |
language: go | language: go | ||||
go: | go: | ||||
- 1.7.3 | |||||
- 1.8.1 | |||||
- 1.9.x | |||||
- 1.10.x | |||||
- 1.11.x | |||||
- tip | - tip | ||||
matrix: | matrix: | ||||
- go: tip | - go: tip | ||||
install: | install: | ||||
- go get github.com/golang/lint/golint | |||||
- go get golang.org/x/lint/golint | |||||
- export PATH=$GOPATH/bin:$PATH | - export PATH=$GOPATH/bin:$PATH | ||||
- go install ./... | - go install ./... | ||||
fmt.Println("flagvar has value ", flagvar) | fmt.Println("flagvar has value ", flagvar) | ||||
``` | ``` | ||||
There are helpers function to get values later if you have the FlagSet but | |||||
it was difficult to keep up with all of the flag pointers in your code. | |||||
There are helper functions available to get the value stored in a Flag if you have a FlagSet but find | |||||
it difficult to keep up with all of the pointers in your code. | |||||
If you have a pflag.FlagSet with a flag called 'flagname' of type int you | If you have a pflag.FlagSet with a flag called 'flagname' of type int you | ||||
can use GetInt() to get the int value. But notice that 'flagname' must exist | can use GetInt() to get the int value. But notice that 'flagname' must exist | ||||
and it must be an int. GetString("flagname") will fail. | and it must be an int. GetString("flagname") will fail. |
return "[" + out + "]" | return "[" + out + "]" | ||||
} | } | ||||
func (s *boolSliceValue) fromString(val string) (bool, error) { | |||||
return strconv.ParseBool(val) | |||||
} | |||||
func (s *boolSliceValue) toString(val bool) string { | |||||
return strconv.FormatBool(val) | |||||
} | |||||
func (s *boolSliceValue) Append(val string) error { | |||||
i, err := s.fromString(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *boolSliceValue) Replace(val []string) error { | |||||
out := make([]bool, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = s.fromString(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *boolSliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = s.toString(d) | |||||
} | |||||
return out | |||||
} | |||||
func boolSliceConv(val string) (interface{}, error) { | func boolSliceConv(val string) (interface{}, error) { | ||||
val = strings.Trim(val, "[]") | val = strings.Trim(val, "[]") | ||||
// Empty string would cause a slice with one (empty) entry | // Empty string would cause a slice with one (empty) entry |
// CountVar defines a count flag with specified name, default value, and usage string. | // CountVar defines a count flag with specified name, default value, and usage string. | ||||
// The argument p points to an int variable in which to store the value of the flag. | // The argument p points to an int variable in which to store the value of the flag. | ||||
// A count flag will add 1 to its value evey time it is found on the command line | |||||
// A count flag will add 1 to its value every time it is found on the command line | |||||
func (f *FlagSet) CountVar(p *int, name string, usage string) { | func (f *FlagSet) CountVar(p *int, name string, usage string) { | ||||
f.CountVarP(p, name, "", usage) | f.CountVarP(p, name, "", usage) | ||||
} | } | ||||
// Count defines a count flag with specified name, default value, and usage string. | // Count defines a count flag with specified name, default value, and usage string. | ||||
// The return value is the address of an int variable that stores the value of the flag. | // The return value is the address of an int variable that stores the value of the flag. | ||||
// A count flag will add 1 to its value evey time it is found on the command line | |||||
// A count flag will add 1 to its value every time it is found on the command line | |||||
func (f *FlagSet) Count(name string, usage string) *int { | func (f *FlagSet) Count(name string, usage string) *int { | ||||
p := new(int) | p := new(int) | ||||
f.CountVarP(p, name, "", usage) | f.CountVarP(p, name, "", usage) |
return "[" + strings.Join(out, ",") + "]" | return "[" + strings.Join(out, ",") + "]" | ||||
} | } | ||||
func (s *durationSliceValue) fromString(val string) (time.Duration, error) { | |||||
return time.ParseDuration(val) | |||||
} | |||||
func (s *durationSliceValue) toString(val time.Duration) string { | |||||
return fmt.Sprintf("%s", val) | |||||
} | |||||
func (s *durationSliceValue) Append(val string) error { | |||||
i, err := s.fromString(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *durationSliceValue) Replace(val []string) error { | |||||
out := make([]time.Duration, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = s.fromString(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *durationSliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = s.toString(d) | |||||
} | |||||
return out | |||||
} | |||||
func durationSliceConv(val string) (interface{}, error) { | func durationSliceConv(val string) (interface{}, error) { | ||||
val = strings.Trim(val, "[]") | val = strings.Trim(val, "[]") | ||||
// Empty string would cause a slice with one (empty) entry | // Empty string would cause a slice with one (empty) entry |
var ip = flag.IntP("flagname", "f", 1234, "help message") | var ip = flag.IntP("flagname", "f", 1234, "help message") | ||||
var flagvar bool | var flagvar bool | ||||
func init() { | func init() { | ||||
flag.BoolVarP("boolname", "b", true, "help message") | |||||
flag.BoolVarP(&flagvar, "boolname", "b", true, "help message") | |||||
} | } | ||||
flag.VarP(&flagVar, "varname", "v", 1234, "help message") | |||||
flag.VarP(&flagval, "varname", "v", "help message") | |||||
Shorthand letters can be used with single dashes on the command line. | Shorthand letters can be used with single dashes on the command line. | ||||
Boolean shorthand flags can be combined with other shorthand flags. | Boolean shorthand flags can be combined with other shorthand flags. | ||||
Type() string | Type() string | ||||
} | } | ||||
// SliceValue is a secondary interface to all flags which hold a list | |||||
// of values. This allows full control over the value of list flags, | |||||
// and avoids complicated marshalling and unmarshalling to csv. | |||||
type SliceValue interface { | |||||
// Append adds the specified value to the end of the flag value list. | |||||
Append(string) error | |||||
// Replace will fully overwrite any data currently in the flag value list. | |||||
Replace([]string) error | |||||
// GetSlice returns the flag value list as an array of strings. | |||||
GetSlice() []string | |||||
} | |||||
// sortFlags returns the flags as a slice in lexicographical sorted order. | // sortFlags returns the flags as a slice in lexicographical sorted order. | ||||
func sortFlags(flags map[NormalizedName]*Flag) []*Flag { | func sortFlags(flags map[NormalizedName]*Flag) []*Flag { | ||||
list := make(sort.StringSlice, len(flags)) | list := make(sort.StringSlice, len(flags)) |
package pflag | |||||
import ( | |||||
"fmt" | |||||
"strconv" | |||||
"strings" | |||||
) | |||||
// -- float32Slice Value | |||||
type float32SliceValue struct { | |||||
value *[]float32 | |||||
changed bool | |||||
} | |||||
func newFloat32SliceValue(val []float32, p *[]float32) *float32SliceValue { | |||||
isv := new(float32SliceValue) | |||||
isv.value = p | |||||
*isv.value = val | |||||
return isv | |||||
} | |||||
func (s *float32SliceValue) Set(val string) error { | |||||
ss := strings.Split(val, ",") | |||||
out := make([]float32, len(ss)) | |||||
for i, d := range ss { | |||||
var err error | |||||
var temp64 float64 | |||||
temp64, err = strconv.ParseFloat(d, 32) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
out[i] = float32(temp64) | |||||
} | |||||
if !s.changed { | |||||
*s.value = out | |||||
} else { | |||||
*s.value = append(*s.value, out...) | |||||
} | |||||
s.changed = true | |||||
return nil | |||||
} | |||||
func (s *float32SliceValue) Type() string { | |||||
return "float32Slice" | |||||
} | |||||
func (s *float32SliceValue) String() string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = fmt.Sprintf("%f", d) | |||||
} | |||||
return "[" + strings.Join(out, ",") + "]" | |||||
} | |||||
func (s *float32SliceValue) fromString(val string) (float32, error) { | |||||
t64, err := strconv.ParseFloat(val, 32) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
return float32(t64), nil | |||||
} | |||||
func (s *float32SliceValue) toString(val float32) string { | |||||
return fmt.Sprintf("%f", val) | |||||
} | |||||
func (s *float32SliceValue) Append(val string) error { | |||||
i, err := s.fromString(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *float32SliceValue) Replace(val []string) error { | |||||
out := make([]float32, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = s.fromString(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *float32SliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = s.toString(d) | |||||
} | |||||
return out | |||||
} | |||||
func float32SliceConv(val string) (interface{}, error) { | |||||
val = strings.Trim(val, "[]") | |||||
// Empty string would cause a slice with one (empty) entry | |||||
if len(val) == 0 { | |||||
return []float32{}, nil | |||||
} | |||||
ss := strings.Split(val, ",") | |||||
out := make([]float32, len(ss)) | |||||
for i, d := range ss { | |||||
var err error | |||||
var temp64 float64 | |||||
temp64, err = strconv.ParseFloat(d, 32) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
out[i] = float32(temp64) | |||||
} | |||||
return out, nil | |||||
} | |||||
// GetFloat32Slice return the []float32 value of a flag with the given name | |||||
func (f *FlagSet) GetFloat32Slice(name string) ([]float32, error) { | |||||
val, err := f.getFlagType(name, "float32Slice", float32SliceConv) | |||||
if err != nil { | |||||
return []float32{}, err | |||||
} | |||||
return val.([]float32), nil | |||||
} | |||||
// Float32SliceVar defines a float32Slice flag with specified name, default value, and usage string. | |||||
// The argument p points to a []float32 variable in which to store the value of the flag. | |||||
func (f *FlagSet) Float32SliceVar(p *[]float32, name string, value []float32, usage string) { | |||||
f.VarP(newFloat32SliceValue(value, p), name, "", usage) | |||||
} | |||||
// Float32SliceVarP is like Float32SliceVar, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) Float32SliceVarP(p *[]float32, name, shorthand string, value []float32, usage string) { | |||||
f.VarP(newFloat32SliceValue(value, p), name, shorthand, usage) | |||||
} | |||||
// Float32SliceVar defines a float32[] flag with specified name, default value, and usage string. | |||||
// The argument p points to a float32[] variable in which to store the value of the flag. | |||||
func Float32SliceVar(p *[]float32, name string, value []float32, usage string) { | |||||
CommandLine.VarP(newFloat32SliceValue(value, p), name, "", usage) | |||||
} | |||||
// Float32SliceVarP is like Float32SliceVar, but accepts a shorthand letter that can be used after a single dash. | |||||
func Float32SliceVarP(p *[]float32, name, shorthand string, value []float32, usage string) { | |||||
CommandLine.VarP(newFloat32SliceValue(value, p), name, shorthand, usage) | |||||
} | |||||
// Float32Slice defines a []float32 flag with specified name, default value, and usage string. | |||||
// The return value is the address of a []float32 variable that stores the value of the flag. | |||||
func (f *FlagSet) Float32Slice(name string, value []float32, usage string) *[]float32 { | |||||
p := []float32{} | |||||
f.Float32SliceVarP(&p, name, "", value, usage) | |||||
return &p | |||||
} | |||||
// Float32SliceP is like Float32Slice, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) Float32SliceP(name, shorthand string, value []float32, usage string) *[]float32 { | |||||
p := []float32{} | |||||
f.Float32SliceVarP(&p, name, shorthand, value, usage) | |||||
return &p | |||||
} | |||||
// Float32Slice defines a []float32 flag with specified name, default value, and usage string. | |||||
// The return value is the address of a []float32 variable that stores the value of the flag. | |||||
func Float32Slice(name string, value []float32, usage string) *[]float32 { | |||||
return CommandLine.Float32SliceP(name, "", value, usage) | |||||
} | |||||
// Float32SliceP is like Float32Slice, but accepts a shorthand letter that can be used after a single dash. | |||||
func Float32SliceP(name, shorthand string, value []float32, usage string) *[]float32 { | |||||
return CommandLine.Float32SliceP(name, shorthand, value, usage) | |||||
} |
package pflag | |||||
import ( | |||||
"fmt" | |||||
"strconv" | |||||
"strings" | |||||
) | |||||
// -- float64Slice Value | |||||
type float64SliceValue struct { | |||||
value *[]float64 | |||||
changed bool | |||||
} | |||||
func newFloat64SliceValue(val []float64, p *[]float64) *float64SliceValue { | |||||
isv := new(float64SliceValue) | |||||
isv.value = p | |||||
*isv.value = val | |||||
return isv | |||||
} | |||||
func (s *float64SliceValue) Set(val string) error { | |||||
ss := strings.Split(val, ",") | |||||
out := make([]float64, len(ss)) | |||||
for i, d := range ss { | |||||
var err error | |||||
out[i], err = strconv.ParseFloat(d, 64) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
if !s.changed { | |||||
*s.value = out | |||||
} else { | |||||
*s.value = append(*s.value, out...) | |||||
} | |||||
s.changed = true | |||||
return nil | |||||
} | |||||
func (s *float64SliceValue) Type() string { | |||||
return "float64Slice" | |||||
} | |||||
func (s *float64SliceValue) String() string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = fmt.Sprintf("%f", d) | |||||
} | |||||
return "[" + strings.Join(out, ",") + "]" | |||||
} | |||||
func (s *float64SliceValue) fromString(val string) (float64, error) { | |||||
return strconv.ParseFloat(val, 64) | |||||
} | |||||
func (s *float64SliceValue) toString(val float64) string { | |||||
return fmt.Sprintf("%f", val) | |||||
} | |||||
func (s *float64SliceValue) Append(val string) error { | |||||
i, err := s.fromString(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *float64SliceValue) Replace(val []string) error { | |||||
out := make([]float64, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = s.fromString(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *float64SliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = s.toString(d) | |||||
} | |||||
return out | |||||
} | |||||
func float64SliceConv(val string) (interface{}, error) { | |||||
val = strings.Trim(val, "[]") | |||||
// Empty string would cause a slice with one (empty) entry | |||||
if len(val) == 0 { | |||||
return []float64{}, nil | |||||
} | |||||
ss := strings.Split(val, ",") | |||||
out := make([]float64, len(ss)) | |||||
for i, d := range ss { | |||||
var err error | |||||
out[i], err = strconv.ParseFloat(d, 64) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
} | |||||
return out, nil | |||||
} | |||||
// GetFloat64Slice return the []float64 value of a flag with the given name | |||||
func (f *FlagSet) GetFloat64Slice(name string) ([]float64, error) { | |||||
val, err := f.getFlagType(name, "float64Slice", float64SliceConv) | |||||
if err != nil { | |||||
return []float64{}, err | |||||
} | |||||
return val.([]float64), nil | |||||
} | |||||
// Float64SliceVar defines a float64Slice flag with specified name, default value, and usage string. | |||||
// The argument p points to a []float64 variable in which to store the value of the flag. | |||||
func (f *FlagSet) Float64SliceVar(p *[]float64, name string, value []float64, usage string) { | |||||
f.VarP(newFloat64SliceValue(value, p), name, "", usage) | |||||
} | |||||
// Float64SliceVarP is like Float64SliceVar, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) Float64SliceVarP(p *[]float64, name, shorthand string, value []float64, usage string) { | |||||
f.VarP(newFloat64SliceValue(value, p), name, shorthand, usage) | |||||
} | |||||
// Float64SliceVar defines a float64[] flag with specified name, default value, and usage string. | |||||
// The argument p points to a float64[] variable in which to store the value of the flag. | |||||
func Float64SliceVar(p *[]float64, name string, value []float64, usage string) { | |||||
CommandLine.VarP(newFloat64SliceValue(value, p), name, "", usage) | |||||
} | |||||
// Float64SliceVarP is like Float64SliceVar, but accepts a shorthand letter that can be used after a single dash. | |||||
func Float64SliceVarP(p *[]float64, name, shorthand string, value []float64, usage string) { | |||||
CommandLine.VarP(newFloat64SliceValue(value, p), name, shorthand, usage) | |||||
} | |||||
// Float64Slice defines a []float64 flag with specified name, default value, and usage string. | |||||
// The return value is the address of a []float64 variable that stores the value of the flag. | |||||
func (f *FlagSet) Float64Slice(name string, value []float64, usage string) *[]float64 { | |||||
p := []float64{} | |||||
f.Float64SliceVarP(&p, name, "", value, usage) | |||||
return &p | |||||
} | |||||
// Float64SliceP is like Float64Slice, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) Float64SliceP(name, shorthand string, value []float64, usage string) *[]float64 { | |||||
p := []float64{} | |||||
f.Float64SliceVarP(&p, name, shorthand, value, usage) | |||||
return &p | |||||
} | |||||
// Float64Slice defines a []float64 flag with specified name, default value, and usage string. | |||||
// The return value is the address of a []float64 variable that stores the value of the flag. | |||||
func Float64Slice(name string, value []float64, usage string) *[]float64 { | |||||
return CommandLine.Float64SliceP(name, "", value, usage) | |||||
} | |||||
// Float64SliceP is like Float64Slice, but accepts a shorthand letter that can be used after a single dash. | |||||
func Float64SliceP(name, shorthand string, value []float64, usage string) *[]float64 { | |||||
return CommandLine.Float64SliceP(name, shorthand, value, usage) | |||||
} |
module github.com/spf13/pflag | |||||
go 1.12 |
package pflag | |||||
import ( | |||||
"fmt" | |||||
"strconv" | |||||
"strings" | |||||
) | |||||
// -- int32Slice Value | |||||
type int32SliceValue struct { | |||||
value *[]int32 | |||||
changed bool | |||||
} | |||||
func newInt32SliceValue(val []int32, p *[]int32) *int32SliceValue { | |||||
isv := new(int32SliceValue) | |||||
isv.value = p | |||||
*isv.value = val | |||||
return isv | |||||
} | |||||
func (s *int32SliceValue) Set(val string) error { | |||||
ss := strings.Split(val, ",") | |||||
out := make([]int32, len(ss)) | |||||
for i, d := range ss { | |||||
var err error | |||||
var temp64 int64 | |||||
temp64, err = strconv.ParseInt(d, 0, 32) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
out[i] = int32(temp64) | |||||
} | |||||
if !s.changed { | |||||
*s.value = out | |||||
} else { | |||||
*s.value = append(*s.value, out...) | |||||
} | |||||
s.changed = true | |||||
return nil | |||||
} | |||||
func (s *int32SliceValue) Type() string { | |||||
return "int32Slice" | |||||
} | |||||
func (s *int32SliceValue) String() string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = fmt.Sprintf("%d", d) | |||||
} | |||||
return "[" + strings.Join(out, ",") + "]" | |||||
} | |||||
func (s *int32SliceValue) fromString(val string) (int32, error) { | |||||
t64, err := strconv.ParseInt(val, 0, 32) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
return int32(t64), nil | |||||
} | |||||
func (s *int32SliceValue) toString(val int32) string { | |||||
return fmt.Sprintf("%d", val) | |||||
} | |||||
func (s *int32SliceValue) Append(val string) error { | |||||
i, err := s.fromString(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *int32SliceValue) Replace(val []string) error { | |||||
out := make([]int32, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = s.fromString(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *int32SliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = s.toString(d) | |||||
} | |||||
return out | |||||
} | |||||
func int32SliceConv(val string) (interface{}, error) { | |||||
val = strings.Trim(val, "[]") | |||||
// Empty string would cause a slice with one (empty) entry | |||||
if len(val) == 0 { | |||||
return []int32{}, nil | |||||
} | |||||
ss := strings.Split(val, ",") | |||||
out := make([]int32, len(ss)) | |||||
for i, d := range ss { | |||||
var err error | |||||
var temp64 int64 | |||||
temp64, err = strconv.ParseInt(d, 0, 32) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
out[i] = int32(temp64) | |||||
} | |||||
return out, nil | |||||
} | |||||
// GetInt32Slice return the []int32 value of a flag with the given name | |||||
func (f *FlagSet) GetInt32Slice(name string) ([]int32, error) { | |||||
val, err := f.getFlagType(name, "int32Slice", int32SliceConv) | |||||
if err != nil { | |||||
return []int32{}, err | |||||
} | |||||
return val.([]int32), nil | |||||
} | |||||
// Int32SliceVar defines a int32Slice flag with specified name, default value, and usage string. | |||||
// The argument p points to a []int32 variable in which to store the value of the flag. | |||||
func (f *FlagSet) Int32SliceVar(p *[]int32, name string, value []int32, usage string) { | |||||
f.VarP(newInt32SliceValue(value, p), name, "", usage) | |||||
} | |||||
// Int32SliceVarP is like Int32SliceVar, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) Int32SliceVarP(p *[]int32, name, shorthand string, value []int32, usage string) { | |||||
f.VarP(newInt32SliceValue(value, p), name, shorthand, usage) | |||||
} | |||||
// Int32SliceVar defines a int32[] flag with specified name, default value, and usage string. | |||||
// The argument p points to a int32[] variable in which to store the value of the flag. | |||||
func Int32SliceVar(p *[]int32, name string, value []int32, usage string) { | |||||
CommandLine.VarP(newInt32SliceValue(value, p), name, "", usage) | |||||
} | |||||
// Int32SliceVarP is like Int32SliceVar, but accepts a shorthand letter that can be used after a single dash. | |||||
func Int32SliceVarP(p *[]int32, name, shorthand string, value []int32, usage string) { | |||||
CommandLine.VarP(newInt32SliceValue(value, p), name, shorthand, usage) | |||||
} | |||||
// Int32Slice defines a []int32 flag with specified name, default value, and usage string. | |||||
// The return value is the address of a []int32 variable that stores the value of the flag. | |||||
func (f *FlagSet) Int32Slice(name string, value []int32, usage string) *[]int32 { | |||||
p := []int32{} | |||||
f.Int32SliceVarP(&p, name, "", value, usage) | |||||
return &p | |||||
} | |||||
// Int32SliceP is like Int32Slice, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) Int32SliceP(name, shorthand string, value []int32, usage string) *[]int32 { | |||||
p := []int32{} | |||||
f.Int32SliceVarP(&p, name, shorthand, value, usage) | |||||
return &p | |||||
} | |||||
// Int32Slice defines a []int32 flag with specified name, default value, and usage string. | |||||
// The return value is the address of a []int32 variable that stores the value of the flag. | |||||
func Int32Slice(name string, value []int32, usage string) *[]int32 { | |||||
return CommandLine.Int32SliceP(name, "", value, usage) | |||||
} | |||||
// Int32SliceP is like Int32Slice, but accepts a shorthand letter that can be used after a single dash. | |||||
func Int32SliceP(name, shorthand string, value []int32, usage string) *[]int32 { | |||||
return CommandLine.Int32SliceP(name, shorthand, value, usage) | |||||
} |
package pflag | |||||
import ( | |||||
"fmt" | |||||
"strconv" | |||||
"strings" | |||||
) | |||||
// -- int64Slice Value | |||||
type int64SliceValue struct { | |||||
value *[]int64 | |||||
changed bool | |||||
} | |||||
func newInt64SliceValue(val []int64, p *[]int64) *int64SliceValue { | |||||
isv := new(int64SliceValue) | |||||
isv.value = p | |||||
*isv.value = val | |||||
return isv | |||||
} | |||||
func (s *int64SliceValue) Set(val string) error { | |||||
ss := strings.Split(val, ",") | |||||
out := make([]int64, len(ss)) | |||||
for i, d := range ss { | |||||
var err error | |||||
out[i], err = strconv.ParseInt(d, 0, 64) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
if !s.changed { | |||||
*s.value = out | |||||
} else { | |||||
*s.value = append(*s.value, out...) | |||||
} | |||||
s.changed = true | |||||
return nil | |||||
} | |||||
func (s *int64SliceValue) Type() string { | |||||
return "int64Slice" | |||||
} | |||||
func (s *int64SliceValue) String() string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = fmt.Sprintf("%d", d) | |||||
} | |||||
return "[" + strings.Join(out, ",") + "]" | |||||
} | |||||
func (s *int64SliceValue) fromString(val string) (int64, error) { | |||||
return strconv.ParseInt(val, 0, 64) | |||||
} | |||||
func (s *int64SliceValue) toString(val int64) string { | |||||
return fmt.Sprintf("%d", val) | |||||
} | |||||
func (s *int64SliceValue) Append(val string) error { | |||||
i, err := s.fromString(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *int64SliceValue) Replace(val []string) error { | |||||
out := make([]int64, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = s.fromString(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *int64SliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = s.toString(d) | |||||
} | |||||
return out | |||||
} | |||||
func int64SliceConv(val string) (interface{}, error) { | |||||
val = strings.Trim(val, "[]") | |||||
// Empty string would cause a slice with one (empty) entry | |||||
if len(val) == 0 { | |||||
return []int64{}, nil | |||||
} | |||||
ss := strings.Split(val, ",") | |||||
out := make([]int64, len(ss)) | |||||
for i, d := range ss { | |||||
var err error | |||||
out[i], err = strconv.ParseInt(d, 0, 64) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
} | |||||
return out, nil | |||||
} | |||||
// GetInt64Slice return the []int64 value of a flag with the given name | |||||
func (f *FlagSet) GetInt64Slice(name string) ([]int64, error) { | |||||
val, err := f.getFlagType(name, "int64Slice", int64SliceConv) | |||||
if err != nil { | |||||
return []int64{}, err | |||||
} | |||||
return val.([]int64), nil | |||||
} | |||||
// Int64SliceVar defines a int64Slice flag with specified name, default value, and usage string. | |||||
// The argument p points to a []int64 variable in which to store the value of the flag. | |||||
func (f *FlagSet) Int64SliceVar(p *[]int64, name string, value []int64, usage string) { | |||||
f.VarP(newInt64SliceValue(value, p), name, "", usage) | |||||
} | |||||
// Int64SliceVarP is like Int64SliceVar, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) Int64SliceVarP(p *[]int64, name, shorthand string, value []int64, usage string) { | |||||
f.VarP(newInt64SliceValue(value, p), name, shorthand, usage) | |||||
} | |||||
// Int64SliceVar defines a int64[] flag with specified name, default value, and usage string. | |||||
// The argument p points to a int64[] variable in which to store the value of the flag. | |||||
func Int64SliceVar(p *[]int64, name string, value []int64, usage string) { | |||||
CommandLine.VarP(newInt64SliceValue(value, p), name, "", usage) | |||||
} | |||||
// Int64SliceVarP is like Int64SliceVar, but accepts a shorthand letter that can be used after a single dash. | |||||
func Int64SliceVarP(p *[]int64, name, shorthand string, value []int64, usage string) { | |||||
CommandLine.VarP(newInt64SliceValue(value, p), name, shorthand, usage) | |||||
} | |||||
// Int64Slice defines a []int64 flag with specified name, default value, and usage string. | |||||
// The return value is the address of a []int64 variable that stores the value of the flag. | |||||
func (f *FlagSet) Int64Slice(name string, value []int64, usage string) *[]int64 { | |||||
p := []int64{} | |||||
f.Int64SliceVarP(&p, name, "", value, usage) | |||||
return &p | |||||
} | |||||
// Int64SliceP is like Int64Slice, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) Int64SliceP(name, shorthand string, value []int64, usage string) *[]int64 { | |||||
p := []int64{} | |||||
f.Int64SliceVarP(&p, name, shorthand, value, usage) | |||||
return &p | |||||
} | |||||
// Int64Slice defines a []int64 flag with specified name, default value, and usage string. | |||||
// The return value is the address of a []int64 variable that stores the value of the flag. | |||||
func Int64Slice(name string, value []int64, usage string) *[]int64 { | |||||
return CommandLine.Int64SliceP(name, "", value, usage) | |||||
} | |||||
// Int64SliceP is like Int64Slice, but accepts a shorthand letter that can be used after a single dash. | |||||
func Int64SliceP(name, shorthand string, value []int64, usage string) *[]int64 { | |||||
return CommandLine.Int64SliceP(name, shorthand, value, usage) | |||||
} |
return "[" + strings.Join(out, ",") + "]" | return "[" + strings.Join(out, ",") + "]" | ||||
} | } | ||||
func (s *intSliceValue) Append(val string) error { | |||||
i, err := strconv.Atoi(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *intSliceValue) Replace(val []string) error { | |||||
out := make([]int, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = strconv.Atoi(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *intSliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = strconv.Itoa(d) | |||||
} | |||||
return out | |||||
} | |||||
func intSliceConv(val string) (interface{}, error) { | func intSliceConv(val string) (interface{}, error) { | ||||
val = strings.Trim(val, "[]") | val = strings.Trim(val, "[]") | ||||
// Empty string would cause a slice with one (empty) entry | // Empty string would cause a slice with one (empty) entry |
return "[" + out + "]" | return "[" + out + "]" | ||||
} | } | ||||
func (s *ipSliceValue) fromString(val string) (net.IP, error) { | |||||
return net.ParseIP(strings.TrimSpace(val)), nil | |||||
} | |||||
func (s *ipSliceValue) toString(val net.IP) string { | |||||
return val.String() | |||||
} | |||||
func (s *ipSliceValue) Append(val string) error { | |||||
i, err := s.fromString(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *ipSliceValue) Replace(val []string) error { | |||||
out := make([]net.IP, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = s.fromString(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *ipSliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = s.toString(d) | |||||
} | |||||
return out | |||||
} | |||||
func ipSliceConv(val string) (interface{}, error) { | func ipSliceConv(val string) (interface{}, error) { | ||||
val = strings.Trim(val, "[]") | val = strings.Trim(val, "[]") | ||||
// Emtpy string would cause a slice with one (empty) entry | |||||
// Empty string would cause a slice with one (empty) entry | |||||
if len(val) == 0 { | if len(val) == 0 { | ||||
return []net.IP{}, nil | return []net.IP{}, nil | ||||
} | } |
return nil | return nil | ||||
} | } | ||||
func (s *stringArrayValue) Append(val string) error { | |||||
*s.value = append(*s.value, val) | |||||
return nil | |||||
} | |||||
func (s *stringArrayValue) Replace(val []string) error { | |||||
out := make([]string, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i] = d | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *stringArrayValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = d | |||||
} | |||||
return out | |||||
} | |||||
func (s *stringArrayValue) Type() string { | func (s *stringArrayValue) Type() string { | ||||
return "stringArray" | return "stringArray" | ||||
} | } |
return "[" + str + "]" | return "[" + str + "]" | ||||
} | } | ||||
func (s *stringSliceValue) Append(val string) error { | |||||
*s.value = append(*s.value, val) | |||||
return nil | |||||
} | |||||
func (s *stringSliceValue) Replace(val []string) error { | |||||
*s.value = val | |||||
return nil | |||||
} | |||||
func (s *stringSliceValue) GetSlice() []string { | |||||
return *s.value | |||||
} | |||||
func stringSliceConv(sval string) (interface{}, error) { | func stringSliceConv(sval string) (interface{}, error) { | ||||
sval = sval[1 : len(sval)-1] | sval = sval[1 : len(sval)-1] | ||||
// An empty string would cause a slice with one (empty) string | // An empty string would cause a slice with one (empty) string | ||||
// The argument p points to a []string variable in which to store the value of the flag. | // The argument p points to a []string variable in which to store the value of the flag. | ||||
// Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. | // Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. | ||||
// For example: | // For example: | ||||
// --ss="v1,v2" -ss="v3" | |||||
// --ss="v1,v2" --ss="v3" | |||||
// will result in | // will result in | ||||
// []string{"v1", "v2", "v3"} | // []string{"v1", "v2", "v3"} | ||||
func (f *FlagSet) StringSliceVar(p *[]string, name string, value []string, usage string) { | func (f *FlagSet) StringSliceVar(p *[]string, name string, value []string, usage string) { | ||||
// The argument p points to a []string variable in which to store the value of the flag. | // The argument p points to a []string variable in which to store the value of the flag. | ||||
// Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. | // Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. | ||||
// For example: | // For example: | ||||
// --ss="v1,v2" -ss="v3" | |||||
// --ss="v1,v2" --ss="v3" | |||||
// will result in | // will result in | ||||
// []string{"v1", "v2", "v3"} | // []string{"v1", "v2", "v3"} | ||||
func StringSliceVar(p *[]string, name string, value []string, usage string) { | func StringSliceVar(p *[]string, name string, value []string, usage string) { | ||||
// The return value is the address of a []string variable that stores the value of the flag. | // The return value is the address of a []string variable that stores the value of the flag. | ||||
// Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. | // Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. | ||||
// For example: | // For example: | ||||
// --ss="v1,v2" -ss="v3" | |||||
// --ss="v1,v2" --ss="v3" | |||||
// will result in | // will result in | ||||
// []string{"v1", "v2", "v3"} | // []string{"v1", "v2", "v3"} | ||||
func (f *FlagSet) StringSlice(name string, value []string, usage string) *[]string { | func (f *FlagSet) StringSlice(name string, value []string, usage string) *[]string { | ||||
// The return value is the address of a []string variable that stores the value of the flag. | // The return value is the address of a []string variable that stores the value of the flag. | ||||
// Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. | // Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. | ||||
// For example: | // For example: | ||||
// --ss="v1,v2" -ss="v3" | |||||
// --ss="v1,v2" --ss="v3" | |||||
// will result in | // will result in | ||||
// []string{"v1", "v2", "v3"} | // []string{"v1", "v2", "v3"} | ||||
func StringSlice(name string, value []string, usage string) *[]string { | func StringSlice(name string, value []string, usage string) *[]string { |
package pflag | |||||
import ( | |||||
"bytes" | |||||
"fmt" | |||||
"strconv" | |||||
"strings" | |||||
) | |||||
// -- stringToInt64 Value | |||||
type stringToInt64Value struct { | |||||
value *map[string]int64 | |||||
changed bool | |||||
} | |||||
func newStringToInt64Value(val map[string]int64, p *map[string]int64) *stringToInt64Value { | |||||
ssv := new(stringToInt64Value) | |||||
ssv.value = p | |||||
*ssv.value = val | |||||
return ssv | |||||
} | |||||
// Format: a=1,b=2 | |||||
func (s *stringToInt64Value) Set(val string) error { | |||||
ss := strings.Split(val, ",") | |||||
out := make(map[string]int64, len(ss)) | |||||
for _, pair := range ss { | |||||
kv := strings.SplitN(pair, "=", 2) | |||||
if len(kv) != 2 { | |||||
return fmt.Errorf("%s must be formatted as key=value", pair) | |||||
} | |||||
var err error | |||||
out[kv[0]], err = strconv.ParseInt(kv[1], 10, 64) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
if !s.changed { | |||||
*s.value = out | |||||
} else { | |||||
for k, v := range out { | |||||
(*s.value)[k] = v | |||||
} | |||||
} | |||||
s.changed = true | |||||
return nil | |||||
} | |||||
func (s *stringToInt64Value) Type() string { | |||||
return "stringToInt64" | |||||
} | |||||
func (s *stringToInt64Value) String() string { | |||||
var buf bytes.Buffer | |||||
i := 0 | |||||
for k, v := range *s.value { | |||||
if i > 0 { | |||||
buf.WriteRune(',') | |||||
} | |||||
buf.WriteString(k) | |||||
buf.WriteRune('=') | |||||
buf.WriteString(strconv.FormatInt(v, 10)) | |||||
i++ | |||||
} | |||||
return "[" + buf.String() + "]" | |||||
} | |||||
func stringToInt64Conv(val string) (interface{}, error) { | |||||
val = strings.Trim(val, "[]") | |||||
// An empty string would cause an empty map | |||||
if len(val) == 0 { | |||||
return map[string]int64{}, nil | |||||
} | |||||
ss := strings.Split(val, ",") | |||||
out := make(map[string]int64, len(ss)) | |||||
for _, pair := range ss { | |||||
kv := strings.SplitN(pair, "=", 2) | |||||
if len(kv) != 2 { | |||||
return nil, fmt.Errorf("%s must be formatted as key=value", pair) | |||||
} | |||||
var err error | |||||
out[kv[0]], err = strconv.ParseInt(kv[1], 10, 64) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
} | |||||
return out, nil | |||||
} | |||||
// GetStringToInt64 return the map[string]int64 value of a flag with the given name | |||||
func (f *FlagSet) GetStringToInt64(name string) (map[string]int64, error) { | |||||
val, err := f.getFlagType(name, "stringToInt64", stringToInt64Conv) | |||||
if err != nil { | |||||
return map[string]int64{}, err | |||||
} | |||||
return val.(map[string]int64), nil | |||||
} | |||||
// StringToInt64Var defines a string flag with specified name, default value, and usage string. | |||||
// The argument p point64s to a map[string]int64 variable in which to store the values of the multiple flags. | |||||
// The value of each argument will not try to be separated by comma | |||||
func (f *FlagSet) StringToInt64Var(p *map[string]int64, name string, value map[string]int64, usage string) { | |||||
f.VarP(newStringToInt64Value(value, p), name, "", usage) | |||||
} | |||||
// StringToInt64VarP is like StringToInt64Var, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) StringToInt64VarP(p *map[string]int64, name, shorthand string, value map[string]int64, usage string) { | |||||
f.VarP(newStringToInt64Value(value, p), name, shorthand, usage) | |||||
} | |||||
// StringToInt64Var defines a string flag with specified name, default value, and usage string. | |||||
// The argument p point64s to a map[string]int64 variable in which to store the value of the flag. | |||||
// The value of each argument will not try to be separated by comma | |||||
func StringToInt64Var(p *map[string]int64, name string, value map[string]int64, usage string) { | |||||
CommandLine.VarP(newStringToInt64Value(value, p), name, "", usage) | |||||
} | |||||
// StringToInt64VarP is like StringToInt64Var, but accepts a shorthand letter that can be used after a single dash. | |||||
func StringToInt64VarP(p *map[string]int64, name, shorthand string, value map[string]int64, usage string) { | |||||
CommandLine.VarP(newStringToInt64Value(value, p), name, shorthand, usage) | |||||
} | |||||
// StringToInt64 defines a string flag with specified name, default value, and usage string. | |||||
// The return value is the address of a map[string]int64 variable that stores the value of the flag. | |||||
// The value of each argument will not try to be separated by comma | |||||
func (f *FlagSet) StringToInt64(name string, value map[string]int64, usage string) *map[string]int64 { | |||||
p := map[string]int64{} | |||||
f.StringToInt64VarP(&p, name, "", value, usage) | |||||
return &p | |||||
} | |||||
// StringToInt64P is like StringToInt64, but accepts a shorthand letter that can be used after a single dash. | |||||
func (f *FlagSet) StringToInt64P(name, shorthand string, value map[string]int64, usage string) *map[string]int64 { | |||||
p := map[string]int64{} | |||||
f.StringToInt64VarP(&p, name, shorthand, value, usage) | |||||
return &p | |||||
} | |||||
// StringToInt64 defines a string flag with specified name, default value, and usage string. | |||||
// The return value is the address of a map[string]int64 variable that stores the value of the flag. | |||||
// The value of each argument will not try to be separated by comma | |||||
func StringToInt64(name string, value map[string]int64, usage string) *map[string]int64 { | |||||
return CommandLine.StringToInt64P(name, "", value, usage) | |||||
} | |||||
// StringToInt64P is like StringToInt64, but accepts a shorthand letter that can be used after a single dash. | |||||
func StringToInt64P(name, shorthand string, value map[string]int64, usage string) *map[string]int64 { | |||||
return CommandLine.StringToInt64P(name, shorthand, value, usage) | |||||
} |
return "[" + strings.Join(out, ",") + "]" | return "[" + strings.Join(out, ",") + "]" | ||||
} | } | ||||
func (s *uintSliceValue) fromString(val string) (uint, error) { | |||||
t, err := strconv.ParseUint(val, 10, 0) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
return uint(t), nil | |||||
} | |||||
func (s *uintSliceValue) toString(val uint) string { | |||||
return fmt.Sprintf("%d", val) | |||||
} | |||||
func (s *uintSliceValue) Append(val string) error { | |||||
i, err := s.fromString(val) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*s.value = append(*s.value, i) | |||||
return nil | |||||
} | |||||
func (s *uintSliceValue) Replace(val []string) error { | |||||
out := make([]uint, len(val)) | |||||
for i, d := range val { | |||||
var err error | |||||
out[i], err = s.fromString(d) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
*s.value = out | |||||
return nil | |||||
} | |||||
func (s *uintSliceValue) GetSlice() []string { | |||||
out := make([]string, len(*s.value)) | |||||
for i, d := range *s.value { | |||||
out[i] = s.toString(d) | |||||
} | |||||
return out | |||||
} | |||||
func uintSliceConv(val string) (interface{}, error) { | func uintSliceConv(val string) (interface{}, error) { | ||||
val = strings.Trim(val, "[]") | val = strings.Trim(val, "[]") | ||||
// Empty string would cause a slice with one (empty) entry | // Empty string would cause a slice with one (empty) entry |
language: go | |||||
go: | |||||
- '1.9' | |||||
- '1.10' | |||||
services: | |||||
- postgresql | |||||
- mysql | |||||
addons: | |||||
postgresql: "9.4" | |||||
before_script: | |||||
- mysql -e 'CREATE DATABASE testfixtures_test;' | |||||
- psql -c 'CREATE DATABASE testfixtures_test;' -U postgres | |||||
install: | |||||
- go get -t -tags 'sqlite postgresql mysql' ./... | |||||
- curl -s https://raw.githubusercontent.com/go-task/task/master/install-task.sh | sh | |||||
- bin/task dl-deps | |||||
- cp .sample.env .env | |||||
script: | |||||
- bin/task lint | |||||
- bin/task test-free |
# Go Test Fixtures | |||||
[![GoDoc](https://godoc.org/gopkg.in/testfixtures.v2?status.svg)](https://godoc.org/gopkg.in/testfixtures.v2) | |||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-testfixtures/testfixtures)](https://goreportcard.com/report/github.com/go-testfixtures/testfixtures) | |||||
[![Build Status](https://travis-ci.org/go-testfixtures/testfixtures.svg?branch=master)](https://travis-ci.org/go-testfixtures/testfixtures) | |||||
[![Build status](https://ci.appveyor.com/api/projects/status/d2h6gq37wxbus1x7?svg=true)](https://ci.appveyor.com/project/andreynering/testfixtures) | |||||
> ***Warning***: this package will wipe the database data before loading the | |||||
fixtures! It is supposed to be used on a test database. Please, double check | |||||
if you are running it against the correct database. | |||||
Writing tests is hard, even more when you have to deal with an SQL database. | |||||
This package aims to make writing functional tests for web apps written in | |||||
Go easier. | |||||
Basically this package mimics the ["Rails' way"][railstests] of writing tests | |||||
for database applications, where sample data is kept in fixtures files. Before | |||||
the execution of every test, the test database is cleaned and the fixture data | |||||
is loaded into the database. | |||||
The idea is running tests against a real database, instead of relying in mocks, | |||||
which is boring to setup and may lead to production bugs not being caught in | |||||
the tests. | |||||
## Installation | |||||
First, get it: | |||||
```bash | |||||
go get -u -v gopkg.in/testfixtures.v2 | |||||
``` | |||||
## Usage | |||||
Create a folder for the fixture files. Each file should contain data for a | |||||
single table and have the name `<table_name>.yml`: | |||||
``` | |||||
myapp/ | |||||
myapp.go | |||||
myapp_test.go | |||||
... | |||||
fixtures/ | |||||
posts.yml | |||||
comments.yml | |||||
tags.yml | |||||
posts_tags.yml | |||||
... | |||||
``` | |||||
The file would look like this (it can have as many record you want): | |||||
```yml | |||||
# comments.yml | |||||
- id: 1 | |||||
post_id: 1 | |||||
content: A comment... | |||||
author_name: John Doe | |||||
author_email: john@doe.com | |||||
created_at: 2016-01-01 12:30:12 | |||||
updated_at: 2016-01-01 12:30:12 | |||||
- id: 2 | |||||
post_id: 2 | |||||
content: Another comment... | |||||
author_name: John Doe | |||||
author_email: john@doe.com | |||||
created_at: 2016-01-01 12:30:12 | |||||
updated_at: 2016-01-01 12:30:12 | |||||
# ... | |||||
``` | |||||
An YAML object or array will be converted to JSON. It can be stored on a native | |||||
JSON type like JSONB on PostgreSQL or as a TEXT or VARCHAR column on other | |||||
databases. | |||||
```yml | |||||
- id: 1 | |||||
post_attributes: | |||||
author: John Due | |||||
author_email: john@due.com | |||||
title: "..." | |||||
tags: | |||||
- programming | |||||
- go | |||||
- testing | |||||
post: "..." | |||||
``` | |||||
If you need to write raw SQL, probably to call a function, prefix the value | |||||
of the column with `RAW=`: | |||||
```yml | |||||
- id: 1 | |||||
uuid_column: RAW=uuid_generate_v4() | |||||
postgis_type_column: RAW=ST_GeomFromText('params...') | |||||
created_at: RAW=NOW() | |||||
updated_at: RAW=NOW() | |||||
``` | |||||
Your tests would look like this: | |||||
```go | |||||
package myapp | |||||
import ( | |||||
"database/sql" | |||||
"log" | |||||
_ "github.com/lib/pq" | |||||
"gopkg.in/testfixtures.v2" | |||||
) | |||||
var ( | |||||
db *sql.DB | |||||
fixtures *testfixtures.Context | |||||
) | |||||
func TestMain(m *testing.M) { | |||||
var err error | |||||
// Open connection with the test database. | |||||
// Do NOT import fixtures in a production database! | |||||
// Existing data would be deleted | |||||
db, err = sql.Open("postgres", "dbname=myapp_test") | |||||
if err != nil { | |||||
log.Fatal(err) | |||||
} | |||||
// creating the context that hold the fixtures | |||||
// see about all compatible databases in this page below | |||||
fixtures, err = testfixtures.NewFolder(db, &testfixtures.PostgreSQL{}, "testdata/fixtures") | |||||
if err != nil { | |||||
log.Fatal(err) | |||||
} | |||||
os.Exit(m.Run()) | |||||
} | |||||
func prepareTestDatabase() { | |||||
if err := fixtures.Load(); err != nil { | |||||
log.Fatal(err) | |||||
} | |||||
} | |||||
func TestX(t *testing.T) { | |||||
prepareTestDatabase() | |||||
// your test here ... | |||||
} | |||||
func TestY(t *testing.T) { | |||||
prepareTestDatabase() | |||||
// your test here ... | |||||
} | |||||
func TestZ(t *testing.T) { | |||||
prepareTestDatabase() | |||||
// your test here ... | |||||
} | |||||
``` | |||||
Alternatively, you can use the `NewFiles` function, to specify which | |||||
files you want to load into the database: | |||||
```go | |||||
fixtures, err := testfixtures.NewFiles(db, &testfixtures.PostgreSQL{}, | |||||
"fixtures/orders.yml", | |||||
"fixtures/customers.yml", | |||||
// add as many files you want | |||||
) | |||||
if err != nil { | |||||
log.Fatal(err) | |||||
} | |||||
``` | |||||
## Security check | |||||
In order to prevent you from accidentally wiping the wrong database, this | |||||
package will refuse to load fixtures if the database name (or database | |||||
filename for SQLite) doesn't contains "test". If you want to disable this | |||||
check, use: | |||||
```go | |||||
testfixtures.SkipDatabaseNameCheck(true) | |||||
``` | |||||
## Sequences | |||||
For PostgreSQL or Oracle, this package also resets all sequences to a high | |||||
number to prevent duplicated primary keys while running the tests. | |||||
The default is 10000, but you can change that with: | |||||
```go | |||||
testfixtures.ResetSequencesTo(10000) | |||||
``` | |||||
## Compatible databases | |||||
### PostgreSQL | |||||
This package has two approaches to disable foreign keys while importing fixtures | |||||
in PostgreSQL databases: | |||||
#### With `DISABLE TRIGGER` | |||||
This is the default approach. For that use: | |||||
```go | |||||
&testfixtures.PostgreSQL{} | |||||
``` | |||||
With the above snippet this package will use `DISABLE TRIGGER` to temporarily | |||||
disabling foreign key constraints while loading fixtures. This work with any | |||||
version of PostgreSQL, but it is **required** to be connected in the database | |||||
as a SUPERUSER. You can make a PostgreSQL user a SUPERUSER with: | |||||
```sql | |||||
ALTER USER your_user SUPERUSER; | |||||
``` | |||||
#### With `ALTER CONSTRAINT` | |||||
This approach don't require to be connected as a SUPERUSER, but only work with | |||||
PostgreSQL versions >= 9.4. Try this if you are getting foreign key violation | |||||
errors with the previous approach. It is as simple as using: | |||||
```go | |||||
&testfixtures.PostgreSQL{UseAlterConstraint: true} | |||||
``` | |||||
### MySQL / MariaDB | |||||
Just make sure the connection string have | |||||
[the multistatement parameter](https://github.com/go-sql-driver/mysql#multistatements) | |||||
set to true, and use: | |||||
```go | |||||
&testfixtures.MySQL{} | |||||
``` | |||||
### SQLite | |||||
SQLite is also supported. It is recommended to create foreign keys as | |||||
`DEFERRABLE` (the default) to prevent problems. See more | |||||
[on the SQLite documentation](https://www.sqlite.org/foreignkeys.html#fk_deferred). | |||||
(Foreign key constraints are no-op by default on SQLite, but enabling it is | |||||
recommended). | |||||
```go | |||||
&testfixtures.SQLite{} | |||||
``` | |||||
### Microsoft SQL Server | |||||
SQL Server support requires SQL Server >= 2008. Inserting on `IDENTITY` columns | |||||
are handled as well. Just make sure you are logged in with a user with | |||||
`ALTER TABLE` permission. | |||||
```go | |||||
&testfixtures.SQLServer{} | |||||
``` | |||||
### Oracle | |||||
Oracle is supported as well. Use: | |||||
```go | |||||
&testfixtures.Oracle{} | |||||
``` | |||||
## Generating fixtures for a existing database (experimental) | |||||
The following code will generate a YAML file for each table of the database in | |||||
the given folder. It may be useful to boostrap a test scenario from a sample | |||||
database of your app. | |||||
```go | |||||
err := testfixtures.GenerateFixtures(db, &testfixtures.PostgreSQL{}, "testdata/fixtures") | |||||
if err != nil { | |||||
log.Fatalf("Error generating fixtures: %v", err) | |||||
} | |||||
``` | |||||
Or | |||||
```go | |||||
err := testfixtures.GenerateFixturesForTables( | |||||
db, | |||||
[]*TableInfo{ | |||||
&TableInfo{Name: "table_name", Where: "foo = 'bar'"}, | |||||
// ... | |||||
}, | |||||
&testfixtures.PostgreSQL{}, | |||||
"testdata/fixtures", | |||||
) | |||||
if err != nil { | |||||
log.Fatalf("Error generating fixtures: %v", err) | |||||
} | |||||
``` | |||||
> This was thought to run in small sample databases. It will likely break | |||||
if run in a production/big database. | |||||
## Contributing | |||||
Tests were written to ensure everything work as expected. You can run the tests | |||||
with: | |||||
```bash | |||||
# running tests for PostgreSQL | |||||
go test -tags postgresql | |||||
# running test for MySQL | |||||
go test -tags mysql | |||||
# running tests for SQLite | |||||
go test -tags sqlite | |||||
# running tests for SQL Server | |||||
go test -tags sqlserver | |||||
# running tests for Oracle | |||||
go test -tags oracle | |||||
# running test for multiple databases at once | |||||
go test -tags 'sqlite postgresql mysql' | |||||
# running tests + benchmark | |||||
go test -v -bench=. -tags postgresql | |||||
``` | |||||
Travis runs tests for PostgreSQL, MySQL and SQLite. AppVeyor run for all | |||||
these and also Microsoft SQL Server. | |||||
To set the connection string of tests for each database, copy the `.sample.env` | |||||
file as `.env` and edit it according to your environment. | |||||
## Alternatives | |||||
If you don't think using fixtures is a good idea, you can try one of these | |||||
packages instead: | |||||
- [factory-go][factorygo]: Factory for Go. Inspired by Python's Factory Boy | |||||
and Ruby's Factory Girl | |||||
- [go-txdb (Single transaction SQL driver for Go)][gotxdb]: Use a single | |||||
database transaction for each functional test, so you can rollback to | |||||
previous state between tests to have the same database state in all tests | |||||
- [go-sqlmock][gosqlmock]: A mock for the sql.DB interface. This allow you to | |||||
unit test database code without having to connect to a real database | |||||
- [dbcleaner][dbcleaner] - Clean database for testing, inspired by | |||||
database_cleaner for Ruby | |||||
[railstests]: http://guides.rubyonrails.org/testing.html#the-test-database | |||||
[gotxdb]: https://github.com/DATA-DOG/go-txdb | |||||
[gosqlmock]: https://github.com/DATA-DOG/go-sqlmock | |||||
[factorygo]: https://github.com/bluele/factory-go | |||||
[dbcleaner]: https://github.com/khaiql/dbcleaner |
# github.com/go-task/task | |||||
version: '2' | |||||
tasks: | |||||
dl-deps: | |||||
desc: Download cli deps | |||||
cmds: | |||||
- go get -u github.com/golang/lint/golint | |||||
lint: | |||||
desc: Runs golint | |||||
cmds: | |||||
- golint . | |||||
test-free: | |||||
desc: Test free databases (PG, MySQL and SQLite) | |||||
cmds: | |||||
- task: test-pg | |||||
- task: test-mysql | |||||
- task: test-sqlite | |||||
test-all: | |||||
desc: Test all databases (PG, MySQL, SQLite, SQLServer and Oracle) | |||||
cmds: | |||||
- task: test-pg | |||||
- task: test-mysql | |||||
- task: test-sqlite | |||||
- task: test-sqlserver | |||||
- task: test-oracle | |||||
test-pg: | |||||
desc: Test PostgreSQL | |||||
cmds: | |||||
- task: test-db | |||||
vars: {DATABASE: postgresql} | |||||
test-mysql: | |||||
desc: Test MySQL | |||||
cmds: | |||||
- task: test-db | |||||
vars: {DATABASE: mysql} | |||||
test-sqlite: | |||||
desc: Test SQLite | |||||
cmds: | |||||
- task: test-db | |||||
vars: {DATABASE: sqlite} | |||||
test-sqlserver: | |||||
desc: Test SQLServer | |||||
cmds: | |||||
- task: test-db | |||||
vars: {DATABASE: sqlserver} | |||||
test-oracle: | |||||
desc: Test Oracle | |||||
cmds: | |||||
- task: test-db | |||||
vars: {DATABASE: oracle} | |||||
test-db: | |||||
cmds: | |||||
- go test -v -tags {{.DATABASE}} |
version: '{build}' | |||||
clone_folder: C:\GOPATH\src\gopkg.in\testfixtures.v2 | |||||
build: false | |||||
deploy: false | |||||
services: | |||||
- postgresql96 | |||||
- mysql | |||||
- mssql2017 | |||||
environment: | |||||
POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 | |||||
PGUSER: postgres | |||||
PGPASSWORD: Password12! | |||||
PG_CONN_STRING: 'user=postgres password=Password12! dbname=testfixtures_test sslmode=disable' | |||||
MYSQL_PATH: C:\Program Files\MySql\MySQL Server 5.7 | |||||
MYSQL_PWD: Password12! | |||||
MYSQL_CONN_STRING: 'root:Password12!@/testfixtures_test?multiStatements=true' | |||||
SQLITE_CONN_STRING: 'testdb.sqlite3' | |||||
SQLSERVER_CONN_STRING: 'server=localhost;database=testfixtures_test;user id=sa;password=Password12!;encrypt=disable' | |||||
MINGW_PATH: C:\MinGW | |||||
GOPATH: C:\GOPATH | |||||
GOVERSION: 1.10.3 | |||||
install: | |||||
- SET PATH=%POSTGRES_PATH%\bin;%MYSQL_PATH%\bin;%MINGW_PATH%\bin;%PATH% | |||||
- rmdir C:\go /s /q | |||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-386.msi | |||||
- msiexec /i go%GOVERSION%.windows-386.msi /q | |||||
- go version | |||||
build_script: | |||||
- createdb testfixtures_test | |||||
- mysql -e "CREATE DATABASE testfixtures_test;" --user=root | |||||
- sqlcmd -S localhost,1433 -U sa -P Password12! -Q "CREATE DATABASE testfixtures_test" -d "master" | |||||
test_script: | |||||
- go get -t -tags "sqlite postgresql mysql sqlserver" ./... | |||||
- go install -v ./... | |||||
- go test -v -tags postgresql | |||||
- go test -v -tags mysql | |||||
- go test -v -tags sqlserver | |||||
- go test -v -tags sqlite |
package testfixtures | |||||
import ( | |||||
"database/sql" | |||||
) | |||||
type ( | |||||
// DataBaseHelper is the helper interface | |||||
// Deprecated: Use Helper instead | |||||
DataBaseHelper Helper | |||||
// PostgreSQLHelper is the PostgreSQL helper | |||||
// Deprecated: Use PostgreSQL{} instead | |||||
PostgreSQLHelper struct { | |||||
PostgreSQL | |||||
UseAlterConstraint bool | |||||
} | |||||
// MySQLHelper is the MySQL helper | |||||
// Deprecated: Use MySQL{} instead | |||||
MySQLHelper struct { | |||||
MySQL | |||||
} | |||||
// SQLiteHelper is the SQLite helper | |||||
// Deprecated: Use SQLite{} instead | |||||
SQLiteHelper struct { | |||||
SQLite | |||||
} | |||||
// SQLServerHelper is the SQLServer helper | |||||
// Deprecated: Use SQLServer{} instead | |||||
SQLServerHelper struct { | |||||
SQLServer | |||||
} | |||||
// OracleHelper is the Oracle helper | |||||
// Deprecated: Use Oracle{} instead | |||||
OracleHelper struct { | |||||
Oracle | |||||
} | |||||
) | |||||
func (h *PostgreSQLHelper) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) error { | |||||
h.PostgreSQL.UseAlterConstraint = h.UseAlterConstraint | |||||
return h.PostgreSQL.disableReferentialIntegrity(db, loadFn) | |||||
} | |||||
// LoadFixtureFiles load all specified fixtures files into database: | |||||
// LoadFixtureFiles(db, &PostgreSQL{}, | |||||
// "fixtures/customers.yml", "fixtures/orders.yml") | |||||
// // add as many files you want | |||||
// | |||||
// Deprecated: Use NewFiles() and Load() instead. | |||||
func LoadFixtureFiles(db *sql.DB, helper Helper, files ...string) error { | |||||
c, err := NewFiles(db, helper, files...) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return c.Load() | |||||
} | |||||
// LoadFixtures loads all fixtures in a given folder into the database: | |||||
// LoadFixtures("myfixturesfolder", db, &PostgreSQL{}) | |||||
// | |||||
// Deprecated: Use NewFolder() and Load() instead. | |||||
func LoadFixtures(folderName string, db *sql.DB, helper Helper) error { | |||||
c, err := NewFolder(db, helper, folderName) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return c.Load() | |||||
} |
package testfixtures | |||||
import ( | |||||
"errors" | |||||
"fmt" | |||||
) | |||||
var ( | |||||
// ErrWrongCastNotAMap is returned when a map is not a map[interface{}]interface{} | |||||
ErrWrongCastNotAMap = errors.New("Could not cast record: not a map[interface{}]interface{}") | |||||
// ErrFileIsNotSliceOrMap is returned the the fixture file is not a slice or map. | |||||
ErrFileIsNotSliceOrMap = errors.New("The fixture file is not a slice or map") | |||||
// ErrKeyIsNotString is returned when a record is not of type string | |||||
ErrKeyIsNotString = errors.New("Record map key is not string") | |||||
// ErrNotTestDatabase is returned when the database name doesn't contains "test" | |||||
ErrNotTestDatabase = errors.New(`Loading aborted because the database name does not contains "test"`) | |||||
) | |||||
// InsertError will be returned if any error happens on database while | |||||
// inserting the record | |||||
type InsertError struct { | |||||
Err error | |||||
File string | |||||
Index int | |||||
SQL string | |||||
Params []interface{} | |||||
} | |||||
func (e *InsertError) Error() string { | |||||
return fmt.Sprintf( | |||||
"testfixtures: error inserting record: %v, on file: %s, index: %d, sql: %s, params: %v", | |||||
e.Err, | |||||
e.File, | |||||
e.Index, | |||||
e.SQL, | |||||
e.Params, | |||||
) | |||||
} |
package testfixtures | |||||
import ( | |||||
"database/sql" | |||||
"fmt" | |||||
"os" | |||||
"path" | |||||
"unicode/utf8" | |||||
"gopkg.in/yaml.v2" | |||||
) | |||||
// TableInfo is settings for generating a fixture for table. | |||||
type TableInfo struct { | |||||
Name string // Table name | |||||
Where string // A condition for extracting records. If this value is empty, extracts all records. | |||||
} | |||||
func (ti *TableInfo) whereClause() string { | |||||
if ti.Where == "" { | |||||
return "" | |||||
} | |||||
return fmt.Sprintf(" WHERE %s", ti.Where) | |||||
} | |||||
// GenerateFixtures generates fixtures for the current contents of a database, and saves | |||||
// them to the specified directory | |||||
func GenerateFixtures(db *sql.DB, helper Helper, dir string) error { | |||||
tables, err := helper.tableNames(db) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
for _, table := range tables { | |||||
filename := path.Join(dir, table+".yml") | |||||
if err := generateFixturesForTable(db, helper, &TableInfo{Name: table}, filename); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
// GenerateFixturesForTables generates fixtures for the current contents of specified tables in a database, and saves | |||||
// them to the specified directory | |||||
func GenerateFixturesForTables(db *sql.DB, tables []*TableInfo, helper Helper, dir string) error { | |||||
for _, table := range tables { | |||||
filename := path.Join(dir, table.Name+".yml") | |||||
if err := generateFixturesForTable(db, helper, table, filename); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func generateFixturesForTable(db *sql.DB, h Helper, table *TableInfo, filename string) error { | |||||
query := fmt.Sprintf("SELECT * FROM %s%s", h.quoteKeyword(table.Name), table.whereClause()) | |||||
rows, err := db.Query(query) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer rows.Close() | |||||
columns, err := rows.Columns() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
fixtures := make([]interface{}, 0, 10) | |||||
for rows.Next() { | |||||
entries := make([]interface{}, len(columns)) | |||||
entryPtrs := make([]interface{}, len(entries)) | |||||
for i := range entries { | |||||
entryPtrs[i] = &entries[i] | |||||
} | |||||
if err := rows.Scan(entryPtrs...); err != nil { | |||||
return err | |||||
} | |||||
entryMap := make(map[string]interface{}, len(entries)) | |||||
for i, column := range columns { | |||||
entryMap[column] = convertValue(entries[i]) | |||||
} | |||||
fixtures = append(fixtures, entryMap) | |||||
} | |||||
if err = rows.Err(); err != nil { | |||||
return err | |||||
} | |||||
f, err := os.Create(filename) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer f.Close() | |||||
marshaled, err := yaml.Marshal(fixtures) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
_, err = f.Write(marshaled) | |||||
return err | |||||
} | |||||
func convertValue(value interface{}) interface{} { | |||||
switch v := value.(type) { | |||||
case []byte: | |||||
if utf8.Valid(v) { | |||||
return string(v) | |||||
} | |||||
} | |||||
return value | |||||
} |
package testfixtures | |||||
var ( | |||||
skipDatabaseNameCheck bool | |||||
resetSequencesTo int64 = 10000 | |||||
) | |||||
// SkipDatabaseNameCheck If true, loading fixtures will not check if the database | |||||
// name constaint "test". Use with caution! | |||||
func SkipDatabaseNameCheck(value bool) { | |||||
skipDatabaseNameCheck = value | |||||
} | |||||
// ResetSequencesTo sets the value the sequences will be reset to. | |||||
// This is used by PostgreSQL and Oracle. | |||||
// Defaults to 10000. | |||||
func ResetSequencesTo(value int64) { | |||||
resetSequencesTo = value | |||||
} |
package testfixtures | |||||
import ( | |||||
"database/sql" | |||||
"fmt" | |||||
"strings" | |||||
) | |||||
// Oracle is the Oracle database helper for this package | |||||
type Oracle struct { | |||||
baseHelper | |||||
enabledConstraints []oracleConstraint | |||||
sequences []string | |||||
} | |||||
type oracleConstraint struct { | |||||
tableName string | |||||
constraintName string | |||||
} | |||||
func (h *Oracle) init(db *sql.DB) error { | |||||
var err error | |||||
h.enabledConstraints, err = h.getEnabledConstraints(db) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
h.sequences, err = h.getSequences(db) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (*Oracle) paramType() int { | |||||
return paramTypeColon | |||||
} | |||||
func (*Oracle) quoteKeyword(str string) string { | |||||
return fmt.Sprintf("\"%s\"", strings.ToUpper(str)) | |||||
} | |||||
func (*Oracle) databaseName(q queryable) (string, error) { | |||||
var dbName string | |||||
err := q.QueryRow("SELECT user FROM DUAL").Scan(&dbName) | |||||
return dbName, err | |||||
} | |||||
func (*Oracle) tableNames(q queryable) ([]string, error) { | |||||
query := ` | |||||
SELECT TABLE_NAME | |||||
FROM USER_TABLES | |||||
` | |||||
rows, err := q.Query(query) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
defer rows.Close() | |||||
var tables []string | |||||
for rows.Next() { | |||||
var table string | |||||
if err = rows.Scan(&table); err != nil { | |||||
return nil, err | |||||
} | |||||
tables = append(tables, table) | |||||
} | |||||
if err = rows.Err(); err != nil { | |||||
return nil, err | |||||
} | |||||
return tables, nil | |||||
} | |||||
func (*Oracle) getEnabledConstraints(q queryable) ([]oracleConstraint, error) { | |||||
var constraints []oracleConstraint | |||||
rows, err := q.Query(` | |||||
SELECT table_name, constraint_name | |||||
FROM user_constraints | |||||
WHERE constraint_type = 'R' | |||||
AND status = 'ENABLED' | |||||
`) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
defer rows.Close() | |||||
for rows.Next() { | |||||
var constraint oracleConstraint | |||||
rows.Scan(&constraint.tableName, &constraint.constraintName) | |||||
constraints = append(constraints, constraint) | |||||
} | |||||
if err = rows.Err(); err != nil { | |||||
return nil, err | |||||
} | |||||
return constraints, nil | |||||
} | |||||
func (*Oracle) getSequences(q queryable) ([]string, error) { | |||||
var sequences []string | |||||
rows, err := q.Query("SELECT sequence_name FROM user_sequences") | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
defer rows.Close() | |||||
for rows.Next() { | |||||
var sequence string | |||||
if err = rows.Scan(&sequence); err != nil { | |||||
return nil, err | |||||
} | |||||
sequences = append(sequences, sequence) | |||||
} | |||||
if err = rows.Err(); err != nil { | |||||
return nil, err | |||||
} | |||||
return sequences, nil | |||||
} | |||||
func (h *Oracle) resetSequences(q queryable) error { | |||||
for _, sequence := range h.sequences { | |||||
_, err := q.Exec(fmt.Sprintf("DROP SEQUENCE %s", h.quoteKeyword(sequence))) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
_, err = q.Exec(fmt.Sprintf("CREATE SEQUENCE %s START WITH %d", h.quoteKeyword(sequence), resetSequencesTo)) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func (h *Oracle) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { | |||||
// re-enable after load | |||||
defer func() { | |||||
for _, c := range h.enabledConstraints { | |||||
_, err2 := db.Exec(fmt.Sprintf("ALTER TABLE %s ENABLE CONSTRAINT %s", h.quoteKeyword(c.tableName), h.quoteKeyword(c.constraintName))) | |||||
if err2 != nil && err == nil { | |||||
err = err2 | |||||
} | |||||
} | |||||
}() | |||||
// disable foreign keys | |||||
for _, c := range h.enabledConstraints { | |||||
_, err := db.Exec(fmt.Sprintf("ALTER TABLE %s DISABLE CONSTRAINT %s", h.quoteKeyword(c.tableName), h.quoteKeyword(c.constraintName))) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
tx, err := db.Begin() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer tx.Rollback() | |||||
if err = loadFn(tx); err != nil { | |||||
return err | |||||
} | |||||
if err = tx.Commit(); err != nil { | |||||
return err | |||||
} | |||||
return h.resetSequences(db) | |||||
} |
package testfixtures | |||||
import ( | |||||
"database/sql" | |||||
"fmt" | |||||
"io/ioutil" | |||||
"path" | |||||
"path/filepath" | |||||
"regexp" | |||||
"strings" | |||||
"gopkg.in/yaml.v2" | |||||
) | |||||
// Context holds the fixtures to be loaded in the database. | |||||
type Context struct { | |||||
db *sql.DB | |||||
helper Helper | |||||
fixturesFiles []*fixtureFile | |||||
} | |||||
type fixtureFile struct { | |||||
path string | |||||
fileName string | |||||
content []byte | |||||
insertSQLs []insertSQL | |||||
} | |||||
type insertSQL struct { | |||||
sql string | |||||
params []interface{} | |||||
} | |||||
var ( | |||||
dbnameRegexp = regexp.MustCompile("(?i)test") | |||||
) | |||||
// NewFolder creates a context for all fixtures in a given folder into the database: | |||||
// NewFolder(db, &PostgreSQL{}, "my/fixtures/folder") | |||||
func NewFolder(db *sql.DB, helper Helper, folderName string) (*Context, error) { | |||||
fixtures, err := fixturesFromFolder(folderName) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
c, err := newContext(db, helper, fixtures) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return c, nil | |||||
} | |||||
// NewFiles creates a context for all specified fixtures files into database: | |||||
// NewFiles(db, &PostgreSQL{}, | |||||
// "fixtures/customers.yml", | |||||
// "fixtures/orders.yml" | |||||
// // add as many files you want | |||||
// ) | |||||
func NewFiles(db *sql.DB, helper Helper, fileNames ...string) (*Context, error) { | |||||
fixtures, err := fixturesFromFiles(fileNames...) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
c, err := newContext(db, helper, fixtures) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return c, nil | |||||
} | |||||
func newContext(db *sql.DB, helper Helper, fixtures []*fixtureFile) (*Context, error) { | |||||
c := &Context{ | |||||
db: db, | |||||
helper: helper, | |||||
fixturesFiles: fixtures, | |||||
} | |||||
if err := c.helper.init(c.db); err != nil { | |||||
return nil, err | |||||
} | |||||
if err := c.buildInsertSQLs(); err != nil { | |||||
return nil, err | |||||
} | |||||
return c, nil | |||||
} | |||||
// DetectTestDatabase returns nil if databaseName matches regexp | |||||
// if err := fixtures.DetectTestDatabase(); err != nil { | |||||
// log.Fatal(err) | |||||
// } | |||||
func (c *Context) DetectTestDatabase() error { | |||||
dbName, err := c.helper.databaseName(c.db) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if !dbnameRegexp.MatchString(dbName) { | |||||
return ErrNotTestDatabase | |||||
} | |||||
return nil | |||||
} | |||||
// Load wipes and after load all fixtures in the database. | |||||
// if err := fixtures.Load(); err != nil { | |||||
// log.Fatal(err) | |||||
// } | |||||
func (c *Context) Load() error { | |||||
if !skipDatabaseNameCheck { | |||||
if err := c.DetectTestDatabase(); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
err := c.helper.disableReferentialIntegrity(c.db, func(tx *sql.Tx) error { | |||||
for _, file := range c.fixturesFiles { | |||||
modified, err := c.helper.isTableModified(tx, file.fileNameWithoutExtension()) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if !modified { | |||||
continue | |||||
} | |||||
if err := file.delete(tx, c.helper); err != nil { | |||||
return err | |||||
} | |||||
err = c.helper.whileInsertOnTable(tx, file.fileNameWithoutExtension(), func() error { | |||||
for j, i := range file.insertSQLs { | |||||
if _, err := tx.Exec(i.sql, i.params...); err != nil { | |||||
return &InsertError{ | |||||
Err: err, | |||||
File: file.fileName, | |||||
Index: j, | |||||
SQL: i.sql, | |||||
Params: i.params, | |||||
} | |||||
} | |||||
} | |||||
return nil | |||||
}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return c.helper.afterLoad(c.db) | |||||
} | |||||
func (c *Context) buildInsertSQLs() error { | |||||
for _, f := range c.fixturesFiles { | |||||
var records interface{} | |||||
if err := yaml.Unmarshal(f.content, &records); err != nil { | |||||
return err | |||||
} | |||||
switch records := records.(type) { | |||||
case []interface{}: | |||||
for _, record := range records { | |||||
recordMap, ok := record.(map[interface{}]interface{}) | |||||
if !ok { | |||||
return ErrWrongCastNotAMap | |||||
} | |||||
sql, values, err := f.buildInsertSQL(c.helper, recordMap) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) | |||||
} | |||||
case map[interface{}]interface{}: | |||||
for _, record := range records { | |||||
recordMap, ok := record.(map[interface{}]interface{}) | |||||
if !ok { | |||||
return ErrWrongCastNotAMap | |||||
} | |||||
sql, values, err := f.buildInsertSQL(c.helper, recordMap) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) | |||||
} | |||||
default: | |||||
return ErrFileIsNotSliceOrMap | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func (f *fixtureFile) fileNameWithoutExtension() string { | |||||
return strings.Replace(f.fileName, filepath.Ext(f.fileName), "", 1) | |||||
} | |||||
func (f *fixtureFile) delete(tx *sql.Tx, h Helper) error { | |||||
_, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", h.quoteKeyword(f.fileNameWithoutExtension()))) | |||||
return err | |||||
} | |||||
func (f *fixtureFile) buildInsertSQL(h Helper, record map[interface{}]interface{}) (sqlStr string, values []interface{}, err error) { | |||||
var ( | |||||
sqlColumns []string | |||||
sqlValues []string | |||||
i = 1 | |||||
) | |||||
for key, value := range record { | |||||
keyStr, ok := key.(string) | |||||
if !ok { | |||||
err = ErrKeyIsNotString | |||||
return | |||||
} | |||||
sqlColumns = append(sqlColumns, h.quoteKeyword(keyStr)) | |||||
// if string, try convert to SQL or time | |||||
// if map or array, convert to json | |||||
switch v := value.(type) { | |||||
case string: | |||||
if strings.HasPrefix(v, "RAW=") { | |||||
sqlValues = append(sqlValues, strings.TrimPrefix(v, "RAW=")) | |||||
continue | |||||
} | |||||
if t, err := tryStrToDate(v); err == nil { | |||||
value = t | |||||
} | |||||
case []interface{}, map[interface{}]interface{}: | |||||
value = recursiveToJSON(v) | |||||
} | |||||
switch h.paramType() { | |||||
case paramTypeDollar: | |||||
sqlValues = append(sqlValues, fmt.Sprintf("$%d", i)) | |||||
case paramTypeQuestion: | |||||
sqlValues = append(sqlValues, "?") | |||||
case paramTypeColon: | |||||
sqlValues = append(sqlValues, fmt.Sprintf(":%d", i)) | |||||
} | |||||
values = append(values, value) | |||||
i++ | |||||
} | |||||
sqlStr = fmt.Sprintf( | |||||
"INSERT INTO %s (%s) VALUES (%s)", | |||||
h.quoteKeyword(f.fileNameWithoutExtension()), | |||||
strings.Join(sqlColumns, ", "), | |||||
strings.Join(sqlValues, ", "), | |||||
) | |||||
return | |||||
} | |||||
func fixturesFromFolder(folderName string) ([]*fixtureFile, error) { | |||||
var files []*fixtureFile | |||||
fileinfos, err := ioutil.ReadDir(folderName) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
for _, fileinfo := range fileinfos { | |||||
if !fileinfo.IsDir() && filepath.Ext(fileinfo.Name()) == ".yml" { | |||||
fixture := &fixtureFile{ | |||||
path: path.Join(folderName, fileinfo.Name()), | |||||
fileName: fileinfo.Name(), | |||||
} | |||||
fixture.content, err = ioutil.ReadFile(fixture.path) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
files = append(files, fixture) | |||||
} | |||||
} | |||||
return files, nil | |||||
} | |||||
func fixturesFromFiles(fileNames ...string) ([]*fixtureFile, error) { | |||||
var ( | |||||
fixtureFiles []*fixtureFile | |||||
err error | |||||
) | |||||
for _, f := range fileNames { | |||||
fixture := &fixtureFile{ | |||||
path: f, | |||||
fileName: filepath.Base(f), | |||||
} | |||||
fixture.content, err = ioutil.ReadFile(fixture.path) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
fixtureFiles = append(fixtureFiles, fixture) | |||||
} | |||||
return fixtureFiles, nil | |||||
} |
package testfixtures | |||||
import ( | |||||
"errors" | |||||
"time" | |||||
) | |||||
var timeFormats = []string{ | |||||
"2006-01-02", | |||||
"2006-01-02 15:04", | |||||
"2006-01-02 15:04:05", | |||||
"20060102", | |||||
"20060102 15:04", | |||||
"20060102 15:04:05", | |||||
"02/01/2006", | |||||
"02/01/2006 15:04", | |||||
"02/01/2006 15:04:05", | |||||
"2006-01-02T15:04-07:00", | |||||
"2006-01-02T15:04:05-07:00", | |||||
} | |||||
// ErrCouldNotConvertToTime is returns when a string is not a reconizable time format | |||||
var ErrCouldNotConvertToTime = errors.New("Could not convert string to time") | |||||
func tryStrToDate(s string) (time.Time, error) { | |||||
for _, f := range timeFormats { | |||||
t, err := time.ParseInLocation(f, s, time.Local) | |||||
if err != nil { | |||||
continue | |||||
} | |||||
return t, nil | |||||
} | |||||
return time.Time{}, ErrCouldNotConvertToTime | |||||
} |
raw_buffer: make([]byte, 0, output_raw_buffer_size), | raw_buffer: make([]byte, 0, output_raw_buffer_size), | ||||
states: make([]yaml_emitter_state_t, 0, initial_stack_size), | states: make([]yaml_emitter_state_t, 0, initial_stack_size), | ||||
events: make([]yaml_event_t, 0, initial_queue_size), | events: make([]yaml_event_t, 0, initial_queue_size), | ||||
best_width: -1, | |||||
} | } | ||||
} | } | ||||
github.com/go-swagger/go-swagger/codescan | github.com/go-swagger/go-swagger/codescan | ||||
github.com/go-swagger/go-swagger/generator | github.com/go-swagger/go-swagger/generator | ||||
github.com/go-swagger/go-swagger/scan | github.com/go-swagger/go-swagger/scan | ||||
# github.com/go-testfixtures/testfixtures/v3 v3.2.0 | |||||
## explicit | |||||
github.com/go-testfixtures/testfixtures/v3 | |||||
# github.com/gobwas/glob v0.2.3 | # github.com/gobwas/glob v0.2.3 | ||||
## explicit | ## explicit | ||||
github.com/gobwas/glob | github.com/gobwas/glob | ||||
github.com/jessevdk/go-flags | github.com/jessevdk/go-flags | ||||
# github.com/jmhodges/levigo v1.0.0 | # github.com/jmhodges/levigo v1.0.0 | ||||
## explicit | ## explicit | ||||
# github.com/joho/godotenv v1.3.0 | |||||
## explicit | |||||
# github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 | # github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 | ||||
## explicit | ## explicit | ||||
github.com/kballard/go-shellquote | github.com/kballard/go-shellquote | ||||
# github.com/mattn/go-isatty v0.0.11 | # github.com/mattn/go-isatty v0.0.11 | ||||
## explicit | ## explicit | ||||
github.com/mattn/go-isatty | github.com/mattn/go-isatty | ||||
# github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d | |||||
## explicit | |||||
# github.com/mattn/go-runewidth v0.0.7 | # github.com/mattn/go-runewidth v0.0.7 | ||||
github.com/mattn/go-runewidth | github.com/mattn/go-runewidth | ||||
# github.com/mattn/go-sqlite3 v1.11.0 | |||||
# github.com/mattn/go-sqlite3 v2.0.2+incompatible | |||||
## explicit | ## explicit | ||||
github.com/mattn/go-sqlite3 | github.com/mattn/go-sqlite3 | ||||
# github.com/matttproud/golang_protobuf_extensions v1.0.1 | # github.com/matttproud/golang_protobuf_extensions v1.0.1 | ||||
github.com/spf13/cast | github.com/spf13/cast | ||||
# github.com/spf13/jwalterweatherman v1.1.0 | # github.com/spf13/jwalterweatherman v1.1.0 | ||||
github.com/spf13/jwalterweatherman | github.com/spf13/jwalterweatherman | ||||
# github.com/spf13/pflag v1.0.3 | |||||
# github.com/spf13/pflag v1.0.5 | |||||
github.com/spf13/pflag | github.com/spf13/pflag | ||||
# github.com/spf13/viper v1.4.0 | # github.com/spf13/viper v1.4.0 | ||||
github.com/spf13/viper | github.com/spf13/viper | ||||
# gopkg.in/ldap.v3 v3.0.2 | # gopkg.in/ldap.v3 v3.0.2 | ||||
## explicit | ## explicit | ||||
gopkg.in/ldap.v3 | gopkg.in/ldap.v3 | ||||
# gopkg.in/testfixtures.v2 v2.5.0 | |||||
## explicit | |||||
gopkg.in/testfixtures.v2 | |||||
# gopkg.in/warnings.v0 v0.1.2 | # gopkg.in/warnings.v0 v0.1.2 | ||||
gopkg.in/warnings.v0 | gopkg.in/warnings.v0 | ||||
# gopkg.in/yaml.v2 v2.2.8 | |||||
# gopkg.in/yaml.v2 v2.3.0 | |||||
## explicit | ## explicit | ||||
gopkg.in/yaml.v2 | gopkg.in/yaml.v2 | ||||
# mvdan.cc/xurls/v2 v2.1.0 | # mvdan.cc/xurls/v2 v2.1.0 |